Skip to content

Commit

Permalink
Merge pull request #2091 from dushaniw/is6.2.0-m2
Browse files Browse the repository at this point in the history
Add extensions for token persistence removal feature
  • Loading branch information
chamathns authored Sep 9, 2023
2 parents 51357b1 + 8193a44 commit 632c3ad
Show file tree
Hide file tree
Showing 11 changed files with 725 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.wso2.carbon.identity.oauth.listener.OAuthApplicationMgtListener;
import org.wso2.carbon.identity.oauth2.OAuth2ScopeService;
import org.wso2.carbon.identity.oauth2.OAuth2Service;
import org.wso2.carbon.identity.oauth2.dao.AccessTokenDAO;
import org.wso2.carbon.identity.oauth2.dao.TokenManagementDAO;
import org.wso2.carbon.identity.oauth2.token.handlers.response.AccessTokenResponseHandler;
import org.wso2.carbon.identity.oauth2.validators.scope.ScopeValidator;
import org.wso2.carbon.identity.organization.management.service.OrganizationUserResidentResolverService;
Expand Down Expand Up @@ -58,7 +60,8 @@ public class OAuthComponentServiceHolder {
private RoleManagementService roleManagementService;
private OrganizationUserResidentResolverService organizationUserResidentResolverService;
private List<AccessTokenResponseHandler> accessTokenResponseHandlers = new ArrayList<>();

private AccessTokenDAO accessTokenDAOService;
private TokenManagementDAO tokenManagementDAOService;

/**
* Get the list of scope validator implementations available.
Expand Down Expand Up @@ -263,4 +266,44 @@ public List<AccessTokenResponseHandler> getAccessTokenResponseHandlers() {

return accessTokenResponseHandlers;
}

/**
* Get AccessTokenDAO instance.
*
* @return AccessTokenDAO {@link AccessTokenDAO} instance.
*/
public AccessTokenDAO getAccessTokenDAOService() {

return accessTokenDAOService;
}

/**
* Set AccessTokenDAO instance.
*
* @param accessTokenDAOService {@link AccessTokenDAO} instance.
*/
public void setAccessTokenDAOService(AccessTokenDAO accessTokenDAOService) {

this.accessTokenDAOService = accessTokenDAOService;
}

/**
* Get TokenManagementDAO instance.
*
* @return TokenManagementDAO {@link TokenManagementDAO} instance.
*/
public TokenManagementDAO getTokenManagementDAOService() {

return tokenManagementDAOService;
}

/**
* Set TokenManagementDAO instance.
*
* @param tokenManagementDAOService {@link TokenManagementDAO} instance.
*/
public void setTokenManagementDAOService(TokenManagementDAO tokenManagementDAOService) {

this.tokenManagementDAOService = tokenManagementDAOService;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.oauth.tokenprocessor;

import org.apache.commons.lang.StringUtils;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;

/**
* Handles oauth2 token revocation when persistence layer exists.
*/
public class DefaultOAuth2RevocationProcessor implements OAuth2RevocationProcessor {

@Override
public void revokeAccessToken(OAuthRevocationRequestDTO revokeRequestDTO, AccessTokenDO accessTokenDO)
throws IdentityOAuth2Exception {

OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO()
.revokeAccessTokens(new String[]{accessTokenDO.getAccessToken()});
}

@Override
public void revokeRefreshToken(OAuthRevocationRequestDTO revokeRequestDTO,
RefreshTokenValidationDataDO refreshTokenDO) throws IdentityOAuth2Exception {

OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO()
.revokeAccessTokens(new String[]{refreshTokenDO.getAccessToken()});
}

@Override
public RefreshTokenValidationDataDO getRevocableRefreshToken(OAuthRevocationRequestDTO revokeRequestDTO)
throws IdentityOAuth2Exception {

return OAuthTokenPersistenceFactory.getInstance().getTokenManagementDAO()
.validateRefreshToken(revokeRequestDTO.getConsumerKey(), revokeRequestDTO.getToken());
}

@Override
public AccessTokenDO getRevocableAccessToken(OAuthRevocationRequestDTO revokeRequestDTO)
throws IdentityOAuth2Exception {

return OAuth2Util.findAccessToken(revokeRequestDTO.getToken(), true);
}

@Override
public boolean isRefreshTokenType(OAuthRevocationRequestDTO revokeRequestDTO) {

return StringUtils.equals(GrantType.REFRESH_TOKEN.toString(), revokeRequestDTO.getTokenType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.oauth.tokenprocessor;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.base.IdentityConstants;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory;
import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO;
import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.openidconnect.OIDCClaimUtil;

import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
* Default implementation of @RefreshTokenProcessor responsible for handling refresh token persistence logic.
*/
public class DefaultRefreshTokenGrantProcessor implements RefreshTokenGrantProcessor {

private static final Log log = LogFactory.getLog(DefaultRefreshTokenGrantProcessor.class);
public static final String PREV_ACCESS_TOKEN = "previousAccessToken";
public static final int LAST_ACCESS_TOKEN_RETRIEVAL_LIMIT = 10;

@Override
public RefreshTokenValidationDataDO validateRefreshToken(OAuthTokenReqMessageContext tokenReqMessageContext)
throws IdentityOAuth2Exception {

OAuth2AccessTokenReqDTO tokenReq = tokenReqMessageContext.getOauth2AccessTokenReqDTO();
RefreshTokenValidationDataDO validationBean = OAuthTokenPersistenceFactory.getInstance().getTokenManagementDAO()
.validateRefreshToken(tokenReq.getClientId(), tokenReq.getRefreshToken());
validatePersistedAccessToken(validationBean, tokenReq.getClientId());
return validationBean;
}

@Override
public void persistNewToken(OAuthTokenReqMessageContext tokenReqMessageContext, AccessTokenDO accessTokenBean,
String userStoreDomain, String clientId) throws IdentityOAuth2Exception {

RefreshTokenValidationDataDO oldAccessToken =
(RefreshTokenValidationDataDO) tokenReqMessageContext.getProperty(PREV_ACCESS_TOKEN);
if (log.isDebugEnabled()) {
if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.ACCESS_TOKEN)) {
log.debug(String.format("Previous access token (hashed): %s", DigestUtils.sha256Hex(
oldAccessToken.getAccessToken())));
}
}
// set the previous access token state to "INACTIVE" and store new access token in single db connection
OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO()
.invalidateAndCreateNewAccessToken(oldAccessToken.getTokenId(),
OAuthConstants.TokenStates.TOKEN_STATE_INACTIVE, clientId,
UUID.randomUUID().toString(), accessTokenBean, userStoreDomain, oldAccessToken.getGrantType());
}

@Override
public AccessTokenDO createAccessTokenBean(OAuthTokenReqMessageContext tokReqMsgCtx,
OAuth2AccessTokenReqDTO tokenReq,
RefreshTokenValidationDataDO validationBean, String tokenType)
throws IdentityOAuth2Exception {

Timestamp timestamp = new Timestamp(new Date().getTime());
String tokenId = UUID.randomUUID().toString();

AccessTokenDO accessTokenDO = new AccessTokenDO();
accessTokenDO.setConsumerKey(tokenReq.getClientId());
accessTokenDO.setAuthzUser(tokReqMsgCtx.getAuthorizedUser());
accessTokenDO.setScope(tokReqMsgCtx.getScope());
accessTokenDO.setTokenType(tokenType);
accessTokenDO.setTokenState(OAuthConstants.TokenStates.TOKEN_STATE_ACTIVE);
accessTokenDO.setTokenId(tokenId);
accessTokenDO.setGrantType(tokenReq.getGrantType());
accessTokenDO.setIssuedTime(timestamp);
accessTokenDO.setTokenBinding(tokReqMsgCtx.getTokenBinding());

if (OAuth2ServiceComponentHolder.isConsentedTokenColumnEnabled()) {
String previousGrantType = validationBean.getGrantType();
// Check if the previous grant type is consent refresh token type or not.
if (!OAuthConstants.GrantTypes.REFRESH_TOKEN.equals(previousGrantType)) {
// If the previous grant type is not a refresh token, then check if it's a consent token or not.
if (OIDCClaimUtil.isConsentBasedClaimFilteringApplicable(previousGrantType)) {
accessTokenDO.setIsConsentedToken(true);
}
} else {
/* When previousGrantType == refresh_token, we need to check whether the original grant type
is consented or not. */
AccessTokenDO accessTokenDOFromTokenIdentifier = OAuth2Util.getAccessTokenDOFromTokenIdentifier(
validationBean.getAccessToken(), false);
accessTokenDO.setIsConsentedToken(accessTokenDOFromTokenIdentifier.isConsentedToken());
}

if (accessTokenDO.isConsentedToken()) {
tokReqMsgCtx.setConsentedToken(true);
}
}
return accessTokenDO;
}

private boolean validatePersistedAccessToken(RefreshTokenValidationDataDO validationBean, String clientId)
throws IdentityOAuth2Exception {

if (validationBean.getAccessToken() == null) {
if (log.isDebugEnabled()) {
log.debug(String.format("Invalid Refresh Token provided for Client with Client Id : %s", clientId));
}
throw new IdentityOAuth2Exception("Persisted access token data not found");
}
return true;
}

@Override
public boolean isLatestRefreshToken(OAuth2AccessTokenReqDTO tokenReq, RefreshTokenValidationDataDO validationBean,
String userStoreDomain) throws IdentityOAuth2Exception {

if (log.isDebugEnabled()) {
if (IdentityUtil.isTokenLoggable(IdentityConstants.IdentityTokens.REFRESH_TOKEN)) {
log.debug(String.format("Evaluating refresh token. Token value(hashed): %s, Token state: %s",
DigestUtils.sha256Hex(tokenReq.getRefreshToken()), validationBean.getRefreshTokenState()));
} else {
log.debug(String.format("Evaluating refresh token. Token state: %s",
validationBean.getRefreshTokenState()));
}
}
if (!OAuthConstants.TokenStates.TOKEN_STATE_ACTIVE.equals(validationBean.getRefreshTokenState())) {
/* if refresh token is not in active state, check whether there is an access token issued with the same
* refresh token.
*/
List<AccessTokenDO> accessTokenBeans = getAccessTokenBeans(tokenReq, validationBean, userStoreDomain);
for (AccessTokenDO token : accessTokenBeans) {
if (tokenReq.getRefreshToken() != null && tokenReq.getRefreshToken().equals(token.getRefreshToken())
&& (OAuthConstants.TokenStates.TOKEN_STATE_ACTIVE.equals(token.getTokenState())
|| OAuthConstants.TokenStates.TOKEN_STATE_EXPIRED.equals(token.getTokenState()))) {
return true;
}
}
if (log.isDebugEnabled()) {
log.debug(String.format("Refresh token: %s is not the latest", tokenReq.getRefreshToken()));
}
return false;
}
return true;
}

private List<AccessTokenDO> getAccessTokenBeans(OAuth2AccessTokenReqDTO tokenReq,
RefreshTokenValidationDataDO validationBean, String userStoreDomain)
throws IdentityOAuth2Exception {

List<AccessTokenDO> accessTokenBeans = OAuthTokenPersistenceFactory.getInstance().getAccessTokenDAO()
.getLatestAccessTokens(tokenReq.getClientId(), validationBean.getAuthorizedUser(), userStoreDomain,
OAuth2Util.buildScopeString(validationBean.getScope()),
validationBean.getTokenBindingReference(), true, LAST_ACCESS_TOKEN_RETRIEVAL_LIMIT);
if (accessTokenBeans == null || accessTokenBeans.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(String.format("No previous access tokens found. User: %s, client: %s, scope: %s",
validationBean.getAuthorizedUser(), tokenReq.getClientId(),
OAuth2Util.buildScopeString(validationBean.getScope())));
}
throw new IdentityOAuth2Exception("No previous access tokens found");
}
return accessTokenBeans;
}
}
Loading

0 comments on commit 632c3ad

Please sign in to comment.