Thanks to visit codestin.com
Credit goes to github.com

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
4 changes: 4 additions & 0 deletions docs/documentation/release_notes/topics/26_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,7 @@ For information on how to upgrade, see the link:{upgradingguide_link}[{upgrading
There are now generalized events for updating (`UPDATE_CREDENTIAL`) and removing (`REMOVE_CREDENTIAL`) a credential. The credential type is described in the `credential_type` attribute of the events. The new event types are supported by the Email Event Listener.

The following event types are now deprecated and will be removed in a future version: `UPDATE_PASSWORD`, `UPDATE_PASSWORD_ERROR`, `UPDATE_TOTP`, `UPDATE_TOTP_ERROR`, `REMOVE_TOTP`, `REMOVE_TOTP_ERROR`

= Lightweight access tokens for Admin REST API

Lightweight access tokens can now be used on the admin REST API. The `security-admin-console` and `admin-cli` clients are now using lightweight access tokens by default, so “Always Use Lightweight Access Token” and “Full Scope Allowed” are now enabled on these two clients. However, the behavior in the admin console should effectively remain the same. Be cautious if you have made changes to these two clients and if you are using them for other purposes.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import java.lang.invoke.MethodHandles;

import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionProvider;
Expand Down Expand Up @@ -55,6 +58,12 @@ public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepres
}

private void migrateRealm(KeycloakSession session, RealmModel realm) {
ClientModel adminConsoleClient = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
adminConsoleClient.setFullScopeAllowed(true);
adminConsoleClient.setAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, String.valueOf(true));
ClientModel adminCliClient = realm.getClientByClientId(Constants.ADMIN_CLI_CLIENT_ID);
adminCliClient.setFullScopeAllowed(true);
adminCliClient.setAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, String.valueOf(true));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public KeycloakIdentity(IDToken token, KeycloakSession keycloakSession, RealmMod
if (userSession == null) {
userSession = sessions.getOfflineUserSession(realm, token.getSessionState());
}

if (userSession == null) {
throw new RuntimeException("No active session associated with the token");
}
Expand Down Expand Up @@ -176,15 +176,22 @@ public KeycloakIdentity(IDToken token, KeycloakSession keycloakSession, RealmMod
}

public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession) {
this(accessToken, keycloakSession, keycloakSession.getContext().getRealm());
}

public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, RealmModel realm) {
if (accessToken == null) {
throw new ErrorResponseException("invalid_bearer_token", "Could not obtain bearer access_token from request.", Status.FORBIDDEN);
}
if (keycloakSession == null) {
throw new ErrorResponseException("no_keycloak_session", "No keycloak session", Status.FORBIDDEN);
}
if (realm == null) {
throw new ErrorResponseException("no_keycloak_session", "No realm set", Status.FORBIDDEN);
}
this.accessToken = accessToken;
this.keycloakSession = keycloakSession;
this.realm = keycloakSession.getContext().getRealm();
this.realm = realm;

Map<String, Collection<String>> attributes = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException;
Expand All @@ -43,11 +48,16 @@
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.util.TokenUtil;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import org.keycloak.utils.RoleResolveUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -283,34 +293,41 @@ public ClientInitialAccessModel getInitialAccessModel() {

private boolean hasRole(String... roles) {
try {
if (jwt.getIssuedFor().equals(Constants.ADMIN_CLI_CLIENT_ID)
|| jwt.getIssuedFor().equals(Constants.ADMIN_CONSOLE_CLIENT_ID)) {
return hasRoleInModel(roles);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to remove method hasRoleInModel now from this class?


} else {
return hasRoleInToken(roles);
//support for lightweight access token
if (jwt.getSubject() == null) {
String sid = (String) jwt.getOtherClaims().get("sid");
if (sid != null) {
final String issuedFor = jwt.getIssuedFor();
UserSessionProvider sessions = session.sessions();
UserSessionModel userSession = sessions.getUserSession(realm, sid);
if (userSession == null) {
userSession = sessions.getOfflineUserSession(realm, sid);
}

if (userSession != null) {
//get client session
ClientModel client = realm.getClientByClientId(issuedFor);
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());

//set realm roles
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, (String) jwt.getOtherClaims().get("scope"), session);
Map<String, AccessToken.Access> resourceAccess = RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx);

Map<String, Map<String, List<String>>> resourceAccessMap = new HashMap<>();
resourceAccess.forEach((key, access) ->
resourceAccessMap.put(key, Map.of("roles", new ArrayList<>(access.getRoles())))
);
jwt.setSubject(userSession.getUser().getId());
jwt.getOtherClaims().put("resource_access", resourceAccessMap);
}
}
}
} catch (Throwable t) {
return false;
}
}
return hasRoleInToken(roles);

