Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extensions for token persistence removal feature #2091

Merged
merged 16 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading