Ignore include in token scope for refresh token

Closes #12326

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-04-22 16:43:44 +02:00 committed by Marek Posolda
parent 5e00fe8b10
commit 8c3f7cc6e9
4 changed files with 60 additions and 7 deletions

View file

@ -54,6 +54,8 @@ public interface ClientSessionContext {
String getScopeString();
String getScopeString(boolean ignoreIncludeInTokenScope);
void setAttribute(String name, Object value);
<T> T getAttribute(String attribute, Class<T> clazz);

View file

@ -1094,6 +1094,7 @@ public class TokenManager {
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;
}

View file

@ -166,8 +166,13 @@ public class DefaultClientSessionContext implements ClientSessionContext {
@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 class DefaultClientSessionContext implements ClientSessionContext {
// Add both default and optional scopes to scope parameter. Don't add client itself
String scopeParam = getClientScopesStream()
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.filter(ClientScopeModel::isIncludeInTokenScope)
.filter(scope-> scope.isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.map(ClientScopeModel::getName)
.collect(Collectors.joining(" "));
@ -196,12 +201,14 @@ public class DefaultClientSessionContext implements ClientSessionContext {
* 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(" "));

View file

@ -50,6 +50,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
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 class RefreshTokenTest extends AbstractKeycloakTest {
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 class RefreshTokenTest extends AbstractKeycloakTest {
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 class RefreshTokenTest extends AbstractKeycloakTest {
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 class RefreshTokenTest extends AbstractKeycloakTest {
}
}
@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 {