From 116b20bb39e60722023580fabc43572781f65b42 Mon Sep 17 00:00:00 2001 From: AfraHussaindeen Date: Wed, 1 Oct 2025 08:46:54 +0530 Subject: [PATCH 1/4] Add support to revoke access token when bound session is expired/invalid. --- .../bindings/impl/AbstractTokenBinder.java | 44 +--- .../impl/SSOSessionBasedTokenBinder.java | 51 +++-- .../handlers/grant/RefreshGrantHandler.java | 42 +++- .../identity/oauth2/util/OAuth2Util.java | 211 ++++++++++++++++++ .../validators/TokenValidationHandler.java | 39 +--- .../TokenValidationHandlerTest.java | 10 +- 6 files changed, 286 insertions(+), 111 deletions(-) diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/AbstractTokenBinder.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/AbstractTokenBinder.java index c7a3ef1e4b3..691f16ac0aa 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/AbstractTokenBinder.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/AbstractTokenBinder.java @@ -18,25 +18,19 @@ package org.wso2.carbon.identity.oauth2.token.bindings.impl; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCache; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheEntry; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheKey; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; -import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinder; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; -import java.net.HttpCookie; import java.util.Optional; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.HttpHeaders; -import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.COMMONAUTH_COOKIE; import static org.wso2.carbon.identity.oauth.common.OAuthConstants.GrantTypes.AUTHORIZATION_CODE; import static org.wso2.carbon.identity.oauth.common.OAuthConstants.GrantTypes.REFRESH_TOKEN; @@ -92,38 +86,12 @@ private boolean isValidTokenBinding(OAuth2AccessTokenReqDTO oAuth2AccessTokenReq if (REFRESH_TOKEN.equals(oAuth2AccessTokenReqDTO.getGrantType())) { - HttpRequestHeader[] httpRequestHeaders = oAuth2AccessTokenReqDTO.getHttpRequestHeaders(); - if (ArrayUtils.isEmpty(httpRequestHeaders)) { - return false; - } - - for (HttpRequestHeader httpRequestHeader : httpRequestHeaders) { - if (HttpHeaders.COOKIE.equalsIgnoreCase(httpRequestHeader.getName())) { - if (ArrayUtils.isEmpty(httpRequestHeader.getValue())) { - return false; - } - - String[] cookies = httpRequestHeader.getValue()[0].split(";"); - String cookiePrefix = cookieName + "="; - for (String cookie : cookies) { - if (StringUtils.isNotBlank(cookie) && cookie.trim().startsWith(cookiePrefix) && - HttpCookie.parse(cookie).get(0) != null) { - String cookieValue = HttpCookie.parse(cookie).get(0).getValue(); - if (StringUtils.isNotBlank(cookieValue)) { - String tokenBindingValue; - if (COMMONAUTH_COOKIE.equals(cookieName)) { - // For sso-session binding,token binding value will be the session context id. - tokenBindingValue = DigestUtils.sha256Hex(cookieValue); - } else { - tokenBindingValue = cookieValue; - } - String receivedBindingReference = - OAuth2Util.getTokenBindingReference(tokenBindingValue); - return bindingReference.equals(receivedBindingReference); - } - } - } - } + Optional tokenBindingValueOptional = OAuth2Util.getTokenBindingValue(oAuth2AccessTokenReqDTO, + cookieName); + if (tokenBindingValueOptional.isPresent()) { + String tokenBindingValue = tokenBindingValueOptional.get(); + String receivedBindingReference = OAuth2Util.getTokenBindingReference(tokenBindingValue); + return bindingReference.equals(receivedBindingReference); } return false; diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinder.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinder.java index d7f682d0b22..017ff3feb45 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinder.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinder.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -30,6 +30,7 @@ import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth2.OAuth2Constants; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import java.util.Arrays; import java.util.Collections; @@ -128,20 +129,7 @@ public boolean isValidTokenBinding(Object request, String bindingReference) { try { String sessionIdentifier = getTokenBindingValue((HttpServletRequest) request); - if (StringUtils.isBlank(sessionIdentifier)) { - if (log.isDebugEnabled()) { - log.debug("CommonAuthId cookie is not found in the request."); - } - return false; - } - /* Retrieve session context information using sessionIdentifier in order to check the validity of - commonAuthId cookie.*/ - SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier); - if (sessionContext == null) { - if (log.isDebugEnabled()) { - log.debug("Session context is not found corresponding to the session identifier: " + - sessionIdentifier); - } + if (!isSSOSessionValid(sessionIdentifier)) { return false; } } catch (OAuthSystemException e) { @@ -154,6 +142,35 @@ public boolean isValidTokenBinding(Object request, String bindingReference) { @Override public boolean isValidTokenBinding(OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO, String bindingReference) { - return isValidTokenBinding(oAuth2AccessTokenReqDTO, bindingReference, COMMONAUTH_COOKIE); + log.debug("Validating SSO session-based token binding for OAuth2 access token request."); + Optional sessionIdentifier = + OAuth2Util.getTokenBindingValue(oAuth2AccessTokenReqDTO, COMMONAUTH_COOKIE); + if (isSSOSessionValid(sessionIdentifier.orElse(null))) { + return isValidTokenBinding(oAuth2AccessTokenReqDTO, bindingReference, COMMONAUTH_COOKIE); + } + return false; + } + + /** + * Check whether the session identified by the session identifier is valid. + * + * @param sessionIdentifier Session identifier. + * @return True if the session is valid. + */ + private boolean isSSOSessionValid(String sessionIdentifier) { + + if (StringUtils.isBlank(sessionIdentifier)) { + log.debug("Invalid session identifier. CommonAuthId cookie is not found in the request."); + return false; + } + SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier); + if (sessionContext == null) { + if (log.isDebugEnabled()) { + log.debug("Session context is not found corresponding to the session identifier: " + + sessionIdentifier); + } + return false; + } + return true; } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java index 3cc18614552..e8f2a71bb13 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java @@ -56,6 +56,7 @@ import org.wso2.carbon.identity.oauth2.IdentityOAuth2ClientException; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.OAuth2Constants; import org.wso2.carbon.identity.oauth2.ResponseHeader; import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; @@ -118,7 +119,20 @@ public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) .validateRefreshToken(tokReqMsgCtx); validateRefreshTokenInRequest(tokenReq, validationBean); - validateTokenBindingReference(tokenReq, validationBean); + + TokenBinding tokenBinding = null; + if (StringUtils.isNotBlank(validationBean.getTokenBindingReference()) && !NONE + .equals(validationBean.getTokenBindingReference())) { + Optional tokenBindingOptional = OAuthTokenPersistenceFactory.getInstance() + .getTokenBindingMgtDAO() + .getTokenBindingByBindingRef(validationBean.getTokenId(), + validationBean.getTokenBindingReference()); + if (tokenBindingOptional.isPresent()) { + tokenBinding = tokenBindingOptional.get(); + tokReqMsgCtx.setTokenBinding(tokenBinding); + } + } + validateTokenBindingReference(tokenReq, validationBean, tokenBinding); validateAuthenticatedUser(validationBean, tokReqMsgCtx); if (log.isDebugEnabled()) { @@ -276,14 +290,6 @@ private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqM tokReqMsgCtx.setScope(validationBean.getScope()); tokReqMsgCtx.getOauth2AccessTokenReqDTO().setAccessTokenExtendedAttributes( validationBean.getAccessTokenExtendedAttributes()); - if (StringUtils.isNotBlank(validationBean.getTokenBindingReference()) && !NONE - .equals(validationBean.getTokenBindingReference())) { - Optional tokenBindingOptional = OAuthTokenPersistenceFactory.getInstance() - .getTokenBindingMgtDAO() - .getTokenBindingByBindingRef(validationBean.getTokenId(), - validationBean.getTokenBindingReference()); - tokenBindingOptional.ifPresent(tokReqMsgCtx::setTokenBinding); - } // Store the old access token as a OAuthTokenReqMessageContext property, this is already // a preprocessed token. tokReqMsgCtx.addProperty(PREV_ACCESS_TOKEN, validationBean); @@ -959,11 +965,11 @@ private boolean isRenewRefreshToken(String renewRefreshToken) { } private void validateTokenBindingReference(OAuth2AccessTokenReqDTO tokenReqDTO, - RefreshTokenValidationDataDO validationDataDO) + RefreshTokenValidationDataDO validationDataDO, + TokenBinding tokenBinding) throws IdentityOAuth2Exception { - if (StringUtils.isBlank(validationDataDO.getTokenBindingReference()) || NONE - .equals(validationDataDO.getTokenBindingReference())) { + if (tokenBinding == null) { return; } @@ -979,6 +985,18 @@ private void validateTokenBindingReference(OAuth2AccessTokenReqDTO tokenReqDTO, return; } + // Validate SSO session bound token. + if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(oAuthAppDO.getTokenBindingType())) { + if (!OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, validationDataDO.getAccessToken(), oAuthAppDO, + validationDataDO.getAuthorizedUser())) { + if (log.isDebugEnabled()) { + log.debug("Token is not bound to an active SSO session."); + } + throw new IdentityOAuth2Exception("Token binding validation failed. Token is not bound to an" + + "active SSO session."); + } + } + Optional tokenBinderOptional = OAuth2ServiceComponentHolder.getInstance() .getTokenBinder(oAuthAppDO.getTokenBindingType()); if (!tokenBinderOptional.isPresent()) { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java index 7d0b3cf5aba..5c04e66626f 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java @@ -64,6 +64,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException; import org.wso2.carbon.identity.application.authentication.framework.exception.UserSessionException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; @@ -141,6 +142,7 @@ import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.ClientAuthenticationMethodModel; import org.wso2.carbon.identity.oauth2.model.ClientCredentialDO; +import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; import org.wso2.carbon.identity.oauth2.token.JWTTokenIssuer; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; @@ -174,6 +176,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.net.HttpCookie; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -209,8 +212,10 @@ import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; import javax.xml.namespace.QName; +import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.COMMONAUTH_COOKIE; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.ORGANIZATION_LOGIN_IDP_NAME; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.USER_ID_CLAIM; import static org.wso2.carbon.identity.oauth.common.OAuthConstants.OAUTH_BUILD_ISSUER_WITH_HOSTNAME; @@ -6270,4 +6275,210 @@ public static boolean isExistingUser(String userName, String tenantDomain) throw realmService.getTenantUserRealm(tenantId).getUserStoreManager(); return userStoreManager.isExistingUser(MultitenantUtils.getTenantAwareUsername(userName)); } + + /** + * Get token binding value from the request headers. + * + * @param oAuth2AccessTokenReqDTO OAuth2AccessTokenReqDTO + * @param cookieName Cookie name + * @return Token binding value + */ + public static Optional getTokenBindingValue(OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO, + String cookieName) { + + HttpRequestHeader[] httpRequestHeaders = oAuth2AccessTokenReqDTO.getHttpRequestHeaders(); + if (ArrayUtils.isEmpty(httpRequestHeaders)) { + return Optional.empty(); + } + + for (HttpRequestHeader httpRequestHeader : httpRequestHeaders) { + if (HttpHeaders.COOKIE.equalsIgnoreCase(httpRequestHeader.getName())) { + if (ArrayUtils.isEmpty(httpRequestHeader.getValue())) { + return Optional.empty(); + } + + String[] cookies = httpRequestHeader.getValue()[0].split(";"); + String cookiePrefix = cookieName + "="; + for (String cookie : cookies) { + if (StringUtils.isNotBlank(cookie) && cookie.trim().startsWith(cookiePrefix) && + !HttpCookie.parse(cookie).isEmpty()) { + String cookieValue = HttpCookie.parse(cookie).get(0).getValue(); + if (StringUtils.isNotBlank(cookieValue)) { + if (COMMONAUTH_COOKIE.equals(cookieName)) { + // For sso-session binding, token binding value will be the session context id. + return Optional.of(DigestUtils.sha256Hex(cookieValue)); + } else { + return Optional.of(cookieValue); + } + } + } + } + } + } + return Optional.empty(); + } + + /** + * Check whether the SSO-session-bound access token is still tied to an active SSO session. + * + * @param accessTokenDO Access token data object. + * @return True if the token is bound to an active SSO session, false otherwise. + */ + public static boolean isTokenBoundToActiveSSOSession(AccessTokenDO accessTokenDO) { + + if (accessTokenDO == null || accessTokenDO.getAuthzUser() == null || accessTokenDO.getTokenBinding() == null) { + return false; + } + + if (!OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER + .equals(accessTokenDO.getTokenBinding().getBindingType()) || + StringUtils.isBlank(accessTokenDO.getTokenBinding().getBindingValue())) { + log.debug("No token binding value is found for SSO session bound token."); + return false; + } + + String sessionIdentifier = accessTokenDO.getTokenBinding().getBindingValue(); + String userTenantDomain = accessTokenDO.getAuthzUser().getTenantDomain(); + String consumerKey = accessTokenDO.getConsumerKey(); + SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier, userTenantDomain); + if (sessionContext == null) { + if (log.isDebugEnabled()) { + log.debug("Session context is not found corresponding to the session identifier: " + + sessionIdentifier); + } + // Revoke the SSO session bound access token. + try { + if (getAppInformationByClientId(consumerKey, getAppResidentTenantDomain(accessTokenDO)) + .isTokenRevocationWithIDPSessionTerminationEnabled()) { + revokeAccessToken(accessTokenDO); + } + } catch (IdentityOAuth2Exception | InvalidOAuthClientException e) { + log.error("Error while revoking SSO session bound access token.", e); + } + return false; + } + + if (log.isDebugEnabled()) { + log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); + } + return true; + } + + /** + * Check whether the SSO-session-bound access token is still tied to an active SSO session. + * + * @param tokenBinding Token binding object. + * @param accessTokenIdentifier Access token identifier. + * @param appDO OAuth application data object. + * @param authenticatedUser Authenticated user. + * @return True if the token is bound to an active SSO session, false otherwise. + */ + public static boolean isTokenBoundToActiveSSOSession(TokenBinding tokenBinding, String accessTokenIdentifier, + OAuthAppDO appDO, AuthenticatedUser authenticatedUser) { + + if (tokenBinding == null || accessTokenIdentifier == null || appDO == null || authenticatedUser == null) { + return false; + } + + if (!OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER + .equals(tokenBinding.getBindingType()) || + StringUtils.isBlank(tokenBinding.getBindingValue())) { + if (log.isDebugEnabled()) { + log.debug("No token binding value is found for SSO session bound token."); + } + return false; + } + + String sessionIdentifier = tokenBinding.getBindingValue(); + SessionContext sessionContext = + FrameworkUtils.getSessionContextFromCache(sessionIdentifier, authenticatedUser.getTenantDomain()); + if (sessionContext == null) { + if (log.isDebugEnabled()) { + log.debug("Session context is not found corresponding to the session identifier: " + + sessionIdentifier); + } + // Revoke the SSO session bound access token. + try { + if (appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { + AccessTokenDO accessTokenDO = getAccessTokenDOFromTokenIdentifier(accessTokenIdentifier, true); + revokeAccessToken(accessTokenDO); + } + } catch (IdentityOAuth2Exception e) { + log.error("Error while revoking SSO session bound access token.", e); + } + return false; + } + + if (log.isDebugEnabled()) { + log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); + } + return true; + } + + /** + * Revokes the given access token. + * + * @param accessTokenDO Access token data object. + * @throws IdentityOAuth2Exception If an error occurs while revoking the token. + */ + private static void revokeAccessToken(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { + + if (log.isDebugEnabled()) { + log.debug("Revoking token: " + accessTokenDO.getTokenId() + "for consumer key: " + + accessTokenDO.getConsumerKey()); + } + + try { + String tokenBindingReference = NONE; + if (accessTokenDO.getTokenBinding() != null && StringUtils + .isNotBlank(accessTokenDO.getTokenBinding().getBindingReference())) { + tokenBindingReference = accessTokenDO.getTokenBinding().getBindingReference(); + } + String consumerKey = accessTokenDO.getConsumerKey(); + String scope = OAuth2Util.buildScopeString(accessTokenDO.getScope()); + String userId = accessTokenDO.getAuthzUser().getUserId(); + + // Clear OAuth cache entries. + OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser(), scope, tokenBindingReference); + OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser(), scope); + OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser()); + OAuthUtil.clearOAuthCache(accessTokenDO); + + // Perform the token revocation in DB. + OAuthRevocationRequestDTO revokeRequestDTO = new OAuthRevocationRequestDTO(); + revokeRequestDTO.setConsumerKey(consumerKey); + revokeRequestDTO.setToken(accessTokenDO.getAccessToken()); + + synchronized ((consumerKey + ":" + userId + ":" + scope + ":" + tokenBindingReference).intern()) { + OAuth2ServiceComponentHolder.getInstance().getRevocationProcessor() + .revokeAccessToken(revokeRequestDTO, accessTokenDO); + } + + if (log.isDebugEnabled()) { + log.debug("Successfully revoked token: " + accessTokenDO.getTokenId() + + " and cleared associated cache entries."); + } + } catch (UserIdNotFoundException e) { + // Masking getLoggableUserId as it will return the username because the user id is not available. + log.error("User id cannot be found for user: " + accessTokenDO.getAuthzUser().getLoggableMaskedUserId()); + throw new IdentityOAuth2Exception("Failed to revoke token: " + accessTokenDO.getTokenId()); + } + } + + /** + * Get the resident tenant domain of the application associated with the access token. + * + * @param accessTokenDO Access token data object. + * @return Resident tenant domain of the application. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the tenant domain. + */ + private static String getAppResidentTenantDomain(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { + + String appResidentTenantDomain = getTenantDomain(accessTokenDO.getAppResidentTenantId()); + if (StringUtils.isBlank(appResidentTenantDomain)) { + // Get user domain as app domain. + appResidentTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + } + return appResidentTenantDomain; + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java index 9d7a7709d05..d66315aea85 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java @@ -24,9 +24,7 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; -import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; import org.wso2.carbon.identity.application.common.model.ServiceProvider; import org.wso2.carbon.identity.application.common.model.ServiceProviderProperty; @@ -680,10 +678,8 @@ private OAuth2IntrospectionResponseDTO validateAccessToken(OAuth2TokenValidation // Validate SSO session bound token. if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(bindingType)) { - if (!isTokenBoundToActiveSSOSession(accessTokenDO)) { - if (log.isDebugEnabled()) { - log.debug("Token is not bound to an active SSO session."); - } + if (!OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO)) { + log.debug("Token is not bound to an active SSO session."); introResp.setActive(false); return introResp; } @@ -1051,35 +1047,4 @@ private void validateIntrospectionForSubOrgTokens(String tenantDomain, AccessTok } } - /** - * Check whether the SSO-session-bound access token is still tied to an active SSO session. - * - * @param accessTokenDO the access token data object to validate. - * @return {@code true} if the token is bound to an active SSO session, {@code false} otherwise. - */ - private boolean isTokenBoundToActiveSSOSession(AccessTokenDO accessTokenDO) { - - if (StringUtils.isBlank(accessTokenDO.getTokenBinding().getBindingValue())) { - if (log.isDebugEnabled()) { - log.debug("No token binding value is found for SSO session bound token."); - } - return false; - } - - String sessionIdentifier = accessTokenDO.getTokenBinding().getBindingValue(); - String tenantDomain = accessTokenDO.getAuthzUser().getTenantDomain(); - SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier, tenantDomain); - if (sessionContext == null) { - if (log.isDebugEnabled()) { - log.debug("Session context is not found corresponding to the session identifier: " + - sessionIdentifier); - } - return false; - } - - if (log.isDebugEnabled()) { - log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); - } - return true; - } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java index 1266cf96866..4db9d6eff4d 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java @@ -34,7 +34,6 @@ import org.testng.annotations.Test; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.FederatedAuthenticatorConfig; @@ -639,9 +638,10 @@ public void testBuildIntrospectionResponseForSSOSessionBoundAccessToken(boolean MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); MockedStatic identityUtil = mockStatic(IdentityUtil.class); MockedStatic organizationManagementUtil = - mockStatic(OrganizationManagementUtil.class); - MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class)) { + mockStatic(OrganizationManagementUtil.class)) { + oAuth2Util.when(() -> OAuth2Util.isTokenBoundToActiveSSOSession(any(AccessTokenDO.class))) + .thenReturn(isSessionValid); organizationManagementUtil.when(() -> OrganizationManagementUtil.isOrganization(anyString())) .thenReturn(false); OAuth2ServiceComponentHolder.setIDPIdColumnEnabled(true); @@ -699,10 +699,6 @@ public void testBuildIntrospectionResponseForSSOSessionBoundAccessToken(boolean // As the token is dummy, no point in getting actual tenant details. oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(StringUtils.EMPTY); - SessionContext sessionContext = isSessionValid ? new SessionContext() : null; - frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) - .thenReturn(sessionContext); - OAuth2IntrospectionResponseDTO introspectionResponse = tokenValidationHandler .buildIntrospectionResponse(validationRequest); From 9e9220d964e788cba73b602a8bf83184ebf569ff Mon Sep 17 00:00:00 2001 From: AfraHussaindeen Date: Tue, 7 Oct 2025 09:54:24 +0530 Subject: [PATCH 2/4] Add unit test --- .../identity/oauth2/util/OAuth2Util.java | 11 +- .../identity/oauth2/util/OAuth2UtilTest.java | 235 ++++++++++++++++++ 2 files changed, 242 insertions(+), 4 deletions(-) diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java index 5c04e66626f..4b6247ccc33 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java @@ -242,6 +242,7 @@ import static org.wso2.carbon.identity.oauth.common.OAuthConstants.SignatureAlgorithms.PREVIOUS_KID_HASHING_ALGORITHM; import static org.wso2.carbon.identity.oauth2.Oauth2ScopeConstants.PERMISSIONS_BINDING_TYPE; import static org.wso2.carbon.identity.oauth2.device.constants.Constants.DEVICE_SUCCESS_ENDPOINT_PATH; +import static org.wso2.carbon.utils.multitenancy.MultitenantConstants.INVALID_TENANT_ID; /** * Utility methods for OAuth 2.0 implementation. @@ -6348,8 +6349,8 @@ public static boolean isTokenBoundToActiveSSOSession(AccessTokenDO accessTokenDO } // Revoke the SSO session bound access token. try { - if (getAppInformationByClientId(consumerKey, getAppResidentTenantDomain(accessTokenDO)) - .isTokenRevocationWithIDPSessionTerminationEnabled()) { + OAuthAppDO appDO = getAppInformationByClientId(consumerKey, getAppResidentTenantDomain(accessTokenDO)); + if (appDO != null && appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { revokeAccessToken(accessTokenDO); } } catch (IdentityOAuth2Exception | InvalidOAuthClientException e) { @@ -6474,9 +6475,11 @@ private static void revokeAccessToken(AccessTokenDO accessTokenDO) throws Identi */ private static String getAppResidentTenantDomain(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { - String appResidentTenantDomain = getTenantDomain(accessTokenDO.getAppResidentTenantId()); + String appResidentTenantDomain = null; + if (accessTokenDO.getAppResidentTenantId() != INVALID_TENANT_ID) { + appResidentTenantDomain = getTenantDomain(accessTokenDO.getAppResidentTenantId()); + } if (StringUtils.isBlank(appResidentTenantDomain)) { - // Get user domain as app domain. appResidentTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); } return appResidentTenantDomain; diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java index c86a21ed057..2fdfafad192 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java @@ -48,6 +48,7 @@ import org.wso2.carbon.base.CarbonBaseConstants; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; @@ -88,6 +89,7 @@ import org.wso2.carbon.identity.oauth.dto.ScopeDTO; import org.wso2.carbon.identity.oauth.dto.TokenBindingMetaDataDTO; import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder; +import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultOAuth2RevocationProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.HashingPersistenceProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.TokenPersistenceProcessor; @@ -98,12 +100,16 @@ import org.wso2.carbon.identity.oauth2.client.authentication.OAuthClientAuthnException; import org.wso2.carbon.identity.oauth2.dao.AccessTokenDAO; import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.ClientAuthenticationMethodModel; import org.wso2.carbon.identity.oauth2.model.ClientCredentialDO; +import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; +import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding; import org.wso2.carbon.identity.oauth2.token.handlers.grant.AuthorizationGrantHandler; import org.wso2.carbon.identity.openidconnect.dao.ScopeClaimMappingDAO; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; @@ -137,6 +143,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.function.Supplier; @@ -208,6 +215,14 @@ public class OAuth2UtilTest { private static final String ES256 = "ES256"; private static final String PS256 = "PS256"; private static final String ES384 = "ES384"; + private static final String COMMONAUTH_COOKIE = "commonAuthId"; + private static final String TEST_TOKEN_IDENTIFIER = "test_token_identifier"; + private static final String TEST_BINDING_VALUE = "test_binding_value"; + private static final String TEST_BINDING_REFERENCE = "test_binding_reference"; + private static final String TEST_USER_ID = "test-user-id"; + private static final String TEST_COOKIE_NAME = "test-cookie"; + private static final String TEST_COOKIE_VALUE = "test-cookie-value"; + private static final String COOKIE_HEADER = "Cookie"; @Mock private OAuthServerConfiguration oauthServerConfigurationMock; @@ -3457,4 +3472,224 @@ public void testGetOrgAuthenticatedUser(String userId, String tenantDomain, Stri } } } + + @DataProvider(name = "tokenBindingValueProvider") + public Object[][] tokenBindingValueProvider() { + + String commonAuthCookieValue = "common-auth-cookie-value"; + String hashedCommonAuthValue = DigestUtils.sha256Hex(commonAuthCookieValue); + + HttpRequestHeader[] noHeaders = new HttpRequestHeader[0]; + HttpRequestHeader[] headersWithCookie = new HttpRequestHeader[]{new HttpRequestHeader(COOKIE_HEADER, + TEST_COOKIE_NAME + "=" + TEST_COOKIE_VALUE)}; + HttpRequestHeader[] headersWithCommonAuthCookie = new HttpRequestHeader[]{new HttpRequestHeader(COOKIE_HEADER, + COMMONAUTH_COOKIE + "=" + commonAuthCookieValue)}; + HttpRequestHeader[] headersWithMultipleCookies = new HttpRequestHeader[]{new HttpRequestHeader(COOKIE_HEADER, + "some-cookie=some-value;" + TEST_COOKIE_NAME + "=" + TEST_COOKIE_VALUE)}; + HttpRequestHeader[] headersWithBlankCookieValue = new HttpRequestHeader[]{new HttpRequestHeader(COOKIE_HEADER, + TEST_COOKIE_NAME + "=")}; + HttpRequestHeader[] headersWithNoCookieHeader = new HttpRequestHeader[]{new HttpRequestHeader("Some-Header", + "some-value")}; + HttpRequestHeader[] headersWithEmptyValue = + new HttpRequestHeader[]{new HttpRequestHeader(COOKIE_HEADER)}; + + return new Object[][]{ + {null, TEST_COOKIE_NAME, Optional.empty()}, + {noHeaders, TEST_COOKIE_NAME, Optional.empty()}, + {headersWithCookie, TEST_COOKIE_NAME, Optional.of(TEST_COOKIE_VALUE)}, + {headersWithCommonAuthCookie, COMMONAUTH_COOKIE, Optional.of(hashedCommonAuthValue)}, + {headersWithMultipleCookies, TEST_COOKIE_NAME, Optional.of(TEST_COOKIE_VALUE)}, + {headersWithBlankCookieValue, TEST_COOKIE_NAME, Optional.empty()}, + {headersWithNoCookieHeader, TEST_COOKIE_NAME, Optional.empty()}, + {headersWithCookie, "non-existent-cookie", Optional.empty()}, + {headersWithEmptyValue, TEST_COOKIE_NAME, Optional.empty()} + }; + } + + @Test(dataProvider = "tokenBindingValueProvider") + public void testGetTokenBindingValue(HttpRequestHeader[] headers, String cookieName, + Optional expectedValue) { + + OAuth2AccessTokenReqDTO reqDTO = new OAuth2AccessTokenReqDTO(); + reqDTO.setHttpRequestHeaders(headers); + Optional tokenBindingValue = OAuth2Util.getTokenBindingValue(reqDTO, cookieName); + assertEquals(tokenBindingValue, expectedValue); + } + + @DataProvider(name = "boundAccessTokenDOProvider") + public Object[][] boundAccessTokenDOProvider() { + + SessionContext activeSession = new SessionContext(); + return new Object[][]{ + // accessTokenDO, sessionContext, expected result + {null, null, false}, + {createAccessTokenDO(false, false, false), null, false}, + {createAccessTokenDO(true, false, false), null, false}, + {createAccessTokenDO(true, true, false), null, false}, + // Session is not active. + {createAccessTokenDO(true, true, true), null, false}, + // Session is active. + {createAccessTokenDO(true, true, true), activeSession, true}, + }; + } + + @Test(dataProvider = "boundAccessTokenDOProvider") + public void testIsTokenBoundToActiveSSOSession_WithTokenDO(AccessTokenDO accessTokenDO, + SessionContext sessionContext, boolean expectedResult) { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS)) { + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(sessionContext); + + if (sessionContext == null) { + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setTokenRevocationWithIDPSessionTerminationEnabled(false); + oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(SUPER_TENANT_DOMAIN_NAME); + oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())) + .thenReturn(appDO); + } + assertEquals(OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO), expectedResult); + } + } + + @Test + public void testIsTokenBoundToActiveSSOSession_WithTokenDO_TokenRevocationEnabled() throws Exception { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS); + MockedStatic oAuthUtil = mockStatic(OAuthUtil.class); + MockedStatic oAuth2ServiceComponentHolder = + mockStatic(OAuth2ServiceComponentHolder.class)) { + + AccessTokenDO accessTokenDO = createAccessTokenDO(true, true, true); + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(null); + + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setTokenRevocationWithIDPSessionTerminationEnabled(true); + oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())).thenReturn(appDO); + oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(SUPER_TENANT_DOMAIN_NAME); + + OAuth2ServiceComponentHolder mockServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); + oAuth2ServiceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(mockServiceComponentHolder); + DefaultOAuth2RevocationProcessor revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); + when(mockServiceComponentHolder.getRevocationProcessor()).thenReturn(revocationProcessor); + + assertFalse(OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO)); + verify(revocationProcessor, times(1)).revokeAccessToken( + any(OAuthRevocationRequestDTO.class), eq(accessTokenDO)); + } + } + + @DataProvider(name = "boundAccessTokenIdentifierProvider") + public Object[][] boundAccessTokenIdentifierProvider() { + + TokenBinding tokenBinding = new TokenBinding(); + tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + tokenBinding.setBindingValue(TEST_BINDING_VALUE); + + TokenBinding wrongTypeBinding = new TokenBinding(); + wrongTypeBinding.setBindingType("WRONG_TYPE"); + wrongTypeBinding.setBindingValue(TEST_BINDING_VALUE); + + TokenBinding blankValueBinding = new TokenBinding(); + blankValueBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + blankValueBinding.setBindingValue(""); + + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setTokenRevocationWithIDPSessionTerminationEnabled(false); + AuthenticatedUser user = new AuthenticatedUser(); + user.setTenantDomain(SUPER_TENANT_DOMAIN_NAME); + String accessTokenIdentifier = TEST_TOKEN_IDENTIFIER; + SessionContext activeSession = new SessionContext(); + + return new Object[][]{ + // tokenBinding, accessTokenIdentifier, appDO, user, sessionContext, expectedResult + {null, null, null, null, null, false}, + {tokenBinding, accessTokenIdentifier, appDO, user, activeSession, true}, + {tokenBinding, accessTokenIdentifier, appDO, user, null, false}, + {wrongTypeBinding, accessTokenIdentifier, appDO, user, null, false}, + {blankValueBinding, accessTokenIdentifier, appDO, user, null, false} + }; + } + + @Test(dataProvider = "boundAccessTokenIdentifierProvider") + public void testIsTokenBoundToActiveSSOSession_WithTokenIdentifier(TokenBinding tokenBinding, + String accessTokenIdentifier, + OAuthAppDO appDO, + AuthenticatedUser user, + SessionContext sessionContext, + boolean expectedResult) { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class)) { + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(sessionContext); + assertEquals(OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, accessTokenIdentifier, appDO, user), + expectedResult); + } + } + + @Test + public void testIsTokenBoundToActiveSSOSession_WithTokenIdentifier_TokenRevocationEnabled() throws Exception { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS); + MockedStatic oAuthUtil = mockStatic(OAuthUtil.class); + MockedStatic oAuth2ServiceComponentHolder = + mockStatic(OAuth2ServiceComponentHolder.class)) { + + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(null); + + TokenBinding tokenBinding = new TokenBinding(); + tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + tokenBinding.setBindingValue(TEST_BINDING_VALUE); + + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setTokenRevocationWithIDPSessionTerminationEnabled(true); + AuthenticatedUser user = new AuthenticatedUser(); + String accessTokenIdentifier = TEST_TOKEN_IDENTIFIER; + AccessTokenDO accessTokenDO = createAccessTokenDO(true, true, true); + + oAuth2Util.when(() -> OAuth2Util.getAccessTokenDOFromTokenIdentifier(accessTokenIdentifier, true)) + .thenReturn(accessTokenDO); + + OAuth2ServiceComponentHolder mockServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); + oAuth2ServiceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(mockServiceComponentHolder); + DefaultOAuth2RevocationProcessor revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); + when(mockServiceComponentHolder.getRevocationProcessor()).thenReturn(revocationProcessor); + + assertFalse(OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, accessTokenIdentifier, appDO, user)); + verify(revocationProcessor, times(1)).revokeAccessToken( + any(OAuthRevocationRequestDTO.class), eq(accessTokenDO)); + } + } + + private AccessTokenDO createAccessTokenDO(boolean withUser, boolean withTokenBinding, boolean withSSOBinding) { + + AccessTokenDO accessTokenDO = new AccessTokenDO(); + accessTokenDO.setConsumerKey(clientId); + accessTokenDO.setAppResidentTenantId(SUPER_TENANT_ID); + if (withUser) { + AuthenticatedUser user = new AuthenticatedUser(); + user.setTenantDomain(SUPER_TENANT_DOMAIN_NAME); + user.setUserId(TEST_USER_ID); + accessTokenDO.setAuthzUser(user); + } + if (withTokenBinding) { + TokenBinding tokenBinding = new TokenBinding(); + if (withSSOBinding) { + tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + } else { + tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.COOKIE_BASED_TOKEN_BINDER); + } + tokenBinding.setBindingValue(TEST_BINDING_VALUE); + tokenBinding.setBindingReference(TEST_BINDING_REFERENCE); + accessTokenDO.setTokenBinding(tokenBinding); + } + return accessTokenDO; + } } From cd884823989437cd38b0a81189eeaf3ee0a6f4ae Mon Sep 17 00:00:00 2001 From: AfraHussaindeen Date: Tue, 7 Oct 2025 15:30:04 +0530 Subject: [PATCH 3/4] Add unit test and update logs --- .../handlers/grant/RefreshGrantHandler.java | 6 +- .../identity/oauth2/util/OAuth2Util.java | 6 +- .../impl/SSOSessionBasedTokenBinderTest.java | 148 ++++++++++++++++++ .../grant/RefreshGrantHandlerTest.java | 71 +++++++++ .../src/test/resources/testng.xml | 1 + 5 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinderTest.java diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java index 231a32ca74f..ec866bc6eef 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java @@ -1008,10 +1008,8 @@ private void validateTokenBindingReference(OAuth2AccessTokenReqDTO tokenReqDTO, if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(oAuthAppDO.getTokenBindingType())) { if (!OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, validationDataDO.getAccessToken(), oAuthAppDO, validationDataDO.getAuthorizedUser())) { - if (log.isDebugEnabled()) { - log.debug("Token is not bound to an active SSO session."); - } - throw new IdentityOAuth2Exception("Token binding validation failed. Token is not bound to an" + + log.debug("Token is not bound to an active SSO session."); + throw new IdentityOAuth2Exception("Token binding validation failed. Token is not bound to an " + "active SSO session."); } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java index b31845b0d03..b2184b168a4 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java @@ -6436,9 +6436,7 @@ public static boolean isTokenBoundToActiveSSOSession(TokenBinding tokenBinding, if (!OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER .equals(tokenBinding.getBindingType()) || StringUtils.isBlank(tokenBinding.getBindingValue())) { - if (log.isDebugEnabled()) { - log.debug("No token binding value is found for SSO session bound token."); - } + log.debug("No token binding value is found for SSO session bound token."); return false; } @@ -6477,7 +6475,7 @@ public static boolean isTokenBoundToActiveSSOSession(TokenBinding tokenBinding, private static void revokeAccessToken(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { if (log.isDebugEnabled()) { - log.debug("Revoking token: " + accessTokenDO.getTokenId() + "for consumer key: " + + log.debug("Revoking token: " + accessTokenDO.getTokenId() + " for consumer key: " + accessTokenDO.getConsumerKey()); } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinderTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinderTest.java new file mode 100644 index 00000000000..b4bb77c7a28 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinderTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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.oauth2.token.bindings.impl; + +import org.apache.commons.codec.digest.DigestUtils; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.oauth.common.OAuthConstants; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.util.Optional; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +@Listeners(MockitoTestNGListener.class) +@WithCarbonHome +public class SSOSessionBasedTokenBinderTest { + + @Mock + private OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO; + + @Mock + private HttpServletRequest httpServletRequest; + + private SSOSessionBasedTokenBinder ssoSessionBasedTokenBinder; + + private static final String COMMONAUTH_COOKIE = "commonAuthId"; + private static final String COMMONAUTH_COOKIE_VALUE = "common-auth-cookie-value"; + private static final String SESSION_IDENTIFIER = DigestUtils.sha256Hex(COMMONAUTH_COOKIE_VALUE); + private static final String BINDING_REFERENCE = "sso-binding-reference"; + + @BeforeMethod + public void setUp() { + + ssoSessionBasedTokenBinder = new SSOSessionBasedTokenBinder(); + } + + @DataProvider(name = "tokenBindingDataProviderForDTO") + public Object[][] tokenBindingDataProviderForDTO() { + + SessionContext sessionContext = mock(SessionContext.class); + return new Object[][]{ + // A valid session context exists for the session identifier, so the binding is valid. + {SESSION_IDENTIFIER, sessionContext, true}, + // No session context for the session identifier (e.g., expired session), so binding is invalid. + {SESSION_IDENTIFIER, null, false}, + // No session identifier cookie in the request, so binding is invalid. + {null, null, false} + }; + } + + @Test(dataProvider = "tokenBindingDataProviderForDTO") + public void testIsValidTokenBindingWithDTO(String sessionIdentifier, SessionContext sessionContext, + boolean expectedResult) { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class)) { + + oAuth2Util.when(() -> OAuth2Util.getTokenBindingValue(oAuth2AccessTokenReqDTO, COMMONAUTH_COOKIE)) + .thenReturn(Optional.ofNullable(sessionIdentifier)); + + if (sessionIdentifier != null) { + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(sessionIdentifier)) + .thenReturn(sessionContext); + // We only need to mock the reference generation for the happy path (when a session exists). + if (sessionContext != null) { + oAuth2Util.when(() -> OAuth2Util.getTokenBindingReference(sessionIdentifier)) + .thenReturn(BINDING_REFERENCE); + when(oAuth2AccessTokenReqDTO.getGrantType()).thenReturn(OAuthConstants.GrantTypes.REFRESH_TOKEN); + } + } + + assertEquals(ssoSessionBasedTokenBinder.isValidTokenBinding(oAuth2AccessTokenReqDTO, BINDING_REFERENCE), + expectedResult); + } + } + + @DataProvider(name = "tokenBindingDataProviderForRequest") + public Object[][] tokenBindingDataProviderForRequest() { + + SessionContext sessionContext = mock(SessionContext.class); + Cookie validCookie = new Cookie(COMMONAUTH_COOKIE, COMMONAUTH_COOKIE_VALUE); + return new Object[][]{ + // A valid session context exists for the session identifier in the cookie, so the binding is valid. + {new Cookie[]{validCookie}, sessionContext, true}, + // No session context for the session identifier (e.g., expired session), so binding is invalid. + {new Cookie[]{validCookie}, null, false}, + // No commonAuth cookie in the request, so binding is invalid. + {new Cookie[]{}, null, false}, + // No cookies in the request at all, so binding is invalid. + {null, null, false} + }; + } + + @Test(dataProvider = "tokenBindingDataProviderForRequest") + public void testIsValidTokenBindingWithRequest(Cookie[] cookies, SessionContext sessionContext, + boolean expectedResult) { + + try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class)) { + + when(httpServletRequest.getCookies()).thenReturn(cookies); + + if (cookies != null && cookies.length > 0) { + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(SESSION_IDENTIFIER)) + .thenReturn(sessionContext); + if (sessionContext != null) { + oAuth2Util.when(() -> OAuth2Util.getTokenBindingReference(SESSION_IDENTIFIER)) + .thenReturn(BINDING_REFERENCE); + } + } + + assertEquals(ssoSessionBasedTokenBinder.isValidTokenBinding(httpServletRequest, BINDING_REFERENCE), + expectedResult); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java index 4237268e086..7fe6829c19e 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java @@ -26,19 +26,25 @@ import org.wso2.carbon.identity.application.authentication.framework.inbound.FrameworkClientException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; +import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; import org.wso2.carbon.identity.oauth.rar.model.AuthorizationDetails; import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultRefreshTokenGrantProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.RefreshTokenGrantProcessor; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.OAuth2Constants; +import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; +import org.wso2.carbon.identity.oauth2.dao.TokenBindingMgtDAO; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO; import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import org.wso2.carbon.identity.test.common.testng.utils.MockAuthenticatedUser; import org.wso2.carbon.identity.user.profile.mgt.association.federation.FederatedAssociationManager; @@ -46,6 +52,8 @@ import org.wso2.carbon.identity.user.profile.mgt.association.federation.exception.FederatedAssociationManagerException; import org.wso2.carbon.user.core.UserCoreConstants; +import java.util.Optional; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -63,6 +71,7 @@ /** * Unit tests for the RefreshGrantHandler class. */ +@WithCarbonHome public class RefreshGrantHandlerTest { private RefreshTokenGrantProcessor refreshTokenGrantProcessor; @@ -72,6 +81,7 @@ public class RefreshGrantHandlerTest { private OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO; private OAuth2ServiceComponentHolder oAuth2ServiceComponentHolder; private AuthorizationDetailsService authorizationDetailsService; + private OAuthAppDO oAuthAppDO; @BeforeMethod public void init() { @@ -82,6 +92,7 @@ public void init() { oAuth2AccessTokenReqDTO = mock(OAuth2AccessTokenReqDTO.class); oAuth2ServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); authorizationDetailsService = mock(AuthorizationDetailsService.class); + oAuthAppDO = mock(OAuthAppDO.class); } @DataProvider(name = "validateGrantWhenUserIsLockedInUserStoreEnd") @@ -214,4 +225,64 @@ public void testValidateGrantWhenUserIsLockedInUserStoreEnd(AuthenticatedUser us } } } + + @Test(expectedExceptions = IdentityOAuth2Exception.class, + description = "Ensure the refresh grant flow fails for an SSO session-bound token if the corresponding " + + "session has expired, even with a valid refresh token.") + public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws Exception { + + when(refreshTokenGrantProcessor.validateRefreshToken(any())).thenReturn(refreshTokenValidationDataDO); + when(refreshTokenValidationDataDO.getAuthorizedUser()).thenReturn(new MockAuthenticatedUser("test_user")); + when(refreshTokenGrantProcessor.isLatestRefreshToken(any(), any(), any())).thenReturn(true); + when(oAuthServerConfiguration.isValidateAuthenticatedUserForRefreshGrantEnabled()).thenReturn(false); + when(oAuth2ServiceComponentHolder.getRefreshTokenGrantProcessor()).thenReturn(refreshTokenGrantProcessor); + when(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO()).thenReturn(oAuth2AccessTokenReqDTO); + when(oAuth2AccessTokenReqDTO.getClientId()).thenReturn("test_client_id"); + when(refreshTokenValidationDataDO.getTokenBindingReference()).thenReturn("sso_binding_ref"); + when(refreshTokenValidationDataDO.getTokenId()).thenReturn("sso_token_id"); + when(refreshTokenValidationDataDO.getRefreshTokenState()) + .thenReturn(OAuthConstants.TokenStates.TOKEN_STATE_ACTIVE); + + TokenBinding tokenBinding = new TokenBinding(); + tokenBinding.setBindingReference("sso_binding_ref"); + tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + + TokenBindingMgtDAO tokenBindingMgtDAO = mock(TokenBindingMgtDAO.class); + when(tokenBindingMgtDAO.getTokenBindingByBindingRef(anyString(), anyString())) + .thenReturn(Optional.of(tokenBinding)); + + OAuthTokenPersistenceFactory oAuthTokenPersistenceFactory = mock(OAuthTokenPersistenceFactory.class); + when(oAuthTokenPersistenceFactory.getTokenBindingMgtDAO()).thenReturn(tokenBindingMgtDAO); + + when(oAuthAppDO.getTokenBindingType()) + .thenReturn(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + + try (MockedStatic oAuthTokenPersistenceFactoryMockedStatic = + mockStatic(OAuthTokenPersistenceFactory.class); + MockedStatic oAuth2UtilMockedStatic = mockStatic(OAuth2Util.class); + MockedStatic oAuthServerConfigurationMockedStatic = + mockStatic(OAuthServerConfiguration.class); + MockedStatic oAuth2ServiceComponentHolderMockedStatic = + mockStatic(OAuth2ServiceComponentHolder.class)) { + + oAuthTokenPersistenceFactoryMockedStatic.when(OAuthTokenPersistenceFactory::getInstance) + .thenReturn(oAuthTokenPersistenceFactory); + + oAuth2UtilMockedStatic.when(() -> OAuth2Util.getTenantId(anyString())).thenReturn(TENANT_ID); + oAuth2UtilMockedStatic.when(() -> OAuth2Util.getAppInformationByClientId(anyString())) + .thenReturn(oAuthAppDO); + oAuth2UtilMockedStatic.when(() -> OAuth2Util + .isTokenBoundToActiveSSOSession(any(), anyString(), any(), any())).thenReturn(false); + + oAuthServerConfigurationMockedStatic.when(OAuthServerConfiguration::getInstance) + .thenReturn(oAuthServerConfiguration); + + oAuth2ServiceComponentHolderMockedStatic.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(oAuth2ServiceComponentHolder); + + RefreshGrantHandler refreshGrantHandler = new RefreshGrantHandler(); + refreshGrantHandler.init(); + refreshGrantHandler.validateGrant(oAuthTokenReqMessageContext); + } + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml index 2d7f6b4f0dd..19595edd353 100755 --- a/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml @@ -43,6 +43,7 @@ + From c25aeb225859e1bd6cdc561ed827d2fe585ce9d0 Mon Sep 17 00:00:00 2001 From: AfraHussaindeen Date: Thu, 9 Oct 2025 03:49:23 +0530 Subject: [PATCH 4/4] Refactor the code. --- .../handlers/grant/RefreshGrantHandler.java | 58 +++++- .../identity/oauth2/util/OAuth2Util.java | 166 ---------------- .../validators/TokenValidationHandler.java | 84 +++++++- .../grant/RefreshGrantHandlerTest.java | 54 +++++- .../identity/oauth2/util/OAuth2UtilTest.java | 181 ------------------ .../TokenValidationHandlerTest.java | 61 ++++-- 6 files changed, 227 insertions(+), 377 deletions(-) diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java index ec866bc6eef..a82b18fbf8d 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java @@ -30,6 +30,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Error; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.exception.FrameworkException; import org.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException; import org.wso2.carbon.identity.application.authentication.framework.inbound.FrameworkClientException; @@ -40,6 +41,7 @@ import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; +import org.wso2.carbon.identity.oauth.OAuthUtil; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCache; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheEntry; import org.wso2.carbon.identity.oauth.cache.AuthorizationGrantCacheKey; @@ -61,6 +63,7 @@ import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO; @@ -1006,9 +1009,10 @@ private void validateTokenBindingReference(OAuth2AccessTokenReqDTO tokenReqDTO, // Validate SSO session bound token. if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(oAuthAppDO.getTokenBindingType())) { - if (!OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, validationDataDO.getAccessToken(), oAuthAppDO, + if (!isTokenBoundToActiveSSOSession(tokenBinding.getBindingValue(), validationDataDO.getAuthorizedUser())) { - log.debug("Token is not bound to an active SSO session."); + // Revoke the SSO session bound access token if the session is invalid/terminated. + revokeSSOSessionBoundToken(oAuthAppDO, validationDataDO.getAccessToken()); throw new IdentityOAuth2Exception("Token binding validation failed. Token is not bound to an " + "active SSO session."); } @@ -1065,4 +1069,54 @@ private void setRARPropertiesForTokenGeneration(final OAuthTokenReqMessageContex oAuthTokenReqMessageContext.setAuthorizationDetails(AuthorizationDetailsUtils .getTrimmedAuthorizationDetails(tokenAuthorizationDetails)); } + + /** + * Check whether the SSO-session-bound access token is still tied to an active SSO session. + * + * @param sessionIdentifier Session identifier. + * @param authenticatedUser Authenticated user. + * @return True if the token is bound to an active SSO session, false otherwise. + */ + private boolean isTokenBoundToActiveSSOSession(String sessionIdentifier, AuthenticatedUser authenticatedUser) { + + SessionContext sessionContext = + FrameworkUtils.getSessionContextFromCache(sessionIdentifier, authenticatedUser.getTenantDomain()); + if (sessionContext == null) { + if (log.isDebugEnabled()) { + log.debug("Session context is not found corresponding to the session identifier: " + + sessionIdentifier); + } + return false; + } + + if (log.isDebugEnabled()) { + log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); + } + return true; + } + + /** + * Revoke the SSO session bound access token if the associated session is terminated. + * This is only applicable for the applications that have enabled 'revokeTokensWhenIdPSessionTerminated'. + * + * @param appDO OAuth application data object. + * @param accessTokenIdentifier Access token identifier. + */ + private void revokeSSOSessionBoundToken(OAuthAppDO appDO, String accessTokenIdentifier) { + + try { + if (appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { + AccessTokenDO accessTokenDO = + OAuth2Util.getAccessTokenDOFromTokenIdentifier(accessTokenIdentifier, true); + OAuthUtil.clearOAuthCache(accessTokenDO); + OAuthRevocationRequestDTO revokeRequestDTO = new OAuthRevocationRequestDTO(); + revokeRequestDTO.setConsumerKey(accessTokenDO.getConsumerKey()); + revokeRequestDTO.setToken(accessTokenDO.getAccessToken()); + OAuth2ServiceComponentHolder.getInstance().getRevocationProcessor() + .revokeAccessToken(revokeRequestDTO, accessTokenDO); + } + } catch (IdentityOAuth2Exception | UserIdNotFoundException e) { + log.error("Error while revoking SSO session bound access token.", e); + } + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java index b2184b168a4..97c4d535712 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/util/OAuth2Util.java @@ -64,7 +64,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException; import org.wso2.carbon.identity.application.authentication.framework.exception.UserSessionException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; @@ -242,7 +241,6 @@ import static org.wso2.carbon.identity.oauth.common.OAuthConstants.SignatureAlgorithms.PREVIOUS_KID_HASHING_ALGORITHM; import static org.wso2.carbon.identity.oauth2.Oauth2ScopeConstants.PERMISSIONS_BINDING_TYPE; import static org.wso2.carbon.identity.oauth2.device.constants.Constants.DEVICE_SUCCESS_ENDPOINT_PATH; -import static org.wso2.carbon.utils.multitenancy.MultitenantConstants.INVALID_TENANT_ID; /** * Utility methods for OAuth 2.0 implementation. @@ -6370,168 +6368,4 @@ public static Optional getTokenBindingValue(OAuth2AccessTokenReqDTO oAut } return Optional.empty(); } - - /** - * Check whether the SSO-session-bound access token is still tied to an active SSO session. - * - * @param accessTokenDO Access token data object. - * @return True if the token is bound to an active SSO session, false otherwise. - */ - public static boolean isTokenBoundToActiveSSOSession(AccessTokenDO accessTokenDO) { - - if (accessTokenDO == null || accessTokenDO.getAuthzUser() == null || accessTokenDO.getTokenBinding() == null) { - return false; - } - - if (!OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER - .equals(accessTokenDO.getTokenBinding().getBindingType()) || - StringUtils.isBlank(accessTokenDO.getTokenBinding().getBindingValue())) { - log.debug("No token binding value is found for SSO session bound token."); - return false; - } - - String sessionIdentifier = accessTokenDO.getTokenBinding().getBindingValue(); - String userTenantDomain = accessTokenDO.getAuthzUser().getTenantDomain(); - String consumerKey = accessTokenDO.getConsumerKey(); - SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier, userTenantDomain); - if (sessionContext == null) { - if (log.isDebugEnabled()) { - log.debug("Session context is not found corresponding to the session identifier: " + - sessionIdentifier); - } - // Revoke the SSO session bound access token. - try { - OAuthAppDO appDO = getAppInformationByClientId(consumerKey, getAppResidentTenantDomain(accessTokenDO)); - if (appDO != null && appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { - revokeAccessToken(accessTokenDO); - } - } catch (IdentityOAuth2Exception | InvalidOAuthClientException e) { - log.error("Error while revoking SSO session bound access token.", e); - } - return false; - } - - if (log.isDebugEnabled()) { - log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); - } - return true; - } - - /** - * Check whether the SSO-session-bound access token is still tied to an active SSO session. - * - * @param tokenBinding Token binding object. - * @param accessTokenIdentifier Access token identifier. - * @param appDO OAuth application data object. - * @param authenticatedUser Authenticated user. - * @return True if the token is bound to an active SSO session, false otherwise. - */ - public static boolean isTokenBoundToActiveSSOSession(TokenBinding tokenBinding, String accessTokenIdentifier, - OAuthAppDO appDO, AuthenticatedUser authenticatedUser) { - - if (tokenBinding == null || accessTokenIdentifier == null || appDO == null || authenticatedUser == null) { - return false; - } - - if (!OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER - .equals(tokenBinding.getBindingType()) || - StringUtils.isBlank(tokenBinding.getBindingValue())) { - log.debug("No token binding value is found for SSO session bound token."); - return false; - } - - String sessionIdentifier = tokenBinding.getBindingValue(); - SessionContext sessionContext = - FrameworkUtils.getSessionContextFromCache(sessionIdentifier, authenticatedUser.getTenantDomain()); - if (sessionContext == null) { - if (log.isDebugEnabled()) { - log.debug("Session context is not found corresponding to the session identifier: " + - sessionIdentifier); - } - // Revoke the SSO session bound access token. - try { - if (appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { - AccessTokenDO accessTokenDO = getAccessTokenDOFromTokenIdentifier(accessTokenIdentifier, true); - revokeAccessToken(accessTokenDO); - } - } catch (IdentityOAuth2Exception e) { - log.error("Error while revoking SSO session bound access token.", e); - } - return false; - } - - if (log.isDebugEnabled()) { - log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); - } - return true; - } - - /** - * Revokes the given access token. - * - * @param accessTokenDO Access token data object. - * @throws IdentityOAuth2Exception If an error occurs while revoking the token. - */ - private static void revokeAccessToken(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { - - if (log.isDebugEnabled()) { - log.debug("Revoking token: " + accessTokenDO.getTokenId() + " for consumer key: " + - accessTokenDO.getConsumerKey()); - } - - try { - String tokenBindingReference = NONE; - if (accessTokenDO.getTokenBinding() != null && StringUtils - .isNotBlank(accessTokenDO.getTokenBinding().getBindingReference())) { - tokenBindingReference = accessTokenDO.getTokenBinding().getBindingReference(); - } - String consumerKey = accessTokenDO.getConsumerKey(); - String scope = OAuth2Util.buildScopeString(accessTokenDO.getScope()); - String userId = accessTokenDO.getAuthzUser().getUserId(); - - // Clear OAuth cache entries. - OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser(), scope, tokenBindingReference); - OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser(), scope); - OAuthUtil.clearOAuthCache(consumerKey, accessTokenDO.getAuthzUser()); - OAuthUtil.clearOAuthCache(accessTokenDO); - - // Perform the token revocation in DB. - OAuthRevocationRequestDTO revokeRequestDTO = new OAuthRevocationRequestDTO(); - revokeRequestDTO.setConsumerKey(consumerKey); - revokeRequestDTO.setToken(accessTokenDO.getAccessToken()); - - synchronized ((consumerKey + ":" + userId + ":" + scope + ":" + tokenBindingReference).intern()) { - OAuth2ServiceComponentHolder.getInstance().getRevocationProcessor() - .revokeAccessToken(revokeRequestDTO, accessTokenDO); - } - - if (log.isDebugEnabled()) { - log.debug("Successfully revoked token: " + accessTokenDO.getTokenId() + - " and cleared associated cache entries."); - } - } catch (UserIdNotFoundException e) { - // Masking getLoggableUserId as it will return the username because the user id is not available. - log.error("User id cannot be found for user: " + accessTokenDO.getAuthzUser().getLoggableMaskedUserId()); - throw new IdentityOAuth2Exception("Failed to revoke token: " + accessTokenDO.getTokenId()); - } - } - - /** - * Get the resident tenant domain of the application associated with the access token. - * - * @param accessTokenDO Access token data object. - * @return Resident tenant domain of the application. - * @throws IdentityOAuth2Exception If an error occurs while retrieving the tenant domain. - */ - private static String getAppResidentTenantDomain(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { - - String appResidentTenantDomain = null; - if (accessTokenDO.getAppResidentTenantId() != INVALID_TENANT_ID) { - appResidentTenantDomain = getTenantDomain(accessTokenDO.getAppResidentTenantId()); - } - if (StringUtils.isBlank(appResidentTenantDomain)) { - appResidentTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); - } - return appResidentTenantDomain; - } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java index d66315aea85..145353c4cd4 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandler.java @@ -24,7 +24,10 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; +import org.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; import org.wso2.carbon.identity.application.common.model.ServiceProvider; import org.wso2.carbon.identity.application.common.model.ServiceProviderProperty; @@ -33,6 +36,7 @@ import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.oauth.OAuthUtil; import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth.common.exception.InvalidOAuthClientException; import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; @@ -47,6 +51,7 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2IntrospectionResponseDTO; import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO; import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; @@ -62,6 +67,7 @@ import static org.wso2.carbon.identity.application.mgt.ApplicationConstants.IS_FRAGMENT_APP; import static org.wso2.carbon.identity.oauth.common.OAuthConstants.IMPERSONATING_ACTOR; import static org.wso2.carbon.identity.oauth2.util.OAuth2Util.isParsableJWT; +import static org.wso2.carbon.utils.multitenancy.MultitenantConstants.INVALID_TENANT_ID; /** * Handles the token validation by invoking the proper validation handler by looking at the token @@ -678,8 +684,10 @@ private OAuth2IntrospectionResponseDTO validateAccessToken(OAuth2TokenValidation // Validate SSO session bound token. if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(bindingType)) { - if (!OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO)) { + if (!isTokenBoundToActiveSSOSession(accessTokenDO)) { log.debug("Token is not bound to an active SSO session."); + // Revoke the SSO session bound access token if the session is invalid/terminated. + revokeSSOSessionBoundToken(accessTokenDO); introResp.setActive(false); return introResp; } @@ -1047,4 +1055,78 @@ private void validateIntrospectionForSubOrgTokens(String tenantDomain, AccessTok } } + /** + * Check whether the SSO-session-bound access token is still tied to an active SSO session. + * + * @param accessTokenDO Access token data object. + * @return True if the token is bound to an active SSO session, false otherwise. + */ + private boolean isTokenBoundToActiveSSOSession(AccessTokenDO accessTokenDO) { + + if (StringUtils.isBlank(accessTokenDO.getTokenBinding().getBindingValue())) { + log.debug("No token binding value is found for SSO session bound token."); + return false; + } + + String sessionIdentifier = accessTokenDO.getTokenBinding().getBindingValue(); + String tenantDomain = accessTokenDO.getAuthzUser().getTenantDomain(); + SessionContext sessionContext = FrameworkUtils.getSessionContextFromCache(sessionIdentifier, tenantDomain); + if (sessionContext == null) { + if (log.isDebugEnabled()) { + log.debug("Session context is not found corresponding to the session identifier: " + + sessionIdentifier); + } + return false; + } + + if (log.isDebugEnabled()) { + log.debug("SSO session validation successful for the given session identifier: " + sessionIdentifier); + } + return true; + } + + /** + * Revoke the SSO session bound access token if the associated session is terminated. + * This is only applicable for the applications that has enabled 'revokeTokensWhenIdPSessionTerminated'. + * + * @param accessTokenDO Access token data object. + */ + private void revokeSSOSessionBoundToken(AccessTokenDO accessTokenDO) { + + String consumerKey = accessTokenDO.getConsumerKey(); + try { + OAuthAppDO appDO = OAuth2Util.getAppInformationByClientId(consumerKey, + getAppResidentTenantDomain(accessTokenDO)); + + if (appDO != null && appDO.isTokenRevocationWithIDPSessionTerminationEnabled()) { + OAuthUtil.clearOAuthCache(accessTokenDO); + OAuthRevocationRequestDTO revokeRequestDTO = new OAuthRevocationRequestDTO(); + revokeRequestDTO.setConsumerKey(consumerKey); + revokeRequestDTO.setToken(accessTokenDO.getAccessToken()); + OAuth2ServiceComponentHolder.getInstance().getRevocationProcessor() + .revokeAccessToken(revokeRequestDTO, accessTokenDO); + } + } catch (InvalidOAuthClientException | IdentityOAuth2Exception | UserIdNotFoundException e) { + log.error("Error while revoking SSO session bound access token.", e); + } + } + + /** + * Get the resident tenant domain of the application associated with the access token. + * + * @param accessTokenDO Access token data object. + * @return Resident tenant domain of the application. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the tenant domain. + */ + private static String getAppResidentTenantDomain(AccessTokenDO accessTokenDO) throws IdentityOAuth2Exception { + + String appResidentTenantDomain = null; + if (accessTokenDO.getAppResidentTenantId() != INVALID_TENANT_ID) { + appResidentTenantDomain = OAuth2Util.getTenantDomain(accessTokenDO.getAppResidentTenantId()); + } + if (StringUtils.isBlank(appResidentTenantDomain)) { + appResidentTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + } + return appResidentTenantDomain; + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java index 7fe6829c19e..eb59882f217 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java @@ -29,10 +29,12 @@ import org.wso2.carbon.identity.common.testng.WithCarbonHome; import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; +import org.wso2.carbon.identity.oauth.OAuthUtil; import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; import org.wso2.carbon.identity.oauth.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultOAuth2RevocationProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultRefreshTokenGrantProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.RefreshTokenGrantProcessor; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; @@ -41,6 +43,7 @@ import org.wso2.carbon.identity.oauth2.dao.TokenBindingMgtDAO; 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.rar.AuthorizationDetailsService; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; @@ -63,6 +66,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_CHECKING_ACCOUNT_LOCK_STATUS; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_GETTING_USERNAME_ASSOCIATED_WITH_IDP; @@ -226,10 +232,20 @@ public void testValidateGrantWhenUserIsLockedInUserStoreEnd(AuthenticatedUser us } } - @Test(expectedExceptions = IdentityOAuth2Exception.class, + @DataProvider(name = "ssoSessionInactiveDataProvider") + public Object[][] ssoSessionInactiveDataProvider() { + + return new Object[][]{ + {true}, + {false} + }; + } + + @Test(dataProvider = "ssoSessionInactiveDataProvider", description = "Ensure the refresh grant flow fails for an SSO session-bound token if the corresponding " + "session has expired, even with a valid refresh token.") - public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws Exception { + public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession(boolean isTokenRevocationEnabled) + throws Exception { when(refreshTokenGrantProcessor.validateRefreshToken(any())).thenReturn(refreshTokenValidationDataDO); when(refreshTokenValidationDataDO.getAuthorizedUser()).thenReturn(new MockAuthenticatedUser("test_user")); @@ -240,12 +256,14 @@ public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws when(oAuth2AccessTokenReqDTO.getClientId()).thenReturn("test_client_id"); when(refreshTokenValidationDataDO.getTokenBindingReference()).thenReturn("sso_binding_ref"); when(refreshTokenValidationDataDO.getTokenId()).thenReturn("sso_token_id"); + when(refreshTokenValidationDataDO.getAccessToken()).thenReturn("test_access_token"); when(refreshTokenValidationDataDO.getRefreshTokenState()) .thenReturn(OAuthConstants.TokenStates.TOKEN_STATE_ACTIVE); TokenBinding tokenBinding = new TokenBinding(); tokenBinding.setBindingReference("sso_binding_ref"); tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + tokenBinding.setBindingValue("sso_binding_value"); TokenBindingMgtDAO tokenBindingMgtDAO = mock(TokenBindingMgtDAO.class); when(tokenBindingMgtDAO.getTokenBindingByBindingRef(anyString(), anyString())) @@ -256,6 +274,9 @@ public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws when(oAuthAppDO.getTokenBindingType()) .thenReturn(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); + when(oAuthAppDO.isTokenRevocationWithIDPSessionTerminationEnabled()).thenReturn(isTokenRevocationEnabled); + + DefaultOAuth2RevocationProcessor revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); try (MockedStatic oAuthTokenPersistenceFactoryMockedStatic = mockStatic(OAuthTokenPersistenceFactory.class); @@ -263,7 +284,9 @@ public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws MockedStatic oAuthServerConfigurationMockedStatic = mockStatic(OAuthServerConfiguration.class); MockedStatic oAuth2ServiceComponentHolderMockedStatic = - mockStatic(OAuth2ServiceComponentHolder.class)) { + mockStatic(OAuth2ServiceComponentHolder.class); + MockedStatic frameworkUtilsMockedStatic = mockStatic(FrameworkUtils.class); + MockedStatic oAuthUtilMockedStatic = mockStatic(OAuthUtil.class)) { oAuthTokenPersistenceFactoryMockedStatic.when(OAuthTokenPersistenceFactory::getInstance) .thenReturn(oAuthTokenPersistenceFactory); @@ -271,8 +294,6 @@ public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws oAuth2UtilMockedStatic.when(() -> OAuth2Util.getTenantId(anyString())).thenReturn(TENANT_ID); oAuth2UtilMockedStatic.when(() -> OAuth2Util.getAppInformationByClientId(anyString())) .thenReturn(oAuthAppDO); - oAuth2UtilMockedStatic.when(() -> OAuth2Util - .isTokenBoundToActiveSSOSession(any(), anyString(), any(), any())).thenReturn(false); oAuthServerConfigurationMockedStatic.when(OAuthServerConfiguration::getInstance) .thenReturn(oAuthServerConfiguration); @@ -280,9 +301,30 @@ public void testValidateGrantForSSOSessionBoundTokenWithInactiveSession() throws oAuth2ServiceComponentHolderMockedStatic.when(OAuth2ServiceComponentHolder::getInstance) .thenReturn(oAuth2ServiceComponentHolder); + frameworkUtilsMockedStatic.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(null); + if (isTokenRevocationEnabled) { + AccessTokenDO accessTokenDO = mock(AccessTokenDO.class); + oAuth2UtilMockedStatic + .when(() -> OAuth2Util.getAccessTokenDOFromTokenIdentifier(anyString(), eq(true))) + .thenReturn(accessTokenDO); + when(oAuth2ServiceComponentHolder.getRevocationProcessor()).thenReturn(revocationProcessor); + } + RefreshGrantHandler refreshGrantHandler = new RefreshGrantHandler(); refreshGrantHandler.init(); - refreshGrantHandler.validateGrant(oAuthTokenReqMessageContext); + + try { + refreshGrantHandler.validateGrant(oAuthTokenReqMessageContext); + fail("Expected exception was not thrown."); + } catch (IdentityOAuth2Exception e) { + if (isTokenRevocationEnabled) { + verify(revocationProcessor, times(1)).revokeAccessToken( + any(), any(AccessTokenDO.class)); + } else { + verify(revocationProcessor, never()).revokeAccessToken(any(), any()); + } + } } } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java index 84dbddecd8e..ada67d9c4c8 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/util/OAuth2UtilTest.java @@ -48,7 +48,6 @@ import org.wso2.carbon.base.CarbonBaseConstants; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; @@ -89,7 +88,6 @@ import org.wso2.carbon.identity.oauth.dto.ScopeDTO; import org.wso2.carbon.identity.oauth.dto.TokenBindingMetaDataDTO; import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder; -import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultOAuth2RevocationProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.HashingPersistenceProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.TokenPersistenceProcessor; @@ -101,7 +99,6 @@ import org.wso2.carbon.identity.oauth2.dao.AccessTokenDAO; import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; -import org.wso2.carbon.identity.oauth2.dto.OAuthRevocationRequestDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.ClientAuthenticationMethodModel; @@ -109,7 +106,6 @@ import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; -import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding; import org.wso2.carbon.identity.oauth2.token.handlers.grant.AuthorizationGrantHandler; import org.wso2.carbon.identity.openidconnect.dao.ScopeClaimMappingDAO; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; @@ -3615,181 +3611,4 @@ public void testGetTokenBindingValue(HttpRequestHeader[] headers, String cookieN Optional tokenBindingValue = OAuth2Util.getTokenBindingValue(reqDTO, cookieName); assertEquals(tokenBindingValue, expectedValue); } - - @DataProvider(name = "boundAccessTokenDOProvider") - public Object[][] boundAccessTokenDOProvider() { - - SessionContext activeSession = new SessionContext(); - return new Object[][]{ - // accessTokenDO, sessionContext, expected result - {null, null, false}, - {createAccessTokenDO(false, false, false), null, false}, - {createAccessTokenDO(true, false, false), null, false}, - {createAccessTokenDO(true, true, false), null, false}, - // Session is not active. - {createAccessTokenDO(true, true, true), null, false}, - // Session is active. - {createAccessTokenDO(true, true, true), activeSession, true}, - }; - } - - @Test(dataProvider = "boundAccessTokenDOProvider") - public void testIsTokenBoundToActiveSSOSession_WithTokenDO(AccessTokenDO accessTokenDO, - SessionContext sessionContext, boolean expectedResult) { - - try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); - MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS)) { - frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) - .thenReturn(sessionContext); - - if (sessionContext == null) { - OAuthAppDO appDO = new OAuthAppDO(); - appDO.setTokenRevocationWithIDPSessionTerminationEnabled(false); - oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(SUPER_TENANT_DOMAIN_NAME); - oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())) - .thenReturn(appDO); - } - assertEquals(OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO), expectedResult); - } - } - - @Test - public void testIsTokenBoundToActiveSSOSession_WithTokenDO_TokenRevocationEnabled() throws Exception { - - try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); - MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS); - MockedStatic oAuthUtil = mockStatic(OAuthUtil.class); - MockedStatic oAuth2ServiceComponentHolder = - mockStatic(OAuth2ServiceComponentHolder.class)) { - - AccessTokenDO accessTokenDO = createAccessTokenDO(true, true, true); - frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) - .thenReturn(null); - - OAuthAppDO appDO = new OAuthAppDO(); - appDO.setTokenRevocationWithIDPSessionTerminationEnabled(true); - oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())).thenReturn(appDO); - oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(SUPER_TENANT_DOMAIN_NAME); - - OAuth2ServiceComponentHolder mockServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); - oAuth2ServiceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) - .thenReturn(mockServiceComponentHolder); - DefaultOAuth2RevocationProcessor revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); - when(mockServiceComponentHolder.getRevocationProcessor()).thenReturn(revocationProcessor); - - assertFalse(OAuth2Util.isTokenBoundToActiveSSOSession(accessTokenDO)); - verify(revocationProcessor, times(1)).revokeAccessToken( - any(OAuthRevocationRequestDTO.class), eq(accessTokenDO)); - } - } - - @DataProvider(name = "boundAccessTokenIdentifierProvider") - public Object[][] boundAccessTokenIdentifierProvider() { - - TokenBinding tokenBinding = new TokenBinding(); - tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); - tokenBinding.setBindingValue(TEST_BINDING_VALUE); - - TokenBinding wrongTypeBinding = new TokenBinding(); - wrongTypeBinding.setBindingType("WRONG_TYPE"); - wrongTypeBinding.setBindingValue(TEST_BINDING_VALUE); - - TokenBinding blankValueBinding = new TokenBinding(); - blankValueBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); - blankValueBinding.setBindingValue(""); - - OAuthAppDO appDO = new OAuthAppDO(); - appDO.setTokenRevocationWithIDPSessionTerminationEnabled(false); - AuthenticatedUser user = new AuthenticatedUser(); - user.setTenantDomain(SUPER_TENANT_DOMAIN_NAME); - String accessTokenIdentifier = TEST_TOKEN_IDENTIFIER; - SessionContext activeSession = new SessionContext(); - - return new Object[][]{ - // tokenBinding, accessTokenIdentifier, appDO, user, sessionContext, expectedResult - {null, null, null, null, null, false}, - {tokenBinding, accessTokenIdentifier, appDO, user, activeSession, true}, - {tokenBinding, accessTokenIdentifier, appDO, user, null, false}, - {wrongTypeBinding, accessTokenIdentifier, appDO, user, null, false}, - {blankValueBinding, accessTokenIdentifier, appDO, user, null, false} - }; - } - - @Test(dataProvider = "boundAccessTokenIdentifierProvider") - public void testIsTokenBoundToActiveSSOSession_WithTokenIdentifier(TokenBinding tokenBinding, - String accessTokenIdentifier, - OAuthAppDO appDO, - AuthenticatedUser user, - SessionContext sessionContext, - boolean expectedResult) { - - try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class)) { - frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) - .thenReturn(sessionContext); - assertEquals(OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, accessTokenIdentifier, appDO, user), - expectedResult); - } - } - - @Test - public void testIsTokenBoundToActiveSSOSession_WithTokenIdentifier_TokenRevocationEnabled() throws Exception { - - try (MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); - MockedStatic oAuth2Util = mockStatic(OAuth2Util.class, Mockito.CALLS_REAL_METHODS); - MockedStatic oAuthUtil = mockStatic(OAuthUtil.class); - MockedStatic oAuth2ServiceComponentHolder = - mockStatic(OAuth2ServiceComponentHolder.class)) { - - frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) - .thenReturn(null); - - TokenBinding tokenBinding = new TokenBinding(); - tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); - tokenBinding.setBindingValue(TEST_BINDING_VALUE); - - OAuthAppDO appDO = new OAuthAppDO(); - appDO.setTokenRevocationWithIDPSessionTerminationEnabled(true); - AuthenticatedUser user = new AuthenticatedUser(); - String accessTokenIdentifier = TEST_TOKEN_IDENTIFIER; - AccessTokenDO accessTokenDO = createAccessTokenDO(true, true, true); - - oAuth2Util.when(() -> OAuth2Util.getAccessTokenDOFromTokenIdentifier(accessTokenIdentifier, true)) - .thenReturn(accessTokenDO); - - OAuth2ServiceComponentHolder mockServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); - oAuth2ServiceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) - .thenReturn(mockServiceComponentHolder); - DefaultOAuth2RevocationProcessor revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); - when(mockServiceComponentHolder.getRevocationProcessor()).thenReturn(revocationProcessor); - - assertFalse(OAuth2Util.isTokenBoundToActiveSSOSession(tokenBinding, accessTokenIdentifier, appDO, user)); - verify(revocationProcessor, times(1)).revokeAccessToken( - any(OAuthRevocationRequestDTO.class), eq(accessTokenDO)); - } - } - - private AccessTokenDO createAccessTokenDO(boolean withUser, boolean withTokenBinding, boolean withSSOBinding) { - - AccessTokenDO accessTokenDO = new AccessTokenDO(); - accessTokenDO.setConsumerKey(clientId); - accessTokenDO.setAppResidentTenantId(SUPER_TENANT_ID); - if (withUser) { - AuthenticatedUser user = new AuthenticatedUser(); - user.setTenantDomain(SUPER_TENANT_DOMAIN_NAME); - user.setUserId(TEST_USER_ID); - accessTokenDO.setAuthzUser(user); - } - if (withTokenBinding) { - TokenBinding tokenBinding = new TokenBinding(); - if (withSSOBinding) { - tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER); - } else { - tokenBinding.setBindingType(OAuth2Constants.TokenBinderType.COOKIE_BASED_TOKEN_BINDER); - } - tokenBinding.setBindingValue(TEST_BINDING_VALUE); - tokenBinding.setBindingReference(TEST_BINDING_REFERENCE); - accessTokenDO.setTokenBinding(tokenBinding); - } - return accessTokenDO; - } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java index 4db9d6eff4d..4b0d4636c8e 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/validators/TokenValidationHandlerTest.java @@ -34,6 +34,7 @@ import org.testng.annotations.Test; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.context.SessionContext; import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.FederatedAuthenticatorConfig; @@ -47,10 +48,12 @@ import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.oauth.OAuthUtil; import org.wso2.carbon.identity.oauth.cache.AppInfoCache; import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder; +import org.wso2.carbon.identity.oauth.tokenprocessor.DefaultOAuth2RevocationProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.TokenProvider; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; @@ -94,9 +97,12 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -616,18 +622,23 @@ public void testBuildClientAppDTOWithInvalidAccessToken() throws Exception { } } - @DataProvider(name = "ssoSessionBoundTokenStatusDataProvider") - public Object[][] ssoSessionBoundTokenStatusDataProvider() { + @DataProvider(name = "ssoSessionBoundTokenDataProvider") + public Object[][] ssoSessionBoundTokenDataProvider() { + SessionContext activeSession = new SessionContext(); return new Object[][]{ - {true, true}, - {false, false} + //isSessionActive, isTokenRevocationEnabled, expectedActiveState + {true, false, true}, + {true, true, true}, + {false, false, false}, + {false, true, false} }; } - @Test(dataProvider = "ssoSessionBoundTokenStatusDataProvider") - public void testBuildIntrospectionResponseForSSOSessionBoundAccessToken(boolean isSessionValid, - boolean expectedActiveStatus) + @Test(dataProvider = "ssoSessionBoundTokenDataProvider") + public void testBuildIntrospectionResponseForSSOSessionBoundToken(boolean isSessionActive, + boolean isTokenRevocationEnabled, + boolean expectedActiveState) throws Exception { try (MockedStatic oAuthServerConfiguration = mockStatic( @@ -638,10 +649,18 @@ public void testBuildIntrospectionResponseForSSOSessionBoundAccessToken(boolean MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); MockedStatic identityUtil = mockStatic(IdentityUtil.class); MockedStatic organizationManagementUtil = - mockStatic(OrganizationManagementUtil.class)) { + mockStatic(OrganizationManagementUtil.class); + MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); + MockedStatic oAuthUtil = mockStatic(OAuthUtil.class)) { + + SessionContext sessionContext = isSessionActive ? new SessionContext() : null; + frameworkUtils.when(() -> FrameworkUtils.getSessionContextFromCache(anyString(), anyString())) + .thenReturn(sessionContext); + + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setTokenRevocationWithIDPSessionTerminationEnabled(isTokenRevocationEnabled); + oAuth2Util.when(() -> OAuth2Util.getAppInformationByClientId(anyString(), anyString())).thenReturn(appDO); - oAuth2Util.when(() -> OAuth2Util.isTokenBoundToActiveSSOSession(any(AccessTokenDO.class))) - .thenReturn(isSessionValid); organizationManagementUtil.when(() -> OrganizationManagementUtil.isOrganization(anyString())) .thenReturn(false); OAuth2ServiceComponentHolder.setIDPIdColumnEnabled(true); @@ -699,21 +718,21 @@ public void testBuildIntrospectionResponseForSSOSessionBoundAccessToken(boolean // As the token is dummy, no point in getting actual tenant details. oAuth2Util.when(() -> OAuth2Util.getTenantDomain(anyInt())).thenReturn(StringUtils.EMPTY); + DefaultOAuth2RevocationProcessor revocationProcessor = null; + if (!isSessionActive && isTokenRevocationEnabled) { + revocationProcessor = mock(DefaultOAuth2RevocationProcessor.class); + when(oAuth2ServiceComponentHolderInstance.getRevocationProcessor()).thenReturn(revocationProcessor); + } OAuth2IntrospectionResponseDTO introspectionResponse = tokenValidationHandler .buildIntrospectionResponse(validationRequest); assertNotNull(introspectionResponse, "Introspection response should not be null"); - if (expectedActiveStatus) { - assertTrue(introspectionResponse.isActive(), - "Token should be active when SSO session is valid"); - assertEquals(introspectionResponse.getBindingType(), - OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER, - "Binding type should match SSO session binder"); - assertEquals(introspectionResponse.getBindingReference(), SSO_SESSION_BINDING_REFERENCE, - "Binding reference should be preserved"); - } else { - assertFalse(introspectionResponse.isActive(), - "Token should be inactive when SSO session has timed out"); + assertEquals(introspectionResponse.isActive(), expectedActiveState); + + if (!isSessionActive && isTokenRevocationEnabled) { + oAuthUtil.verify(() -> OAuthUtil.clearOAuthCache(accessTokenDO)); + verify(revocationProcessor, times(1)).revokeAccessToken( + any(), eq(accessTokenDO)); } } }