8000 Fix admin roles assignments by protocol mappers for DCR (#957) by pskopek · Pull Request #51499 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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 @@ -39,8 +39,13 @@
*/
public class AccessToken extends IDToken {

// Access token claims
public static final String REALM_ACCESS = "realm_access";
public static final String RESOURCE_ACCESS = "resource_access";
public static final String ROLES = "roles";

public static class Access implements Serializable {
@JsonProperty("roles")
@JsonProperty(ROLES)
protected Set<String> roles;
@JsonProperty("verify_caller")
protected Boolean verifyCaller;
Expand Down Expand Up @@ -147,10 +152,10 @@ public void setJktType(String jktType) {
@JsonProperty("allowed-origins")
protected Set<String> allowedOrigins;

@JsonProperty("realm_access")
@JsonProperty(REALM_ACCESS)
protected Access realmAccess;

@JsonProperty("resource_access")
@JsonProperty(RESOURCE_ACCESS)
protected Map<String, Access> resourceAccess;

@JsonProperty("authorization")
Expand Down
62 changes: 62 additions & 0 deletions core/src/main/java/org/keycloak/util/TokenUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import java.io.IOException;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;

import org.keycloak.OAuth2Constants;
import org.keycloak.jose.jwe.JWE;
Expand All @@ -31,9 +33,15 @@
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.json.KeycloakJsonMapperFactory;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;

import org.jboss.logging.Logger;

import static org.keycloak.representations.AccessToken.REALM_ACCESS;
import static org.keycloak.representations.AccessToken.RESOURCE_ACCESS;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
Expand Down Expand Up @@ -67,6 +75,8 @@ public class TokenUtil {

public static final String TOKEN_BACKCHANNEL_LOGOUT_EVENT_REVOKE_OFFLINE_TOKENS = "revoke_offline_access";

private static final Logger logger = Logger.getLogger(TokenUtil.class);

public static String attachOIDCScope(String scopeParam) {
if (scopeParam == null || scopeParam.isEmpty()) {
return OAuth2Constants.SCOPE_OPENID;
Expand Down Expand Up @@ -244,4 +254,56 @@ public static byte[] jweDirectVerifyAndDecode(Key aesKey, Key hmacKey, String jw
return jwe.getContent();

}

/**
* If token contains claims like "realm_access" or "resource_access" inside the "otherClaims" map, then those would be merged with the original fields "realm_access"
* and "resource_access", which are present directly on the token
*
* @param token access token
*/
public static void convertTokenRolesFromOtherClaims(AccessToken token) {
if (token.getOtherClaims() == null) return;

if (token.getOtherClaims().containsKey(REALM_ACCESS)) {
AccessToken.Access otherClaimsRealmAccess = convertToAccess(token.getOtherClaims().get(REALM_ACCESS), REALM_ACCESS);
if (otherClaimsRealmAccess != null) {
token.setRealmAccess(mergeAccess(token.getRealmAccess(), otherClaimsRealmAccess));
token.getOtherClaims().remove(REALM_ACCESS);
}
}

if (token.getOtherClaims().containsKey(RESOURCE_ACCESS)) {
token.setResourceAccess(new HashMap<>(token.getResourceAccess())); // Re-create as hashMap as it might be unmodifiable map

if (token.getOtherClaims().get(RESOURCE_ACCESS) instanceof Map) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) token.getOtherClaims().get(RESOURCE_ACCESS)).entrySet()) {
String clientId = entry.getKey();
AccessToken.Access otherClaimsClientAccess = convertToAccess(entry.getValue(), RESOURCE_ACCESS + "." + clientId);
AccessToken.Access mergedAccess = mergeAccess(token.getResourceAccess(clientId), otherClaimsClientAccess);
if (mergedAccess != null) {
token.getResourceAccess().put(clientId, mergedAccess);
}
}
token.getOtherClaims().remove(RESOURCE_ACCESS);
} else {
logger.warnf("Claim resource_access in the incorrect format in the access token of user %s. Ignoring", token.getPreferredUsername());
}
}
}

private static AccessToken.Access convertToAccess(Object accessClaim, String claimName) {
try {
return JsonSerialization.readValue(JsonSerialization.writeValueAsString(accessClaim), AccessToken.Access.class);
} catch (IOException ioe) {
logger.warnf( "Failed to convert roles from claim %s. Ignoring", claimName);
return null;
}
}

private static AccessToken.Access mergeAccess(AccessToken.Access access1, AccessToken.Access access2) {
if (access1 == null) return access2;
if (access2 == null) return access1;
access1.getRoles().addAll(access2.getRoles());
return access1;
Comment on lines +304 to +307
}
}
123 changes: 123 additions & 0 deletions core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,25 @@
package org.keycloak.jose;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil;

import org.junit.Assert;
import org.junit.Test;

import static org.keycloak.representations.AccessToken.REALM_ACCESS;
import static org.keycloak.representations.AccessToken.RESOURCE_ACCESS;
import static org.keycloak.representations.AccessToken.ROLES;

import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -117,4 +129,115 @@ public void isActiveShouldReturnFalseWhenWhenBeforeTimeInFutureOutsideTimeSkew()
assertFalse(jsonWebToken.isActive(allowedClockSkew));
}

@Test
public void testRealmAccessMerge() {
Set<String> role1Set = new HashSet<>(Collections.singleton("role1"));
Map<String, Object> map1 = new HashMap<>();
map1.put(ROLES, role1Set);

Set<String> role2Set = new HashSet<>(Collections.singleton("role2"));
Set<String> rolesSet = new HashSet<>(role1Set);
rolesSet.addAll(role2Set);

// No "otherClaims"
AccessToken at = new AccessToken();
at.setRealmAccess(new AccessToken.Access());
at.getRealmAccess().roles(role1Set);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(at.getRealmAccess().getRoles(), role1Set);
Assert.assertNull(at.getOtherClaims().get(REALM_ACCESS));

// Just "otherClaims"
at = new AccessToken();
at.getOtherClaims().put(REALM_ACCESS, map1);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(at.getRealmAccess().getRoles(), role1Set);
Assert.assertNull(at.getOtherClaims().get(REALM_ACCESS));

// Both
at = new AccessToken();
at.setRealmAccess(new AccessToken.Access());
at.getRealmAccess().roles(role2Set);
at.getOtherClaims().put(REALM_ACCESS, map1);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(at.getRealmAccess().getRoles(), rolesSet);
Assert.assertNull(at.getOtherClaims().get(REALM_ACCESS));

// Invalid stuff in "otherClaims"
at = new AccessToken();
at.setRealmAccess(new AccessToken.Access());
at.getRealmAccess().roles(role2Set);
at.getOtherClaims().put(REALM_ACCESS, role1Set);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(at.getRealmAccess().getRoles(), role2Set);
Assert.assertNotNull(at.getOtherClaims().get(REALM_ACCESS));
}

@Test
public void testResourceAccessMerge() throws IOException {
Set<String> role1Set = new HashSet<>(Collections.singleton("role1"));
Map<String, Object> map1 = new HashMap<>();
map1.put(ROLES, role1Set);

Set<String> role2Set = new HashSet<>(Collections.singleton("role2"));
Map<String, Object> map2 = new HashMap<>();
map2.put(ROLES, role2Set);

Set<String> rolesSet = new HashSet<>(role1Set);
rolesSet.addAll(role2Set);

AccessToken.Access access1 = new AccessToken.Access();
access1.roles(role1Set);
Map<String, AccessToken.Access> access1Map = new HashMap<>();
access1Map.put("client1", access1);

// No "otherClaims"
AccessToken at = new AccessToken();
at.setResourceAccess(access1Map);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(1, at.getResourceAccess().size());
Assert.assertEquals(role1Set, at.getResourceAccess().get("client1").getRoles());

// Just "otherClaims"
at = new AccessToken();
Map<?,?> map = JsonSerialization.readValue(JsonSerialization.writeValueAsString(access1Map), Map.class);
at.getOtherClaims().put(RESOURCE_ACCESS, map);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(1, at.getResourceAccess().size());
Assert.assertEquals(role1Set, at.getResourceAccess().get("client1").getRoles());

// Both - different clients
at = new AccessToken();
map = JsonSerialization.readValue(JsonSerialization.writeValueAsString(access1Map), Map.class);
at.getOtherClaims().put(RESOURCE_ACCESS, map);
Map<String, AccessToken.Access> accessMap = new HashMap<>();
accessMap.put("client2", access1);
at.setResourceAccess(accessMap);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(2, at.getResourceAccess().size());
Assert.assertEquals(role1Set, at.getResourceAccess().get("client1").getRoles());
Assert.assertEquals(role1Set, at.getResourceAccess().get("client2").getRoles());

// Both - same client
at = new AccessToken();
map = JsonSerialization.readValue(JsonSerialization.writeValueAsString(access1Map), Map.class);
at.getOtherClaims().put(RESOURCE_ACCESS, map);
accessMap = new HashMap<>();
AccessToken.Access access2 = new AccessToken.Access();
access2.roles(role2Set);
accessMap.put("client1", access2);
at.setResourceAccess(accessMap);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(1, at.getResourceAccess().size());
Assert.assertEquals(rolesSet, at.getResourceAccess().get("client1").getRoles());

// Invalid
at = new AccessToken();
at.setResourceAccess(access1Map);
at.getOtherClaims().put(RESOURCE_ACCESS, map1);
TokenUtil.convertTokenRolesFromOtherClaims(at);
Assert.assertEquals(1, at.getResourceAccess().size());
Assert.assertEquals(role1Set, at.getResourceAccess().get("client1").getRoles());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponseException;
Expand All @@ -53,8 +54,11 @@
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.UserSessionUtil;
import org.keycloak.util.TokenUtil;

import static org.keycloak.models.utils.KeycloakModelUtils.removeTransientAdminRoles;

/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
Expand Down Expand Up @@ -339,7 +343,24 @@ public ClientInitialAccessModel getInitialAccessModel() {

private boolean hasRole(String... roles) {
try {
AuthenticationManager.resolveLightweightAccessTokenRoles(session, jwt, session.getContext().getRealm());
boolean lightweight = AuthenticationManager.resolveLightweightAccessTokenRoles(session, jwt, session.getContext().getRealm());

if (!lightweight) {
// For lightweight access token, the roles are already considered just for those, which are present on UserModel
if (isBearerToken()) {
String clientId = getMgmtClientId();
AccessToken.Access mgmtClientAccess = jwt.getResourceAccess(clientId);
if (mgmtClientAccess != null) {
ClientModel client = realm.getClientByClientId(jwt.getIssuedFor());
if (client == null) return false;
UserSessionModel userSession = UserSessionUtil.findValidSessionForAccessToken(session,realm, jwt, client, (invalidUserSession -> {})).getUserSession();
if (userSession == null) return false;

removeTransientAdminRoles(realm, clientId, userSession.getUser(), mgmtClientAccess);
}
}
}

return hasRoleInToken(roles);
} catch (Throwable t) {
return false;
Expand All @@ -352,9 +373,7 @@ private boolean hasRoleInToken(String[] role) {
return false;
}

String clientId = realm.getName().equals(Config.getAdminRealm())
? realm.getMasterAdminClient().getClientId()
: Constants.REALM_MANAGEMENT_CLIENT_ID;
String clientId = getMgmtClientId();

Set<String> roles = Optional.ofNullable(resourceAccess.get(clientId))
.map(AccessToken.Access::getRoles)
Expand All @@ -363,6 +382,12 @@ private boolean hasRoleInToken(String[] role) {
return Arrays.stream(role).anyMatch(roles::contains);
}

private String getMgmtClientId() {
return realm.getName().equals(Config.getAdminRealm())
? realm.getMasterAdminClient().getClientId()
: Constants.REALM_MANAGEMENT_CLIENT_ID;
}

private boolean authenticatePublicClient(ClientModel client) {
if (client == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1669,11 +1669,19 @@ private static boolean isClientValid(UserSessionModel userSession, ClientModel c
return true;
}

public static void resolveLightweightAccessTokenRoles(KeycloakSession session, AccessToken accessToken, RealmModel realm) {
/**
* Resolve lightweight access token roles
*
* @param session session
* @param accessToken access token
* @param realm realm
* @return true if some roles were resolved. False if the token was not lightweight or there was some issue (EG. user session not found)
*/
public static boolean resolveLightweightAccessTokenRoles(KeycloakSession session, AccessToken accessToken, RealmModel realm) {
final String issuedFor = accessToken.getIssuedFor();
ClientModel client = realm.getClientByClientId(issuedFor);
if(client == null) {
return;
return false;
}

TokenContextEncoderProvider encoder = session.getProvider(TokenContextEncoderProvider.class);
Expand All @@ -1693,8 +1701,10 @@ public static void resolveLightweightAccessTokenRoles(KeycloakSession session, A
accessToken.subject(userSession.getUser().getId());
accessToken.setRealmAccess(realmAccess);
accessToken.setResourceAccess(clientAccess);
return true;
}
}
E34F return false;
}

public enum AuthenticationStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.keycloak.protocol.oidc.token.TokenPostProcessorContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Access;
import org.keycloak.util.TokenUtil;

import static org.keycloak.models.utils.KeycloakModelUtils.removeTransientAdminRoles;

Expand All @@ -37,6 +38,8 @@ public void process(TokenPostProcessorContext context) {
RealmModel realm = session.getContext().getRealm();
AccessToken accessToken = context.accessToken();

TokenUtil.convertTokenRolesFromOtherClaims(accessToken);

removeTransientAdminRoles(realm, null, user, accessToken.getRealmAccess());

Map<String, Access> resourceAccess = accessToken.getResourceAccess();
Expand Down
Loading
Loading
0