Skip to content

Commit

Permalink
Ignore include in token scope for refresh token
Browse files Browse the repository at this point in the history
Closes #12326

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
  • Loading branch information
graziang authored and mposolda committed May 3, 2024
1 parent 5e00fe8 commit 8c3f7cc
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 7 deletions.
Expand Up @@ -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);
Expand Down
Expand Up @@ -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;
}

Expand Down
Expand Up @@ -166,8 +166,13 @@ private Set<RoleModel> 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)) {
Expand All @@ -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<ClientScopeModel>) ClientModel.class::isInstance).negate())
.filter(ClientScopeModel::isIncludeInTokenScope)
.filter(scope-> scope.isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.map(ClientScopeModel::getName)
.collect(Collectors.joining(" "));

Expand All @@ -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(" "));
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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 {
Expand Down

0 comments on commit 8c3f7cc

Please sign in to comment.