package org.gcube.portal.oauth; import static org.gcube.common.authorization.client.Constants.authorizationService; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.xml.bind.DatatypeConverter; import org.gcube.common.authorization.library.ClientType; import org.gcube.portal.oauth.cache.MemCachedBean; import org.gcube.portal.oauth.output.AccessTokenBeanResponse; import org.gcube.portal.oauth.output.AccessTokenErrorResponse; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.slf4j.LoggerFactory; import net.spy.memcached.MemcachedClient; @Path("/v2") @Singleton public class OauthService { public static final String OAUTH_TOKEN_GET_METHOD_NAME_REQUEST = "access-token"; private static final String GRANT_TYPE_VALUE = "authorization_code"; private static final String AUTHORIZATION_HEADER = "Authorization"; private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OauthService.class); /** * This map contains couples */ private MemcachedClient entries; /** * 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() { logger.info("Singleton gcube-oauth service built."); entries = DistributedCacheClient.getInstance().getMemcachedClient(); } @Override protected void finalize(){ entries.shutdown(); } /** * 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); } @GET @Path("check") @Produces(MediaType.TEXT_PLAIN) public Response checkService(){ return Response.status(Status.OK).entity("Ready!").build(); } @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) @Path(OAUTH_TOKEN_GET_METHOD_NAME_REQUEST) /** * 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" @Context HttpServletRequest request ){ Status status = Status.BAD_REQUEST; logger.info("Request to exchange code for token"); try{ CredentialsBean credentials = new CredentialsBean(clientId, clientSecret); if (clientId == null) credentials = getCredentialFromBasicAuthorization(request); else if (request.getHeader(AUTHORIZATION_HEADER)!=null) throw new Exception("the client MUST NOT use more than one authentication method"); logger.info("Params are client_id = " + credentials.getClientId() + ", client_secret = " + credentials.getClientSecret() + "*******************"+ ", redirect_uri = " +redirectUri + ", code = " + code + "*******************" + ", grant_type = " + grantType); // check if something is missing MemCachedBean cachedBean = checkRequest(credentials, redirectUri, code, grantType, request); if(!cachedBean.isSuccess()){ String errorMessage = cachedBean.getErrorMessage(); logger.error("The request fails because of " + errorMessage); return Response.status(status).entity(new AccessTokenErrorResponse(errorMessage, null)).build(); }else{ logger.info("The request is ok"); String tokenToReturn = cachedBean.getToken(); String scope = cachedBean.getScope(); status = Status.OK; return Response.status(status).entity(new AccessTokenBeanResponse(tokenToReturn, scope)).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)).build(); } } private CredentialsBean getCredentialFromBasicAuthorization(HttpServletRequest request) { String basicAuthorization = request.getHeader(AUTHORIZATION_HEADER); String base64Credentials = basicAuthorization.substring("Basic".length()).trim(); String credentials = new String(DatatypeConverter.parseBase64Binary(base64Credentials)); // credentials = username:password String[] splitCreds = credentials.split(":"); String clientId = null; String clientSecret = null; try { clientId = URLDecoder.decode(splitCreds[0], java.nio.charset.StandardCharsets.UTF_8.toString()); clientSecret = URLDecoder.decode(splitCreds[1], java.nio.charset.StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new CredentialsBean(clientId, clientSecret); } /** * 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 MemCachedBean checkRequest(CredentialsBean credentials, String redirectUri, String code, String grantType, HttpServletRequest request) { try{ if(credentials.getClientId() == null || credentials.getClientSecret() == null || redirectUri == null || code == null || grantType == null ) return new MemCachedBean("invalid_request"); if(credentials.getClientId().isEmpty() || credentials.getClientSecret().isEmpty() || redirectUri.isEmpty() || code.isEmpty() || grantType.isEmpty()) return new MemCachedBean("invalid_request"); if(!checkIsapplicationTokenType(authorizationService().get(credentials.getClientSecret()).getClientInfo().getType())) // it is not an app token or it is not a token return new MemCachedBean("invalid_client"); if(entries.get(code) == null) return new MemCachedBean("invalid_grant"); logger.debug("Got tempCode and looking into memcached for correspondance, "+code); JSONParser parser = new JSONParser(); JSONObject json = (JSONObject) parser.parse((String) entries.get(code)); String cachedRedirectUri = (String) json.get("redirect_uri"); String cachedClientId = (String) json.get("client_id"); logger.debug("Found tempCode into memcached, cachedClientId="+cachedClientId); if(!cachedRedirectUri.equals(redirectUri) || !cachedClientId.equals(credentials.getClientId())) return new MemCachedBean("invalid_grant"); if(!grantType.equals(GRANT_TYPE_VALUE)) return new MemCachedBean("unsupported_grant_type"); String cachedToken = (String) json.get("token"); String cachedContext = (String) json.get("context"); logger.debug("Returning cachedToken="+cachedToken + " and cachedContext="+cachedContext); return new MemCachedBean(cachedToken, cachedContext, null); }catch(Exception e){ logger.error("Failed to check the correctness of the request", e); return new MemCachedBean("invalid_request"); } } }