Skip to content

Commit

Permalink
front port fixes from wso2-extensions#2091.
Browse files Browse the repository at this point in the history
  • Loading branch information
dushaniw committed Jan 29, 2024
1 parent cb0dabe commit a9862db
Show file tree
Hide file tree
Showing 10 changed files with 723 additions and 138 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2024, 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.lang3.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) 2024, 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2024, 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.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO;

/**
* Abstraction layer between OAuth2Service and persistence layer to handle
* revocation logic during token persistence and non-persistence scenarios.
*/
public interface OAuth2RevocationProcessor {

/**
* Revoke access token.
*
* @param revokeRequestDTO Metadata containing revoke token request.
* @param accessTokenDO {@link AccessTokenDO} instance.
* @throws IdentityOAuth2Exception If an error occurs while revoking the access token.
* @throws UserIdNotFoundException If the user id is not found.
*/
void revokeAccessToken(OAuthRevocationRequestDTO revokeRequestDTO, AccessTokenDO accessTokenDO)
throws IdentityOAuth2Exception, UserIdNotFoundException;

/**
* Revoke refresh token.
*
* @param revokeRequestDTO Metadata containing revoke token request.
* @param refreshTokenDO {@link RefreshTokenValidationDataDO} instance.
* @throws IdentityOAuth2Exception If an error occurs while revoking the refresh token.
*/
void revokeRefreshToken(OAuthRevocationRequestDTO revokeRequestDTO,
RefreshTokenValidationDataDO refreshTokenDO) throws IdentityOAuth2Exception;

/**
* Validate and return the refresh token metadata.
*
* @param revokeRequestDTO Metadata containing revoke token request.
* @return RefreshTokenValidationDataDO {@link RefreshTokenValidationDataDO} instance.
* @throws IdentityOAuth2Exception If an error occurs while validating the refresh token.
*/
RefreshTokenValidationDataDO getRevocableRefreshToken(OAuthRevocationRequestDTO revokeRequestDTO)
throws IdentityOAuth2Exception;

/**
* Validate and return the access token metadata.
*
* @param revokeRequestDTO Metadata containing revoke token request.
* @return AccessTokenDO {@link AccessTokenDO} instance.
* @throws IdentityOAuth2Exception If an error occurs while validating the access token.
*/
AccessTokenDO getRevocableAccessToken(OAuthRevocationRequestDTO revokeRequestDTO)
throws IdentityOAuth2Exception;

/**
* Check whether revoke request is related to access token or revoke token.
*
* @param revokeRequestDTO Metadata containing revoke token request.
* @return boolean whether it is a refresh token request or not
*/
boolean isRefreshTokenType(OAuthRevocationRequestDTO revokeRequestDTO);
}
Loading

0 comments on commit a9862db

Please sign in to comment.