2017-01-27 16:12:57 +01:00
package org.gcube.portal.oauth ;
import static org.gcube.common.authorization.client.Constants.authorizationService ;
2019-03-21 14:39:33 +01:00
import java.net.URLDecoder ;
2017-01-27 16:12:57 +01:00
import java.util.Map ;
import java.util.concurrent.ConcurrentHashMap ;
import javax.inject.Singleton ;
2019-03-21 14:39:33 +01:00
import javax.servlet.http.HttpServletRequest ;
2017-01-27 16:12:57 +01:00
import javax.ws.rs.Consumes ;
import javax.ws.rs.FormParam ;
2017-01-29 18:11:55 +01:00
import javax.ws.rs.GET ;
2017-01-27 16:12:57 +01:00
import javax.ws.rs.POST ;
import javax.ws.rs.Path ;
import javax.ws.rs.Produces ;
2019-03-21 14:39:33 +01:00
import javax.ws.rs.core.Context ;
2017-01-27 16:12:57 +01:00
import javax.ws.rs.core.MediaType ;
import javax.ws.rs.core.Response ;
import javax.ws.rs.core.Response.Status ;
2019-03-21 14:39:33 +01:00
import javax.xml.bind.DatatypeConverter ;
2017-01-27 16:12:57 +01:00
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 ;
2017-01-27 16:19:39 +01:00
import org.gcube.common.scope.api.ScopeProvider ;
2017-01-27 16:12:57 +01:00
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 ;
2019-03-21 14:39:33 +01:00
import org.gcube.smartgears.Constants ;
2017-01-27 16:12:57 +01:00
import org.slf4j.LoggerFactory ;
2017-01-29 18:11:55 +01:00
@Path ( " /v2 " )
2017-01-27 16:12:57 +01:00
@Singleton
public class OauthService {
2017-01-29 18:11:55 +01:00
public static final String OAUTH_TOKEN_GET_METHOD_NAME_REQUEST = " access-token " ;
private static final String GRANT_TYPE_VALUE = " authorization_code " ;
2019-03-21 14:39:33 +01:00
private static final String AUTHORIZATION_HEADER = " Authorization " ;
2017-01-27 16:12:57 +01:00
private static final org . slf4j . Logger logger = LoggerFactory . getLogger ( OauthService . class ) ;
/ * *
2017-02-01 16:03:48 +01:00
* This map contains couples < code , { qualifier - token , insert time , scope , redirect uri , client id } >
2017-01-27 16:12:57 +01:00
* /
2017-01-29 18:11:55 +01:00
private Map < String , CacheBean > entries ;
2017-01-27 16:12:57 +01:00
2017-01-29 18:54:43 +01:00
/ * *
* Cleaner thread
* /
CacheCleaner cleaner ;
2017-01-27 16:12:57 +01:00
/ * *
* 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 ( ) {
2017-01-29 18:11:55 +01:00
logger . info ( " Singleton gcube-oauth service built. " ) ;
entries = new ConcurrentHashMap < String , CacheBean > ( ) ;
2017-01-29 18:54:43 +01:00
cleaner = new CacheCleaner ( entries ) ;
2017-01-27 16:12:57 +01:00
cleaner . start ( ) ;
}
2017-01-29 18:54:43 +01:00
@Override
protected void finalize ( ) {
if ( cleaner ! = null )
cleaner . interrupt ( ) ;
}
2017-01-27 16:12:57 +01:00
/ * *
* 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 ) ;
}
2017-01-29 18:54:43 +01:00
2017-01-29 18:11:55 +01:00
@GET
@Path ( " check " )
@Produces ( MediaType . TEXT_PLAIN )
public Response checkService ( ) {
return Response . status ( Status . OK ) . entity ( " Ready! " ) . build ( ) ;
}
2017-01-27 16:12:57 +01:00
@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 {
2017-02-01 16:03:48 +01:00
// check parameters
String code = bean . getCode ( ) ;
String clientId = bean . getClientId ( ) ;
String redirectUri = bean . getRedirectUri ( ) ;
if ( code = = null | | code . isEmpty ( ) )
return Response . status ( Status . BAD_REQUEST ) .
entity ( " { \" error \" = \" 'code' cannot be null or missing \" " ) . build ( ) ;
if ( clientId = = null | | clientId . isEmpty ( ) )
return Response . status ( Status . BAD_REQUEST ) .
entity ( " { \" error \" = \" 'client_id' cannot be null or missing \" " ) . build ( ) ;
if ( redirectUri = = null | | redirectUri . isEmpty ( ) )
return Response . status ( Status . BAD_REQUEST ) .
entity ( " { \" error \" = \" 'redirect_uri' cannot be null or missing \" " ) . build ( ) ;
logger . info ( " Saving entry defined by " + bean + " in cache, token is " + token . substring ( 0 , 10 ) + " *************** " ) ;
entries . put ( code , new CacheBean ( token , ScopeProvider . instance . get ( ) , redirectUri , clientId , System . currentTimeMillis ( ) ) ) ;
2017-01-27 16:12:57 +01:00
return Response . status ( status ) . build ( ) ;
}
}
@POST
@Consumes ( MediaType . APPLICATION_FORM_URLENCODED )
@Produces ( MediaType . APPLICATION_JSON )
2017-01-29 18:11:55 +01:00
@Path ( OAUTH_TOKEN_GET_METHOD_NAME_REQUEST )
2017-01-27 16:12:57 +01:00
/ * *
* 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 ,
2019-03-21 14:39:33 +01:00
@FormParam ( " grant_type " ) String grantType , // it must always be equal to "authorization_code"
@Context HttpServletRequest request
2017-01-27 16:12:57 +01:00
) {
Status status = Status . BAD_REQUEST ;
logger . info ( " Request to exchange code for token " ) ;
2019-03-21 14:39:33 +01:00
2017-01-27 16:12:57 +01:00
try {
2019-03-21 14:39:33 +01:00
CredentialsBean credentials = new CredentialsBean ( clientId , clientSecret ) ;
if ( clientId = = null )
credentials = getCredentialFromBasicAuthorization ( request ) ;
else if ( request . getHeader ( AUTHORIZATION_HEADER ) ! = null )
throw new Exception ( " he 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 ) ;
2017-01-27 16:12:57 +01:00
// check if something is missing
2019-03-21 14:39:33 +01:00
String errorMessage = checkRequest ( credentials , redirectUri , code , grantType , request ) ;
2017-01-27 16:12:57 +01:00
if ( errorMessage ! = null ) {
logger . error ( " The request fails because of " + errorMessage ) ;
2017-02-03 15:46:49 +01:00
return Response . status ( status ) . entity ( new AccessTokenErrorResponse ( errorMessage , null ) ) . build ( ) ;
2017-01-27 16:12:57 +01:00
} else {
logger . info ( " The request is ok " ) ;
String tokenToReturn = entries . get ( code ) . getToken ( ) ;
2017-01-27 16:19:39 +01:00
String scope = entries . get ( code ) . getScope ( ) ;
2017-01-27 16:12:57 +01:00
status = Status . OK ;
2017-01-27 16:19:39 +01:00
return Response . status ( status ) . entity ( new AccessTokenBeanResponse ( tokenToReturn , scope ) ) . build ( ) ;
2017-01-27 16:12:57 +01:00
}
} catch ( Exception e ) {
logger . error ( " Failed to perform this operation " , e ) ;
status = Status . BAD_REQUEST ;
2017-02-03 15:46:49 +01:00
return Response . status ( status ) . entity ( new AccessTokenErrorResponse ( " invalid_request " , null ) ) . build ( ) ;
2017-01-27 16:12:57 +01:00
}
}
2019-03-21 14:39:33 +01:00
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 = URLDecoder . decode ( splitCreds [ 0 ] ) ;
String clientSecret = URLDecoder . decode ( splitCreds [ 1 ] ) ;
return new CredentialsBean ( clientId , clientSecret ) ;
}
2017-01-27 16:12:57 +01:00
/ * *
* Check request parameters
* @param clientId
* @param clientSecret
* @param redirectUri
* @param code
* @param grantType
* @return see https : //tools.ietf.org/html/rfc6749#section-5.2
* /
2019-03-21 14:39:33 +01:00
private String checkRequest ( CredentialsBean credentials ,
String redirectUri , String code , String grantType , HttpServletRequest request ) {
2017-01-27 16:12:57 +01:00
try {
2019-03-21 14:39:33 +01:00
if ( credentials . getClientId ( ) = = null | | credentials . getClientSecret ( ) = = null | | redirectUri = = null | | code = = null | | grantType = = null )
2017-01-27 16:12:57 +01:00
return " invalid_request " ;
2019-03-21 14:39:33 +01:00
if ( credentials . getClientId ( ) . isEmpty ( ) | | credentials . getClientSecret ( ) . isEmpty ( ) | | redirectUri . isEmpty ( ) | | code . isEmpty ( ) | | grantType . isEmpty ( ) )
2017-01-27 16:12:57 +01:00
return " invalid_request " ;
2019-03-21 14:39:33 +01:00
if ( ! checkIsapplicationTokenType ( authorizationService ( ) . get ( credentials . getClientSecret ( ) ) . getClientInfo ( ) . getType ( ) ) ) // it is not an app token or it is not a token
2017-01-27 16:12:57 +01:00
return " invalid_client " ;
if ( ! entries . containsKey ( code ) | | CacheBean . isExpired ( entries . get ( code ) ) )
return " invalid_grant " ;
2017-01-27 16:24:33 +01:00
CacheBean entry = entries . get ( code ) ;
2019-03-21 14:39:33 +01:00
if ( ! entry . getRedirectUri ( ) . equals ( redirectUri ) | | ! entry . getClientId ( ) . equals ( credentials . getClientId ( ) ) )
2017-01-27 16:24:33 +01:00
return " invalid_grant " ;
2017-01-29 18:11:55 +01:00
if ( ! grantType . equals ( GRANT_TYPE_VALUE ) )
2017-01-27 16:12:57 +01:00
return " unsupported_grant_type " ;
return null ;
} catch ( Exception e ) {
logger . error ( " Failed to check the correctness of the request " , e ) ;
return " invalid_request " ;
}
}
}