Admin API with Lightweight access token and transient session

Closes #32802

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-09-11 16:47:16 +02:00 committed by Marek Posolda
parent 9c780e9190
commit e6c5ee31e4
4 changed files with 57 additions and 16 deletions

View file

@ -35,6 +35,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.util.JsonSerialization;
@ -136,6 +137,10 @@ public class KeycloakIdentity implements Identity {
throw new RuntimeException("No active session associated with the token");
}
if (AuthenticationManager.isSessionValid(realm, userSession) && token.isIssuedBeforeSessionStart(userSession.getStarted())) {
throw new RuntimeException("Invalid token");
}
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessionByClient(client.getId());
@ -307,7 +312,9 @@ public class KeycloakIdentity implements Identity {
if (userSession == null) {
userSession = sessions.getOfflineUserSession(realm, accessToken.getSessionState());
}
if (AuthenticationManager.isSessionValid(realm, userSession) && accessToken.isIssuedBeforeSessionStart(userSession.getStarted())) {
return null;
}
return userSession.getUser();
}
}

View file

@ -29,6 +29,8 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@ -52,6 +54,7 @@ import java.util.Map;
import jakarta.ws.rs.ForbiddenException;
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.UserSessionUtil;
import org.keycloak.utils.RoleResolveUtil;
/**
@ -107,20 +110,17 @@ 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) {
ClientModel client = adminsRealm.getClientByClientId(issuedFor);
//support for lightweight access token and transient session
if (accessToken.getSubject() == null || (accessToken.getSessionId() == null && accessToken.getResourceAccess().isEmpty() && accessToken.getRealmAccess() == 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());
}
EventBuilder event = new EventBuilder(adminsRealm, session);
event.event(EventType.INTROSPECT_TOKEN);
UserSessionModel userSession = UserSessionUtil.findValidSession(session, adminsRealm, accessToken, event, client);
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);

View file

@ -700,12 +700,9 @@ public abstract class AbstractKeycloakTest {
Time.setOffset(offset);
Map result = testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset)));
// force refreshing token after time offset has changed
try {
adminClient.tokenManager().refreshToken();
} catch (RuntimeException e) {
// force getting new token after time offset has changed
adminClient.tokenManager().grantToken();
}
return String.valueOf(result);
}

View file

@ -32,6 +32,7 @@ import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
@ -68,6 +69,7 @@ import org.keycloak.utils.MediaType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -521,6 +523,41 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
}
}
@Test
public void testAdminApiWithLightweightAccessTokenAndTransientSession() {
RealmResource masterRealm = realmsResouce().realm("master");
ClientRepresentation transientClient = KeycloakModelUtils.createClient(realmsResouce().realm("master").toRepresentation(), "transient_client");
transientClient.setServiceAccountsEnabled(Boolean.TRUE);
transientClient.setAttributes(new HashMap<>());
transientClient.getAttributes().put(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, String.valueOf(true));
masterRealm.clients().create(transientClient);
transientClient = masterRealm.clients().findByClientId(transientClient.getClientId()).get(0);
UserRepresentation userRep = masterRealm.clients().get(transientClient.getId()).getServiceAccountUser();
masterRealm.users().get(userRep.getId()).roles().realmLevel().add(Collections.singletonList(masterRealm.roles().get(AdminRoles.ADMIN).toRepresentation()));
try {
oauth.realm("master");
oauth.clientId(transientClient.getClientId());
OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest(transientClient.getSecret());
String accessTokenString = tokenResponse.getAccessToken();
Assert.assertNull(tokenResponse.getRefreshToken());
AccessToken accessToken = oauth.verifyToken(accessTokenString);
Assert.assertNotNull(accessToken.getSubject());
Assert.assertNull(accessToken.getSessionId());
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpGet get = new HttpGet(OAuthClient.SERVER_ROOT + "/auth/admin/realms/master");
get.setHeader("Authorization", "Bearer " + accessTokenString);
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 {