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"; } } }