/**
* =============================================================================
*
* ORCID (R) Open Source
* http://orcid.org
*
* Copyright (c) 2012-2014 ORCID, Inc.
* Licensed under an MIT-Style License (MIT)
* http://orcid.org/open-source-license
*
* This copyright and license information (including a link to the full license)
* shall be included in its entirety in all copies or substantial portion of
* the software.
*
* =============================================================================
*/
package org.orcid.core.oauth.service;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Resource;
import org.orcid.core.constants.OrcidOauth2Constants;
import org.orcid.core.manager.ClientDetailsEntityCacheManager;
import org.orcid.core.manager.ProfileEntityManager;
import org.orcid.core.oauth.OrcidOAuth2Authentication;
import org.orcid.core.oauth.OrcidOauth2AuthInfo;
import org.orcid.core.oauth.OrcidOauth2UserAuthentication;
import org.orcid.core.oauth.OrcidRandomValueTokenServices;
import org.orcid.core.security.aop.LockedException;
import org.orcid.jaxb.model.message.ScopePathType;
import org.orcid.persistence.dao.OrcidOauth2AuthoriziationCodeDetailDao;
import org.orcid.persistence.dao.OrcidOauth2TokenDetailDao;
import org.orcid.persistence.jpa.entities.ClientDetailsEntity;
import org.orcid.persistence.jpa.entities.OrcidOauth2TokenDetail;
import org.orcid.persistence.jpa.entities.ProfileEntity;
import org.orcid.pojo.ajaxForm.PojoUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Declan Newman (declan) Date: 11/05/2012
*/
public class OrcidRandomValueTokenServicesImpl extends DefaultTokenServices implements OrcidRandomValueTokenServices {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidRandomValueTokenServicesImpl.class);
@Value("${org.orcid.core.token.write_validity_seconds:3600}")
private int writeValiditySeconds;
@Value("${org.orcid.core.token.read_validity_seconds:631138519}")
private int readValiditySeconds;
private TokenStore orcidTokenStore;
private TokenEnhancer customTokenEnhancer;
@Resource
private OrcidOauth2AuthoriziationCodeDetailDao orcidOauth2AuthoriziationCodeDetailDao;
@Resource
private OrcidOAuth2RequestValidator orcidOAuth2RequestValidator;
@Resource
private ClientDetailsEntityCacheManager clientDetailsEntityCacheManager;
private boolean customSupportRefreshToken;
@Resource
private OrcidOauth2TokenDetailDao orcidOauth2TokenDetailDao;
@Resource
private ProfileEntityManager profileEntityManager;
@Resource
private AuthenticationKeyGenerator authenticationKeyGenerator;
public boolean isCustomSupportRefreshToken() {
return customSupportRefreshToken;
}
public void setCustomSupportRefreshToken(boolean customSupportRefreshToken) {
this.customSupportRefreshToken = customSupportRefreshToken;
}
@Override
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OrcidOauth2AuthInfo authInfo = new OrcidOauth2AuthInfo(authentication);
String userOrcid = authInfo.getUserOrcid();
DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
accessToken.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
accessToken.setScope(authentication.getOAuth2Request().getScope());
if(customTokenEnhancer != null) {
accessToken = new DefaultOAuth2AccessToken(customTokenEnhancer.enhance(accessToken, authentication));
}
if(this.isSupportRefreshToken(authentication.getOAuth2Request())) {
OAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(UUID.randomUUID().toString());
accessToken.setRefreshToken(refreshToken);
}
orcidTokenStore.storeAccessToken(accessToken, authentication);
LOGGER.info("Creating new access token: clientId={}, scopes={}, userOrcid={}", new Object[] { authInfo.getClientId(), authInfo.getScopes(), userOrcid });
return accessToken;
}
/**
* The access token validity period in seconds
*
* @param authorizationRequest
* the current authorization request
* @return the access token validity period in seconds
*/
@Override
protected int getAccessTokenValiditySeconds(OAuth2Request authorizationRequest) {
Set<ScopePathType> requestedScopes = ScopePathType.getScopesFromStrings(authorizationRequest.getScope());
if (isClientCredentialsGrantType(authorizationRequest)) {
boolean allAreClientCredentialsScopes = true;
for (ScopePathType scope : requestedScopes) {
if (!scope.isClientCreditalScope()) {
allAreClientCredentialsScopes = false;
break;
}
}
if (allAreClientCredentialsScopes) {
return readValiditySeconds;
}
} else if (isPersistentTokenEnabled(authorizationRequest)) {
return readValiditySeconds;
}
return writeValiditySeconds;
}
public int getWriteValiditySeconds() {
return writeValiditySeconds;
}
public int getReadValiditySeconds() {
return readValiditySeconds;
}
/**
* Checks the authorization code to verify if the user enable the persistent
* token or not
* */
private boolean isPersistentTokenEnabled(OAuth2Request authorizationRequest) {
if (authorizationRequest != null) {
Map<String, String> params = authorizationRequest.getRequestParameters();
if (params != null) {
if (params.containsKey(OrcidOauth2Constants.IS_PERSISTENT)) {
String isPersistent = params.get(OrcidOauth2Constants.IS_PERSISTENT);
if (Boolean.valueOf(isPersistent)) {
return true;
}
} else if (params.containsKey("code")) {
String code = params.get("code");
if (orcidOauth2AuthoriziationCodeDetailDao.find(code) != null) {
if (orcidOauth2AuthoriziationCodeDetailDao.isPersistentToken(code)) {
return true;
}
}
}
}
}
return false;
}
/**
* Checks if the authorization request grant type is client_credentials
* */
private boolean isClientCredentialsGrantType(OAuth2Request authorizationRequest) {
Map<String, String> params = authorizationRequest.getRequestParameters();
if (params != null) {
if (params.containsKey(OrcidOauth2Constants.GRANT_TYPE)) {
String grantType = params.get(OrcidOauth2Constants.GRANT_TYPE);
if (OrcidOauth2Constants.GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType))
return true;
}
}
return false;
}
@Override
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException {
OAuth2AccessToken accessToken = orcidTokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else {
// If it is, respect the token expiration
if (accessToken.isExpired()) {
orcidTokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
Map<String, Object> additionalInfo = accessToken.getAdditionalInformation();
if(additionalInfo != null) {
String clientId = (String)additionalInfo.get(OrcidOauth2Constants.CLIENT_ID);
ClientDetailsEntity clientEntity = clientDetailsEntityCacheManager.retrieve(clientId);
try {
orcidOAuth2RequestValidator.validateClientIsEnabled(clientEntity);
} catch (LockedException le) {
throw new InvalidTokenException(le.getMessage());
}
}
}
OAuth2Authentication result = orcidTokenStore.readAuthentication(accessToken);
return result;
}
public void setOrcidtokenStore(TokenStore orcidTokenStore) {
super.setTokenStore(orcidTokenStore);
this.orcidTokenStore = orcidTokenStore;
}
public void setCustomTokenEnhancer(TokenEnhancer customTokenEnhancer) {
super.setTokenEnhancer(customTokenEnhancer);
this.customTokenEnhancer = customTokenEnhancer;
}
public boolean longLifeTokenExist(String clientId, String userId, Collection<String> scopes) {
Collection<OAuth2AccessToken> existingTokens = orcidTokenStore.findTokensByClientIdAndUserName(clientId, userId);
if(existingTokens == null || existingTokens.isEmpty()) {
return false;
}
for(OAuth2AccessToken token : existingTokens) {
if(!token.isExpired() && token.getExpiration().after(java.sql.Timestamp.valueOf(LocalDateTime.now().plusHours(1)))) {
if(token.getScope().containsAll(scopes) && scopes.containsAll(token.getScope())) {
return true;
}
}
}
return false;
}
@Override
@Transactional
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
String parentTokenValue = tokenRequest.getRequestParameters().get(OrcidOauth2Constants.AUTHORIZATION);
String clientId = tokenRequest.getClientId();
String scopes = tokenRequest.getRequestParameters().get(OAuth2Utils.SCOPE);
Long expiresIn = tokenRequest.getRequestParameters().containsKey(OrcidOauth2Constants.EXPIRES_IN)
? Long.valueOf(tokenRequest.getRequestParameters().get(OrcidOauth2Constants.EXPIRES_IN)) : 0L;
Boolean revokeOld = tokenRequest.getRequestParameters().containsKey(OrcidOauth2Constants.REVOKE_OLD) ? Boolean.valueOf(tokenRequest.getRequestParameters().get(OrcidOauth2Constants.REVOKE_OLD)) : true;
// Check if the refresh token is enabled
if (!customSupportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// Check if the client support refresh token
ClientDetailsEntity clientDetails = clientDetailsEntityCacheManager.retrieve(clientId);
if (!clientDetails.getAuthorizedGrantTypes().contains(OrcidOauth2Constants.REFRESH_TOKEN)) {
throw new InvalidGrantException("Client " + clientId + " doesnt have refresh token enabled");
}
OrcidOauth2TokenDetail parentToken = orcidOauth2TokenDetailDao.findByTokenValue(parentTokenValue);
ProfileEntity profileEntity = new ProfileEntity(parentToken.getProfile().getId());
OrcidOauth2TokenDetail newToken = new OrcidOauth2TokenDetail();
newToken.setApproved(true);
newToken.setClientDetailsId(clientId);
newToken.setDateCreated(new Date());
newToken.setLastModified(new Date());
newToken.setPersistent(parentToken.isPersistent());
newToken.setProfile(profileEntity);
newToken.setRedirectUri(parentToken.getRedirectUri());
newToken.setRefreshTokenValue(UUID.randomUUID().toString());
newToken.setResourceId(parentToken.getResourceId());
newToken.setResponseType(parentToken.getResponseType());
newToken.setState(parentToken.getState());
newToken.setTokenDisabled(false);
if(expiresIn <= 0) {
//If expiresIn is 0 or less, set the parent token
newToken.setTokenExpiration(parentToken.getTokenExpiration());
} else {
//Assumes expireIn already contains the real expired time expressed in millis
newToken.setTokenExpiration(new Date(expiresIn));
}
newToken.setTokenType(parentToken.getTokenType());
newToken.setTokenValue(UUID.randomUUID().toString());
newToken.setVersion(parentToken.getVersion());
if (PojoUtil.isEmpty(scopes)) {
newToken.setScope(parentToken.getScope());
} else {
newToken.setScope(scopes);
}
//Generate an authentication object to be able to generate the authentication key
Set<String> scopesSet = OAuth2Utils.parseParameterList(newToken.getScope());
AuthorizationRequest request = new AuthorizationRequest(clientId, scopesSet);
request.setApproved(true);
Authentication authentication = new OrcidOauth2UserAuthentication(profileEntity, true);
OrcidOAuth2Authentication orcidAuthentication = new OrcidOAuth2Authentication(request, authentication, newToken.getTokenValue());
newToken.setAuthenticationKey(authenticationKeyGenerator.extractKey(orcidAuthentication));
// Store the new token and return it
orcidOauth2TokenDetailDao.persist(newToken);
// Revoke the old token when required
if (revokeOld) {
orcidOauth2TokenDetailDao.disableAccessToken(parentTokenValue);
}
// Save the changes
orcidOauth2TokenDetailDao.flush();
// Transform the OrcidOauth2TokenDetail into a OAuth2AccessToken
// and return it
return toOAuth2AccessToken(newToken);
}
private OAuth2AccessToken toOAuth2AccessToken(OrcidOauth2TokenDetail token) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(token.getTokenValue());
result.setExpiration(token.getTokenExpiration());
result.setRefreshToken(new DefaultOAuth2RefreshToken(token.getRefreshTokenValue()));
result.setScope(OAuth2Utils.parseParameterList(token.getScope()));
result.setTokenType(token.getTokenType());
result.setValue(token.getTokenValue());
Map<String, Object> additionalInfo = new HashMap<String, Object>();
if(token.getProfile() != null) {
additionalInfo.put(OrcidOauth2Constants.ORCID, token.getProfile().getId());
additionalInfo.put(OrcidOauth2Constants.NAME, profileEntityManager.retrivePublicDisplayName(token.getProfile().getId()));
}
result.setAdditionalInformation(additionalInfo);
return result;
}
}