private boolean hasRoleInModel(String[] roles) {
ClientModel roleNamespace;
UserModel user = session.users().getUserById(realm, jwt.getSubject());
if (user == null) {
} catch (Throwable t) {
return false;
}
if (realm.getName().equals(Config.getAdminRealm())) {
roleNamespace = realm.getMasterAdminClient();
} else {
roleNamespace = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
}
for (String role : roles) {
RoleModel roleModel = roleNamespace.getRole(role);
if (user.hasRole(roleModel)) return true;
}
return false;
}

private boolean hasRoleInToken(String[] role) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ protected void setupAdminConsole(RealmModel realm) {

adminConsole.setEnabled(true);
adminConsole.setAlwaysDisplayInConsole(false);
adminConsole.setFullScopeAllowed(true);
adminConsole.setPublicClient(true);
adminConsole.setFullScopeAllowed(false);
adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);

adminConsole.setAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD, "S256");
adminConsole.setAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, "true");
}

protected void setupAdminConsoleLocaleMapper(RealmModel realm) {
Expand All @@ -214,10 +215,11 @@ public void setupAdminCli(RealmModel realm) {
adminCli.setName("${client_" + Constants.ADMIN_CLI_CLIENT_ID + "}");
adminCli.setEnabled(true);
adminCli.setAlwaysDisplayInConsole(false);
adminCli.setFullScopeAllowed(false);
adminCli.setFullScopeAllowed(true);
adminCli.setStandardFlowEnabled(false);
adminCli.setDirectAccessGrantsEnabled(true);
adminCli.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
adminCli.setAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, "true");
}

}
Expand Down Expand Up @@ -644,7 +646,7 @@ protected void rollbackImpl() {
}

