From 8c3f7cc6e92ca8327cc6ab722b4e8565ca727dbd Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Mon, 22 Apr 2024 16:43:44 +0200 Subject: [PATCH] Ignore include in token scope for refresh token Closes #12326 Signed-off-by: Giuseppe Graziano --- .../keycloak/models/ClientSessionContext.java | 2 + .../keycloak/protocol/oidc/TokenManager.java | 1 + .../util/DefaultClientSessionContext.java | 15 ++++-- .../testsuite/oauth/RefreshTokenTest.java | 49 +++++++++++++++++-- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java b/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java index 44ee5765482..e613ebcbcb6 100644 --- a/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientSessionContext.java @@ -54,6 +54,8 @@ public interface ClientSessionContext { String getScopeString(); + String getScopeString(boolean ignoreIncludeInTokenScope); + void setAttribute(String name, Object value); T getAttribute(String attribute, Class clazz); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index a67ce295858..1a4089f36f2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -1094,6 +1094,7 @@ public AccessTokenResponseBuilder generateRefreshToken() { ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(realm, OAuth2Constants.OFFLINE_ACCESS); boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId()); generateRefreshToken(offlineTokenRequested); + refreshToken.setScope(clientSessionCtx.getScopeString(true)); return this; } diff --git a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java index afc05fd1145..1357238d46b 100644 --- a/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java +++ b/services/src/main/java/org/keycloak/services/util/DefaultClientSessionContext.java @@ -166,8 +166,13 @@ private Set getUserRoles() { @Override public String getScopeString() { + return getScopeString(false); + } + + @Override + public String getScopeString(boolean ignoreIncludeInTokenScope) { if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) { - String scopeParam = buildScopesStringFromAuthorizationRequest(); + String scopeParam = buildScopesStringFromAuthorizationRequest(ignoreIncludeInTokenScope); logger.tracef("Generated scope param with Dynamic Scopes enabled: %1s", scopeParam); String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE); if (TokenUtil.isOIDCRequest(scopeSent)) { @@ -178,7 +183,7 @@ public String getScopeString() { // Add both default and optional scopes to scope parameter. Don't add client itself String scopeParam = getClientScopesStream() .filter(((Predicate) ClientModel.class::isInstance).negate()) - .filter(ClientScopeModel::isIncludeInTokenScope) + .filter(scope-> scope.isIncludeInTokenScope() || ignoreIncludeInTokenScope) .map(ClientScopeModel::getName) .collect(Collectors.joining(" ")); @@ -196,12 +201,14 @@ public String getScopeString() { * they should be included in tokens or not. * Then return the scope name from the data stored in the RAR object representation. * + * @param ignoreIncludeInTokenScope ignore include in token scope from client scope options + * * @return see description */ - private String buildScopesStringFromAuthorizationRequest() { + private String buildScopesStringFromAuthorizationRequest(boolean ignoreIncludeInTokenScope) { return AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, clientSession.getNote(OAuth2Constants.SCOPE)).getAuthorizationDetailEntries().stream() .filter(authorizationDetails -> authorizationDetails.getSource().equals(AuthorizationRequestSource.SCOPE)) - .filter(authorizationDetails -> authorizationDetails.getClientScope().isIncludeInTokenScope()) + .filter(authorizationDetails -> authorizationDetails.getClientScope().isIncludeInTokenScope() || ignoreIncludeInTokenScope) .filter(authorizationDetails -> isClientScopePermittedForUser(authorizationDetails.getClientScope())) .map(authorizationDetails -> authorizationDetails.getAuthorizationDetails().getScopeNameFromCustomData()) .collect(Collectors.joining(" ")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 10b1863c22d..e25474d9656 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -50,6 +50,7 @@ import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; @@ -537,7 +538,7 @@ public void refreshTokenReuseTokenWithoutRefreshTokensRevokedWithLessScopes() th oauth.scope(optionalScope); OAuthClient.AccessTokenResponse response1 = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken()); - AbstractOIDCScopeTest.assertScopes("openid email phone address profile", refreshToken1.getScope()); + AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile address phone", refreshToken1.getScope()); setTimeOffset(2); @@ -548,7 +549,7 @@ public void refreshTokenReuseTokenWithoutRefreshTokensRevokedWithLessScopes() th AbstractOIDCScopeTest.assertScopes("openid email phone profile", response2.getScope()); RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken()); assertNotNull(refreshToken2); - AbstractOIDCScopeTest.assertScopes("openid email phone address profile", refreshToken2.getScope()); + AbstractOIDCScopeTest.assertScopes("openid acr roles phone address email profile basic web-origins", refreshToken2.getScope()); } finally { setTimeOffset(0); @@ -566,7 +567,7 @@ public void refreshTokenReuseTokenScopeParameterNotInRefreshToken() throws Excep OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password"); RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken()); - AbstractOIDCScopeTest.assertScopes("openid email profile", refreshToken1.getScope()); + AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken1.getScope()); setTimeOffset(2); @@ -582,6 +583,48 @@ public void refreshTokenReuseTokenScopeParameterNotInRefreshToken() throws Excep } } + @Test + public void refreshWithOptionalClientScopeWithIncludeInTokenScopeDisabled() throws Exception { + //set roles client scope as optional + ClientScopeRepresentation rolesScope = ApiUtil.findClientScopeByName(adminClient.realm("test"), OIDCLoginProtocolFactory.ROLES_SCOPE).toRepresentation(); + ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).removeClientScope(rolesScope.getId(),true); + ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(rolesScope.getId(),false); + + try { + oauth.scope("roles"); + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken()); + + AbstractOIDCScopeTest.assertScopes("openid email profile", accessToken.getScope()); + AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken.getScope()); + + Assert.assertNotNull(accessToken.getRealmAccess()); + Assert.assertNotNull(accessToken.getResourceAccess()); + + oauth.scope(null); + + response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password"); + + accessToken = oauth.verifyToken(response.getAccessToken()); + refreshToken = oauth.parseRefreshToken(response.getRefreshToken()); + + AbstractOIDCScopeTest.assertScopes("openid email profile", accessToken.getScope()); + AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken.getScope()); + + Assert.assertNotNull(accessToken.getRealmAccess()); + Assert.assertNotNull(accessToken.getResourceAccess()); + + } finally { + ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).removeClientScope(rolesScope.getId(),false); + ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(rolesScope.getId(),true); + } + } + @Test public void refreshTokenReuseTokenWithRefreshTokensRevoked() throws Exception { try {