-
Notifications
You must be signed in to change notification settings - Fork 399
Revoke access tokens that are bound to an SSO session when that session is invalid. #2943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
116b20b
9e9220d
c91255b
cd88482
c25aeb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -56,10 +58,12 @@ | |
| 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; | ||
| 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; | ||
|
|
@@ -118,7 +122,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<TokenBinding> 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()) { | ||
|
|
@@ -277,14 +294,6 @@ private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqM | |
| tokReqMsgCtx.getOauth2AccessTokenReqDTO().setAccessTokenExtendedAttributes( | ||
| validationBean.getAccessTokenExtendedAttributes()); | ||
| propagateImpersonationInfo(tokReqMsgCtx); | ||
| if (StringUtils.isNotBlank(validationBean.getTokenBindingReference()) && !NONE | ||
| .equals(validationBean.getTokenBindingReference())) { | ||
| Optional<TokenBinding> 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); | ||
|
|
@@ -978,11 +987,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; | ||
| } | ||
|
|
||
|
|
@@ -998,6 +1007,17 @@ private void validateTokenBindingReference(OAuth2AccessTokenReqDTO tokenReqDTO, | |
| return; | ||
| } | ||
|
|
||
| // Validate SSO session bound token. | ||
| if (OAuth2Constants.TokenBinderType.SSO_SESSION_BASED_TOKEN_BINDER.equals(oAuthAppDO.getTokenBindingType())) { | ||
| if (!isTokenBoundToActiveSSOSession(tokenBinding.getBindingValue(), | ||
| validationDataDO.getAuthorizedUser())) { | ||
| // 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 " + | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a behavioural change. |
||
| "active SSO session."); | ||
| } | ||
| } | ||
|
|
||
| Optional<TokenBinder> tokenBinderOptional = OAuth2ServiceComponentHolder.getInstance() | ||
| .getTokenBinder(oAuthAppDO.getTokenBindingType()); | ||
| if (!tokenBinderOptional.isPresent()) { | ||
|
|
@@ -1049,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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,6 +141,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 +175,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 +211,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; | ||
|
|
@@ -6322,4 +6326,46 @@ 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<String> getTokenBindingValue(OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be a public util method or should go under session binder? Are we using this method in different places? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is utilized in |
||
| String cookieName) { | ||
AfraHussaindeen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| HttpRequestHeader[] httpRequestHeaders = oAuth2AccessTokenReqDTO.getHttpRequestHeaders(); | ||
| if (ArrayUtils.isEmpty(httpRequestHeaders)) { | ||
AfraHussaindeen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return Optional.empty(); | ||
| } | ||
|
|
||
| for (HttpRequestHeader httpRequestHeader : httpRequestHeaders) { | ||
| if (HttpHeaders.COOKIE.equalsIgnoreCase(httpRequestHeader.getName())) { | ||
| if (ArrayUtils.isEmpty(httpRequestHeader.getValue())) { | ||
AfraHussaindeen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 { | ||
AfraHussaindeen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return Optional.of(cookieValue); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
AfraHussaindeen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return Optional.empty(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this check should comes as last logic, after checking for isPresent and isValidTokenBinding method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intentionally placed the code block earlier because, within the isValidTokenBinding method [1], we cannot perform token revocation—at that point we don’t have access to the access token details.
By executing the session check before the token binding validation, we ensure that the session validity is verified regardless of whether the validateTokenBinding configuration is enabled. As a result, the subsequent binding reference validation will only be executed if the session is confirmed to be valid.
If we were to do it the other way around—relying solely on isValidTokenBinding [1]—then the session context would be checked there. If invalid, it would simply return false. At that point, before throwing the exception, we would still need to perform token revocation. But this would mean re-checking the session context, because we cannot conclusively say that the false result was due to session invalidation—it might have been triggered by some other reason.
That’s why I decided to proceed with the current approach. Please correct me if my understanding is off.
[1] https://github.com/AfraHussaindeen/identity-inbound-auth-oauth/blob/e15d74de0792f246d69dfce9fe6e65625b4695b6/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/bindings/impl/SSOSessionBasedTokenBinder.java#L143