Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String> tokenBindingValueOptional = OAuth2Util.getTokenBindingValue(oAuth2AccessTokenReqDTO,
cookieName);
if (tokenBindingValueOptional.isPresent()) {
String tokenBindingValue = tokenBindingValueOptional.get();
String receivedBindingReference = OAuth2Util.getTokenBindingReference(tokenBindingValue);
return bindingReference.equals(receivedBindingReference);
}

return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand All @@ -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())) {
Copy link
Contributor

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

Copy link
Contributor Author

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

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 " +
Copy link
Contributor

Choose a reason for hiding this comment

The 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()) {
Expand Down Expand Up @@ -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
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is utilized in SSOSessionBasedTokenBinder and AbstractTokenBinder

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();
}
}
Loading