private String determineDefaultRoleName(RealmRepresentation rep) {
String defaultRoleName = Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + rep.getRealm().toLowerCase();
String defaultRoleName = Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + rep.getRealm().toLowerCase();
if (! hasRealmRole(rep, defaultRoleName)) {
return defaultRoleName;
} else {
Expand Down Expand Up @@ -778,7 +780,7 @@ public void setupClientServiceAccountsAndAuthorizationOnImport(RealmRepresentati
ClientModel clientModel = Optional.ofNullable(client.getId())
.map(realmModel::getClientById)
.orElseGet(() -> realmModel.getClientByClientId(client.getClientId()));

if (clientModel == null) {
throw new RuntimeException("Cannot find provided client by dir import.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,29 @@
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.common.Profile;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminAuth;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import jakarta.ws.rs.ForbiddenException;
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.utils.RoleResolveUtil;

/**
* @author <a href="mailto:[email protected]">Bill Burke</a>
Expand Down Expand Up @@ -98,17 +106,30 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage

private void initIdentity(KeycloakSession session, AdminAuth auth) {
final String issuedFor = auth.getToken().getIssuedFor();
AccessToken accessToken = auth.getToken();
//support for lightweight access token
if (auth.getToken().getSubject() == null) {
//get user session
UserSessionProvider sessions = session.sessions();
UserSessionModel userSession = sessions.getUserSession(adminsRealm, auth.getToken().getSessionId());
if (userSession == null) {
userSession = sessions.getOfflineUserSession(adminsRealm, auth.getToken().getSessionId());
}

if (Constants.ADMIN_CLI_CLIENT_ID.equals(issuedFor) || Constants.ADMIN_CONSOLE_CLIENT_ID.equals(issuedFor)) {
this.identity = new UserModelIdentity(auth.getRealm(), auth.getUser());
} else {
ClientModel client = session.clients().getClientByClientId(auth.getRealm(), issuedFor);
if (client != null && Boolean.parseBoolean(client.getAttribute(Constants.SECURITY_ADMIN_CONSOLE_ATTR))) {
this.identity = new UserModelIdentity(auth.getRealm(), auth.getUser());
} else {
this.identity = new KeycloakIdentity(auth.getToken(), session);
if (userSession != null) {
//get client session
ClientModel client = adminsRealm.getClientByClientId(issuedFor);
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());

//set realm roles
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, auth.getToken().getScope(), session);
AccessToken.Access realmAccess = RoleResolveUtil.getResolvedRealmRoles(session, clientSessionCtx, false);
Map<String, AccessToken.Access> clientAccess = RoleResolveUtil.getAllResolvedClientRoles(session, clientSessionCtx);
accessToken.setRealmAccess(realmAccess);
accessToken.setResourceAccess(clientAccess);
}
}
this.identity = new KeycloakIdentity(accessToken, session, adminsRealm);
}

MgmtPermissions(KeycloakSession session, RealmModel adminsRealm, UserModel admin) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,10 @@ protected void testMigrationTo26_0_0(boolean testIdentityProviderConfigMigration
if (testIdentityProviderConfigMigration) {
testIdentityProviderConfigMigration(migrationRealm2);
}
testLightweightClientAndFullScopeAllowed(masterRealm, Constants.ADMIN_CONSOLE_CLIENT_ID);
testLightweightClientAndFullScopeAllowed(masterRealm, Constants.ADMIN_CLI_CLIENT_ID);
testLightweightClientAndFullScopeAllowed(migrationRealm, Constants.ADMIN_CONSOLE_CLIENT_ID);
testLightweightClientAndFullScopeAllowed(migrationRealm, Constants.ADMIN_CLI_CLIENT_ID);
}

private void testClientContainsExpectedClientScopes() {
Expand Down Expand Up @@ -1351,4 +1355,10 @@ private void testIdentityProviderConfigMigration(final RealmResource realm) {
assertThat(rep.isHideOnLogin(), is(true));
assertThat(rep.getConfig().containsKey(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR), is(false));
}

private void testLightweightClientAndFullScopeAllowed(RealmResource realm, String clientId) {
ClientRepresentation clientRepresentation = realm.clients().findByClientId(clientId).get(0);
assertTrue(clientRepresentation.isFullScopeAllowed());
assertTrue(Boolean.parseBoolean(clientRepresentation.getAttributes().get(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@

import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.RealmResource;
Expand Down Expand Up @@ -486,6 +491,36 @@ public void clientCredentialWithoutBasicClaims() throws Exception {
}
}

@Test
public void testAdminConsoleClientWithLightweightAccessToken() {

oauth.realm("master");
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
oauth.redirectUri(OAuthClient.SERVER_ROOT + "/auth/admin/master/console");
PkceGenerator pkce = new PkceGenerator();
oauth.codeChallenge(pkce.getCodeChallenge());
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.codeVerifier(pkce.getCodeVerifier());

OAuthClient.AuthorizationEndpointResponse authsEndpointResponse = oauth.doLogin("admin", "admin");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET);
String accessToken = tokenResponse.getAccessToken();
logger.debug("access token:" + accessToken);
assertBasicClaims(oauth.verifyToken(accessToken), true, true);

try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
HttpGet get = new HttpGet(OAuthClient.SERVER_ROOT + "/auth/admin/realms/master");
get.setHeader("Authorization", "Bearer " + accessToken);
try (CloseableHttpResponse response = client.execute(get)) {
Assert.assertEquals(200, response.getStatusLine().getStatusCode());
RealmRepresentation realmRepresentation = JsonSerialization.readValue(response.getEntity().getContent(), RealmRepresentation.class);
Assert.assertEquals("master", realmRepresentation.getRealm());
}
} catch (Exception e) {
Assert.fail(e.getMessage());
}
}

private void removeSession(final String sessionId) {
testingClient.testing().removeExpired(REALM_NAME);
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ public AdminEventRepresentation assertEvent(AdminEventRepresentation actual) {

AuthDetailsRepresentation actualAuth = actual.getAuthDetails();
Assert.assertEquals(expectedAuth.getRealmId(), actualAuth.getRealmId());
Assert.assertEquals(expectedAuth.getUserId(), actualAuth.getUserId());
if(expectedAuth.getUserId() != null) {
Assert.assertEquals(expectedAuth.getUserId(), actualAuth.getUserId());
}
if (expectedAuth.getClientId() != null) {
Assert.assertEquals(expectedAuth.getClientId(), actualAuth.getClientId());
}
Expand Down