diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java index 68c75ffb4b5..d649d7abbef 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java @@ -165,6 +165,9 @@ public interface ClientResource { @Path("/roles") RolesResource roles(); + @Path("/evaluate-scopes") + ClientScopeEvaluateResource clientScopesEvaluate(); + /** * Get default client scopes. Only name and ids are returned. * diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopeEvaluateResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopeEvaluateResource.java new file mode 100644 index 00000000000..816ef913ca3 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopeEvaluateResource.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.admin.client.resource; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; + +import java.util.Map; + +/** + * @author Giuseppe Graziano + */ +public interface ClientScopeEvaluateResource { + + @GET + @Path("generate-example-access-token") + AccessToken generateAccessToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId, @QueryParam("audience") String audience); + + @GET + @Path("generate-example-id-token") + IDToken generateExampleIdToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId, @QueryParam("audience") String audience); + + + @GET + @Path("generate-example-userinfo") + Map generateExampleUserinfo(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId); + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java index 358ae4b3b90..9aae60aeba1 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java @@ -274,6 +274,7 @@ public class ClientScopeEvaluateResource { private R sessionAware(UserModel user, String scopeParam, String audienceParam, TriFunction function) { AuthenticationSessionModel authSession = null; + UserSessionModel userSession = null; AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session); try { @@ -285,8 +286,8 @@ public class ClientScopeEvaluateResource { authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scopeParam); - UserSessionModel userSession = new UserSessionManager(session).createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(), - clientConnection.getRemoteHost(), "example-auth", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT); + userSession = new UserSessionManager(session).createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(), + clientConnection.getRemoteHost(), "example-auth", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); AuthenticationManager.setClientScopesInSession(session, authSession); ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession); @@ -302,6 +303,9 @@ public class ClientScopeEvaluateResource { if (authSession != null) { authSessionManager.removeAuthenticationSession(realm, authSession, false); } + if (userSession != null) { + session.sessions().removeUserSession(realm, userSession); + } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeEvaluateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeEvaluateTest.java new file mode 100644 index 00000000000..44e43348b7d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeEvaluateTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.keycloak.testsuite.admin.client; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.testsuite.admin.ApiUtil; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Giuseppe Graziano + */ +public class ClientScopeEvaluateTest extends AbstractClientTest { + + private ClientResource accountClient; + + @Before + public void init() { + accountClient = findClientResourceById("account"); + createTestUserWithAdminClient(); + getCleanup().addUserId(testUser.getId()); + } + + @Test + public void testGenerateAccessToken() { + AccessToken accessToken = accountClient.clientScopesEvaluate().generateAccessToken("openid", testUser.getId(), null); + assertNotNull(accessToken); + assertNotNull(accessToken.getSubject()); + assertNotNull(accessToken.getPreferredUsername()); + + List sessions = accountClient.getUserSessions(0, 5); + assertEquals(0, sessions.size()); + } + + @Test + public void testGenerateIdToken() { + IDToken idToken = accountClient.clientScopesEvaluate().generateExampleIdToken("openid", testUser.getId(), null); + assertNotNull(idToken); + assertNotNull(idToken.getSubject()); + assertNotNull(idToken.getPreferredUsername()); + } + + @Test + public void testGenerateUserInfo() { + Map userinfo = accountClient.clientScopesEvaluate().generateExampleUserinfo("openid", testUser.getId()); + assertFalse(userinfo.isEmpty()); + assertNotNull(userinfo.get(IDToken.SUBJECT)); + assertNotNull(userinfo.get(IDToken.PREFERRED_USERNAME)); + } + + @Test + public void testGenerateAccessTokenWithoutBasicScope() { + String basicScopeId = ApiUtil.findClientScopeByName(testRealmResource(),"basic").toRepresentation().getId(); + accountClient.removeDefaultClientScope(basicScopeId); + + AccessToken accessToken = accountClient.clientScopesEvaluate().generateAccessToken("openid", testUser.getId(), null); + assertNotNull(accessToken); + assertNull(accessToken.getSubject()); + + accountClient.addDefaultClientScope(basicScopeId); + } + + @Test + public void testGenerateAccessTokenWithOptionalScope() { + String emailScopeId = ApiUtil.findClientScopeByName(testRealmResource(),"email").toRepresentation().getId(); + accountClient.removeDefaultClientScope(emailScopeId); + accountClient.addOptionalClientScope(emailScopeId); + + AccessToken accessToken = accountClient.clientScopesEvaluate().generateAccessToken("openid", testUser.getId(), null); + assertNotNull(accessToken); + assertNull(accessToken.getEmail()); + + accessToken = accountClient.clientScopesEvaluate().generateAccessToken("openid email", testUser.getId(), null); + assertNotNull(accessToken); + assertNotNull(accessToken.getEmail()); + + accountClient.removeOptionalClientScope(emailScopeId); + accountClient.addDefaultClientScope(emailScopeId); + } + +}