diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..91f2707 --- /dev/null +++ b/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..dd99283 --- /dev/null +++ b/.project @@ -0,0 +1,42 @@ + + + oauth + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope new file mode 100644 index 0000000..b72a6a4 --- /dev/null +++ b/.settings/.jsdtscope @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..839d647 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..443e085 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..09dd0d8 --- /dev/null +++ b/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..cc81385 --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..fffd28a --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/.settings/org.eclipse.wst.validation.prefs b/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 0000000..04cad8c --- /dev/null +++ b/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,2 @@ +disabled=06target +eclipse.preferences.version=1 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..50447cf --- /dev/null +++ b/pom.xml @@ -0,0 +1,178 @@ + + + 4.0.0 + + maven-parent + org.gcube.tools + 1.0.0 + + + + org.gcube.portal + oauth + war + 1.0.0-SNAPSHOT + oauth gCube App + + + 1.7 + 2.22.1 + ${project.basedir}/distro + ${project.build.directory}/${project.build.finalName} + distro + UTF-8 + UTF-8 + + + + scm:svn:http://svn.d4science.research-infrastructures.eu/gcube/trunk/portal/${project.artifactId} + scm:https://svn.d4science.research-infrastructures.eu/gcube/trunk/portal/${project.artifactId} + http://svn.d4science.research-infrastructures.eu/gcube/trunk/portal/${project.artifactId} + + + + + + org.gcube.distribution + maven-smartgears-bom + LATEST + pom + import + + + + + + + + org.gcube.core + common-smartgears + provided + + + org.gcube.core + common-smartgears-app + compile + + + javax.servlet + servlet-api + 3.0-alpha-1 + compile + + + org.glassfish.jersey.containers + + jersey-container-servlet-core + ${version.jersey} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${version.jersey} + + + org.glassfish.jersey.media + jersey-media-json-processing + ${version.jersey} + + + org.glassfish.jersey.media + jersey-media-multipart + ${version.jersey} + + + org.glassfish.jersey.media + jersey-media-sse + ${version.jersey} + + + org.glassfish.jersey.ext + jersey-bean-validation + ${version.jersey} + + + junit + junit + 3.8.1 + test + + + + ${name} + + + org.apache.maven.plugins + maven-war-plugin + 2.1.1 + + + compile + + exploded + + + + + ${webappDirectory} + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2 + + + ${distroDirectory}/descriptor.xml + + + + + servicearchive + install + + single + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.5 + + + copy-profile + install + + copy-resources + + + target + + + ${distroDirectory} + true + + profile.xml + + + + + + + + + + diff --git a/src/main/java/org/gcube/portal/oauth/OauthService.java b/src/main/java/org/gcube/portal/oauth/OauthService.java new file mode 100644 index 0000000..8ca4c36 --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/OauthService.java @@ -0,0 +1,174 @@ +package org.gcube.portal.oauth; + +import static org.gcube.common.authorization.client.Constants.authorizationService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.gcube.common.authorization.library.ClientType; +import org.gcube.common.authorization.library.provider.AuthorizationProvider; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.authorization.library.utils.Caller; +import org.gcube.portal.oauth.cache.CacheBean; +import org.gcube.portal.oauth.cache.CacheCleaner; +import org.gcube.portal.oauth.input.PushCodeBean; +import org.gcube.portal.oauth.output.AccessTokenBeanResponse; +import org.gcube.portal.oauth.output.AccessTokenErrorResponse; +import org.slf4j.LoggerFactory; + + +@Path("v2/") +@Singleton +public class OauthService { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OauthService.class); + + /** + * This map contains couples + */ + private Map entries = new ConcurrentHashMap(); + + /** + * Since this is a singleton sub-service, there will be just one call to this constructor and one running thread + * to clean up expired codes. + */ + public OauthService() { + CacheCleaner cleaner = new CacheCleaner(entries); + cleaner.start(); + } + + /** + * Used to check that the token type is of type user + * @param clientType + * @param token + * @return + */ + private boolean checkIsQualifierTokenType(ClientType clientType){ + return clientType.equals(ClientType.USER); + } + + /** + * Used to check that the token type is of type application + * @param clientType + * @param token + * @return + */ + private boolean checkIsapplicationTokenType(ClientType clientType){ + return clientType.equals(ClientType.EXTERNALSERVICE); + } + + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("push-authentication-code") + /** + * The portal will push a qualified token together a code + * @return Response with status 201 if the code has been saved correctly + */ + public Response pushAuthCode(PushCodeBean bean) { + + logger.info("Request to push "); + + Caller caller = AuthorizationProvider.instance.get(); + String token = SecurityTokenProvider.instance.get(); + Status status = Status.CREATED; + + if(!checkIsQualifierTokenType(caller.getClient().getType())){ + status = Status.FORBIDDEN; + logger.warn("Trying to access users method via a token different than USER is not allowed"); + return Response.status(status).entity("{\"error\"=\"Trying to access push-authentication-code method via a token different than USER is not allowed\"").build(); + }else{ + + logger.info("Saving entry defined by " + bean + " in cache, token is " + token.substring(0, 10)); + entries.put(bean.getCode(), new CacheBean(token, bean.getRedirectUri(), bean.getClientId(), System.currentTimeMillis())); + return Response.status(status).build(); + } + + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + @Path("access-token") + /** + * The method should accept input values or in a json object or as FormParam. The request is validated here and not from SmartGears. + * @param requestInJson + * @param clientId + * @param clientSecret + * @param redirectUri + * @param code + * @param grantType + * @return + */ + public Response tokenRequest( + @FormParam("client_id") String clientId, + @FormParam("client_secret") String clientSecret, // i.e., application token + @FormParam("redirect_uri") String redirectUri, + @FormParam("code") String code, + @FormParam("grant_type") String grantType // it must always be equal to "authorization_code" + ){ + + Status status = Status.BAD_REQUEST; + logger.info("Request to exchange code for token"); + logger.info("Params are + client_id = " + clientId + ", client_secret = " + clientSecret.substring(0, 10) + ", redirect_uri = " +redirectUri + ", code = " + code.substring(0, 10)); + + try{ + // check if something is missing + String errorMessage = checkRequest(clientId, clientSecret, redirectUri, code, grantType); + if(errorMessage != null){ + logger.error("The request fails because of " + errorMessage); + return Response.status(status).entity(new AccessTokenErrorResponse(errorMessage, null, null)).build(); + }else{ + logger.info("The request is ok"); + String tokenToReturn = entries.get(code).getToken(); + status = Status.OK; + return Response.status(status).entity(new AccessTokenBeanResponse(tokenToReturn, authorizationService().get(tokenToReturn).getContext())).build(); + } + }catch(Exception e){ + logger.error("Failed to perform this operation", e); + status = Status.BAD_REQUEST; + return Response.status(status).entity(new AccessTokenErrorResponse("invalid_request", null, null)).build(); + } + } + + /** + * Check request parameters + * @param clientId + * @param clientSecret + * @param redirectUri + * @param code + * @param grantType + * @return see https://tools.ietf.org/html/rfc6749#section-5.2 + */ + private String checkRequest(String clientId, String clientSecret, + String redirectUri, String code, String grantType) { + + try{ + if(clientId == null || clientSecret == null || redirectUri == null || code == null || grantType == null) + return "invalid_request"; + if(clientId.isEmpty() || clientSecret.isEmpty() || redirectUri.isEmpty() || code.isEmpty() || grantType.isEmpty()) + return "invalid_request"; + if(!checkIsapplicationTokenType(authorizationService().get(clientSecret).getClientInfo().getType())) // it is not an app token or it is not a token + return "invalid_client"; + if(!entries.containsKey(code) || CacheBean.isExpired(entries.get(code))) + return "invalid_grant"; + if(!grantType.equals("authorization_code")) + return "unsupported_grant_type"; + return null; + }catch(Exception e){ + logger.error("Failed to check the correctness of the request", e); + return "invalid_request"; + } + } +} diff --git a/src/main/java/org/gcube/portal/oauth/cache/CacheBean.java b/src/main/java/org/gcube/portal/oauth/cache/CacheBean.java new file mode 100644 index 0000000..5201ef9 --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/cache/CacheBean.java @@ -0,0 +1,79 @@ +package org.gcube.portal.oauth.cache; + + +/** + * A cache bean object for oauth support + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public class CacheBean { + + private String token; + private String redirectUri; + private String clientId; + private Long insertTime; + private static final int TOKEN_TTL = 1000 * 10; + + /** + * @param token + * @param redirectUri + * @param clientId + * @param insertTime + */ + public CacheBean(String token, String redirectUri, String clientId, + Long insertTime) { + super(); + this.token = token; + this.redirectUri = redirectUri; + this.clientId = clientId; + this.insertTime = insertTime; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Long getInsertTime() { + return insertTime; + } + + public void setInsertTime(Long insertTime) { + this.insertTime = insertTime; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public String toString() { + return "CacheBean [token=" + token + ", redirectUri=" + redirectUri + + ", clientId=" + clientId + ", insertTime=" + insertTime + "]"; + } + + /** + * True if the code expired, false otherwise + * @return + */ + public static boolean isExpired(CacheBean bean){ + + return System.currentTimeMillis() > TOKEN_TTL + bean.insertTime; + + } + +} diff --git a/src/main/java/org/gcube/portal/oauth/cache/CacheCleaner.java b/src/main/java/org/gcube/portal/oauth/cache/CacheCleaner.java new file mode 100644 index 0000000..c8f2690 --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/cache/CacheCleaner.java @@ -0,0 +1,60 @@ +package org.gcube.portal.oauth.cache; + +import java.util.Date; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.LoggerFactory; + + +/** + * This thread cleans a cache by removing expired entries. + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public class CacheCleaner extends Thread { + + private Map cacheReference; + private static final int CHECK_AFTER_MS = 1000 * 60 * 10; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CacheCleaner.class); + + /** + * Build a cleaner thread. + * @param cache + */ + public CacheCleaner(Map cache) { + this.cacheReference = cache; + } + + @Override + public void run() { + + while (!isInterrupted()) { + + try { + + sleep(CHECK_AFTER_MS); + logger.info("Going to clean up cache and old codes [" + new Date() + "]"); + + Iterator> iterator = cacheReference.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator + .next(); + if(CacheBean.isExpired(entry.getValue())){ + logger.debug("Removing entry " + entry.getValue()); + iterator.remove(); + } + } + + logger.info("Going to sleep [" + new Date() + "]"); + + } catch (InterruptedException e) { + logger.warn("Exception was " + e.getMessage()); + continue; + } + + } + } + +} diff --git a/src/main/java/org/gcube/portal/oauth/input/PushCodeBean.java b/src/main/java/org/gcube/portal/oauth/input/PushCodeBean.java new file mode 100644 index 0000000..15ed14f --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/input/PushCodeBean.java @@ -0,0 +1,67 @@ +package org.gcube.portal.oauth.input; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The code to be pushed into the cache of codes (plus some other informations) + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public class PushCodeBean { + + @JsonProperty("code") + @NotNull(message="code cannot be null") + private String code; + + @JsonProperty("redirect_uri") + @NotNull(message="redirect_uri cannot be null") + private String redirectUri; + + @JsonProperty("client_id") + @NotNull(message="client_id cannot be null") + private String clientId; + + /** + * @param code + * @param redirectUri + * @param clientId + */ + public PushCodeBean(String code, String redirectUri, String clientId) { + super(); + this.code = code; + this.redirectUri = redirectUri; + this.clientId = clientId; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public String toString() { + return "PushCodeBean [code=" + code + ", redirectUri=" + redirectUri + + ", clientId=" + clientId + "]"; + } + +} diff --git a/src/main/java/org/gcube/portal/oauth/output/AccessTokenBeanResponse.java b/src/main/java/org/gcube/portal/oauth/output/AccessTokenBeanResponse.java new file mode 100644 index 0000000..e22ed12 --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/output/AccessTokenBeanResponse.java @@ -0,0 +1,62 @@ +package org.gcube.portal.oauth.output; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response to a request token. + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public class AccessTokenBeanResponse { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("expires_in") + private static final Long expiresIn = Long.MAX_VALUE; // the number of seconds remaining (max long value) + + @JsonProperty("scope") + private String scope; + + @JsonProperty("token_type") + private static final String tokenType = "Bearer"; + + /** + * @param accessToken + * @param scope + */ + public AccessTokenBeanResponse(String accessToken, String scope) { + super(); + this.accessToken = accessToken; + this.scope = scope; + } + + public static Long getExpiresin() { + return expiresIn; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public static String getTokentype() { + return tokenType; + } + + @Override + public String toString() { + return "AccessTokenBeanResponse [accessToken=" + accessToken + + ", scope=" + scope + "]"; + } +} diff --git a/src/main/java/org/gcube/portal/oauth/output/AccessTokenErrorResponse.java b/src/main/java/org/gcube/portal/oauth/output/AccessTokenErrorResponse.java new file mode 100644 index 0000000..afd60da --- /dev/null +++ b/src/main/java/org/gcube/portal/oauth/output/AccessTokenErrorResponse.java @@ -0,0 +1,66 @@ +package org.gcube.portal.oauth.output; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Bean used on failed request + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public class AccessTokenErrorResponse { + + @NotNull + @JsonProperty("error") + private String error; + + @JsonProperty("error_description") + private String errorDescription; + + @JsonProperty("error_uri") + private String errorUri; + + /** + * @param error + * @param errorDescription + * @param errorUri + */ + public AccessTokenErrorResponse(String error, String errorDescription, + String errorUri) { + super(); + this.error = error; + this.errorDescription = errorDescription; + this.errorUri = errorUri; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getErrorUri() { + return errorUri; + } + + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + @Override + public String toString() { + return "AccessTokenErrorResponse [error=" + error + + ", errorDescription=" + errorDescription + ", errorUri=" + + errorUri + "]"; + } +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..9f88c1f --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,7 @@ + + + + Archetype Created Web Application + diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp new file mode 100644 index 0000000..767f206 --- /dev/null +++ b/src/main/webapp/index.jsp @@ -0,0 +1,5 @@ + + +

The gCube OAUTH web service is up and running!

+ +