From c76cbc94d89128ef1af5353132bae2463a7ed78e Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Mon, 8 Apr 2024 18:29:17 +0200 Subject: [PATCH] Add sub via protocol mapper to access token Closes #21185 Signed-off-by: Giuseppe Graziano --- .../main/java/org/keycloak/TokenVerifier.java | 1 - .../topics/clients/con-protocol-mappers.adoc | 8 +- .../topics/changes/changes-25_0_0.adoc | 14 ++- .../AccessTokenIntrospectionProvider.java | 23 ++--- .../oidc/OIDCLoginProtocolFactory.java | 9 +- .../keycloak/protocol/oidc/TokenManager.java | 8 +- .../oidc/mappers/SessionStateMapper.java | 6 +- .../protocol/oidc/mappers/SubMapper.java | 94 ++++++++++++++++++ .../org.keycloak.protocol.ProtocolMapper | 1 + .../HardcodedClientStorageProvider.java | 5 +- .../OIDCPairwiseClientRegistrationTest.java | 28 +++++- .../oauth/OIDCProtocolMappersTest.java | 4 +- .../oauth/TokenIntrospectionTest.java | 1 - .../oidc/LightWeightAccessTokenTest.java | 97 ++++++++++++++++++- 14 files changed, 263 insertions(+), 36 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/mappers/SubMapper.java diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index dc2321a9b2..c120f48711 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -256,7 +256,6 @@ public class TokenVerifier { public TokenVerifier withDefaultChecks() { return withChecks( RealmUrlCheck.NULL_INSTANCE, - SUBJECT_EXISTS_CHECK, TokenTypeCheck.INSTANCE_DEFAULT_TOKEN_TYPE, IS_ACTIVE ); diff --git a/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc b/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc index 7e8d02bb9b..4fe5aa7adf 100644 --- a/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc +++ b/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc @@ -67,6 +67,12 @@ Use the *Script Mapper* to map claims to tokens by running user-defined JavaScri When scripts deploy, you should be able to select the deployed scripts from the list of available mappers. +== Pairwise subject identifier mapper + +Subject claim _sub_ is mapped by default by *Subject (sub)* protocol mapper in the default client scope *basic*. + +To use a pairwise subject identifier by using a protocol mapper such as *Pairwise subject identifier*, remove the *Subject (sub)* protocol mapper from the *basic* client scope. + [[_using_lightweight_access_token]] == Using lightweight access token The access token in {project_name} contains sensitive information, including Personal Identifiable Information (PII). @@ -75,7 +81,7 @@ Further, when the resource server acquires the PII removed from the access token Information that cannot be removed from a lightweight access token:: Protocol mappers can controls which information is put onto an access token and the lightweight access token use the protocol mappers. Therefore, the following information cannot be removed from the lightweight access. + - `exp`, `iat`, `jti`, `iss`, `sub`, `typ`, `azp`, `nonce`, `sid`, `scope`, `cnf` + `exp`, `iat`, `jti`, `iss`, `typ`, `azp`, `sid`, `scope`, `cnf` Using a lightweight access token in {project_name}:: By applying `use-lightweight-access-token` executor of <<_client_policies, client policies>> to a client, the client can receive a lightweight access token instead of an access token. The lightweight access token contains a claim controlled by a protocol mapper where its setting `Add to lightweight access token`(default OFF) is turned ON. Also, by turning ON its setting `Add to token introspection` of the protocol mapper, the client can obtain the claim by sending the access token to {project_name}'s token introspection endpoint. diff --git a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc index 0eb0369cad..25b6d67a2f 100644 --- a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -89,7 +89,19 @@ Note that the `setSessionState()` method is also removed from the `IDToken` clas A new `Session State (session_state)` mapper is also included and can be assigned to client scopes (for instance `basic` client scope) to revert to the old behavior. -If an old version of the JS adapter is used, the `Session State (session_state)` mapper should also be used via client scopes as described above. +If an old version of the JS adapter is used, the `Session State (session_state)` mapper should also be used by using client scopes as described above. + += `sub` claim is added to access token via protocol mapper + +The `sub` claim, which was always added to the access token, is now added by default but using a new `Subject (sub)` protocol mapper. + +The `Subject (sub)` mapper is configured by default in the `basic` client scope. Therefore, no extra configuration is required after upgrading to this version. + +Only in the case you are using `Pairwise subject identifier` mapper to map `sub` claim for access token you should disable or remove `Subject (sub)` mapper. + +You can use the `Subject (sub)` mapper to configure the `sub` claim only for access token, lightweight access token, and introspection response. IDToken and Userinfo always contain `sub` claim. + +The mapper has no effects for service accounts, because no user session exists, and the`sub` claim is always added to the access token. = Default `http-pool-max-threads` reduced diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java index 157006972a..bb2f7cdfca 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java @@ -70,6 +70,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi ObjectNode tokenMetadata; if (accessToken != null) { + UserSessionModel userSession = accessToken.getSessionId() == null ? null : session.sessions().getUserSession(realm, accessToken.getSessionId()); tokenMetadata = JsonSerialization.createObjectNode(accessToken); tokenMetadata.put("client_id", accessToken.getIssuedFor()); @@ -82,25 +83,21 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi if (accessToken.getPreferredUsername() != null) { tokenMetadata.put("username", accessToken.getPreferredUsername()); } else { - UserModel userModel = session.users().getUserById(realm, accessToken.getSubject()); + UserModel userModel = accessToken.getSubject() == null ? null : session.users().getUserById(realm, accessToken.getSubject()); if (userModel != null) { tokenMetadata.put("username", userModel.getUsername()); + } else if (userSession != null && userSession.getUser() != null) { + tokenMetadata.put("username", userSession.getUser().getUsername()); } } } - String sessionState = accessToken.getSessionState(); + if (userSession != null) { + String actor = userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()); - if (sessionState != null) { - UserSessionModel userSession = session.sessions().getUserSession(realm, sessionState); - - if (userSession != null) { - String actor = userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()); - - if (actor != null) { - // for token exchange delegation semantics when an entity (actor) other than the subject is the acting party to whom authority has been delegated - tokenMetadata.putObject("act").put("sub", actor); - } + if (actor != null) { + // for token exchange delegation semantics when an entity (actor) other than the subject is the acting party to whom authority has been delegated + tokenMetadata.putObject("act").put("sub", actor); } } @@ -157,7 +154,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi AccessToken newToken = new AccessToken(); newToken.id(token.getId()); newToken.type(token.getType()); - newToken.subject(token.getSubject() != null ? token.getSubject() : userSession.getUser().getId()); + newToken.subject(token.getSubject()); newToken.iat(token.getIat()); newToken.exp(token.getExp()); newToken.issuedFor(token.getIssuedFor()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index 03d3251e49..d1eea24b9a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -45,6 +45,7 @@ import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; +import org.keycloak.protocol.oidc.mappers.SubMapper; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.ServicesLogger; @@ -226,7 +227,10 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { model = UserSessionNoteMapper.createClaimMapper(IDToken.AUTH_TIME, AuthenticationManager.AUTH_TIME, IDToken.AUTH_TIME, "long", true, true, false, true); - builtins.put(BASIC_SCOPE, model); + builtins.put(IDToken.AUTH_TIME, model); + + model = SubMapper.create(IDToken.SUBJECT,true, true); + builtins.put(IDToken.SUBJECT, model); } private void createUserAttributeMapper(String name, String attrName, String claimName, String type) { @@ -420,7 +424,8 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { basicScope.setDisplayOnConsentScreen(false); basicScope.setIncludeInTokenScope(false); basicScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - basicScope.addProtocolMapper(builtins.get(BASIC_SCOPE)); + basicScope.addProtocolMapper(builtins.get(IDToken.AUTH_TIME)); + basicScope.addProtocolMapper(builtins.get(IDToken.SUBJECT)); newRealm.addDefaultClientScope(basicScope, true); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index d7fbb0ad6c..ab24919f99 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -387,7 +387,7 @@ public class TokenManager { */ public static UserModel lookupUserFromStatelessToken(KeycloakSession session, RealmModel realm, AccessToken token) { // Try to lookup user based on "sub" claim. It should work for most cases with some rare exceptions (EG. OIDC "pairwise" subjects) - UserModel user = session.users().getUserById(realm, token.getSubject()); + UserModel user = token.getSubject() == null ? null : session.users().getUserById(realm, token.getSubject()); if (user != null) { return user; } @@ -976,7 +976,9 @@ public class TokenManager { AccessToken token = new AccessToken(); token.id(KeycloakModelUtils.generateId()); token.type(TokenUtil.TOKEN_TYPE_BEARER); - token.subject(user.getId()); + if (UserSessionModel.SessionPersistenceState.TRANSIENT.equals(session.getPersistenceState())) { + token.subject(user.getId()); + } token.issuedNow(); token.issuedFor(client.getClientId()); @@ -1184,7 +1186,7 @@ public class TokenManager { idToken = new IDToken(); idToken.id(KeycloakModelUtils.generateId()); idToken.type(TokenUtil.TOKEN_TYPE_ID); - idToken.subject(accessToken.getSubject()); + idToken.subject(userSession.getUser().getId()); idToken.audience(client.getClientId()); idToken.issuedNow(); idToken.issuedFor(accessToken.getIssuedFor()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java index 83421c9d47..469b192483 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SessionStateMapper.java @@ -40,10 +40,14 @@ public class SessionStateMapper extends AbstractOIDCProtocolMapper implements OI public static final String PROVIDER_ID = "oidc-session-state-mapper"; - private static final Logger logger = Logger.getLogger(AcrProtocolMapper.class); + private static final Logger logger = Logger.getLogger(SessionStateMapper.class); private static final List configProperties = new ArrayList<>(); + static { + OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, SessionStateMapper.class); + } + public List getConfigProperties() { return configProperties; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/SubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SubMapper.java new file mode 100644 index 0000000000..7ecd01ada4 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/SubMapper.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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.protocol.oidc.mappers; + +import org.jboss.logging.Logger; +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.IDToken; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Giuseppe Graziano + */ +public class SubMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, TokenIntrospectionTokenMapper { + + + public static final String PROVIDER_ID = "oidc-sub-mapper"; + + private static final Logger logger = Logger.getLogger(SubMapper.class); + + private static final List configProperties = new ArrayList<>(); + + static { + OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, SubMapper.class); + } + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Subject (sub)"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Add Subject (sub) claim"; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, + ClientSessionContext clientSessionCtx) { + if (userSession != null && userSession.getUser() != null) { + token.subject(userSession.getUser().getId()); + } + } + + public static ProtocolMapperModel create(String name, boolean accessToken, boolean introspectionEndpoint) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + Map config = new HashMap<>(); + if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + if (introspectionEndpoint) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "true"); + mapper.setConfig(config); + return mapper; + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 904acd0201..716fc14990 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -49,3 +49,4 @@ org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper org.keycloak.protocol.oidc.mappers.NonceBackwardsCompatibleMapper org.keycloak.protocol.oidc.mappers.SessionStateMapper +org.keycloak.protocol.oidc.mappers.SubMapper diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java index 25e4d73af0..756423c2d4 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jboss.logging.Logger; @@ -107,8 +108,10 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl if (defaultScope) { ClientScopeModel rolesScope = KeycloakModelUtils.getClientScopeByName(realm, OIDCLoginProtocolFactory.ROLES_SCOPE); ClientScopeModel webOriginsScope = KeycloakModelUtils.getClientScopeByName(realm, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE); - return Arrays.asList(rolesScope, webOriginsScope) + ClientScopeModel basicScope = KeycloakModelUtils.getClientScopeByName(realm, OIDCLoginProtocolFactory.BASIC_SCOPE); + return Arrays.asList(rolesScope, webOriginsScope, basicScope) .stream() + .filter(Objects::nonNull) .collect(Collectors.toMap(ClientScopeModel::getName, clientScope -> clientScope)); } else { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java index 8719ba286b..9ad69ecc8d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCPairwiseClientRegistrationTest.java @@ -28,6 +28,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.HttpErrorException; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; @@ -90,6 +91,7 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati OIDCClientRepresentation clientRep = createRep(); clientRep.setSubjectType("pairwise"); OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep); + removeDefaultBasicClientScope(pairwiseClient.getClientId()); return pairwiseClient; } @@ -327,11 +329,8 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati Assert.assertEquals(user.getId(), tokenUserId); // Create pairwise client - OIDCClientRepresentation clientRep = createRep(); - clientRep.setSubjectType("pairwise"); - OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep); + OIDCClientRepresentation pairwiseClient = createPairwise(); Assert.assertEquals("pairwise", pairwiseClient.getSubjectType()); - // Login to pairwise client oauth.clientId(pairwiseClient.getClientId()); oauth.openLoginForm(); @@ -371,7 +370,6 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati public void refreshPairwiseToken() throws Exception { // Create pairwise client OIDCClientRepresentation pairwiseClient = createPairwise(); - // Login to pairwise client OAuthClient.AccessTokenResponse accessTokenResponse = login(pairwiseClient, "test-user@localhost", "password"); @@ -487,4 +485,24 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati String payloadBase64 = token.split("\\.")[1]; return new String(Base64.getDecoder().decode(payloadBase64)); } + + public void addDefaultBasicClientScope(String clientId) { + realmsResouce().realm(REALM_NAME).getDefaultDefaultClientScopes() + .stream() + .filter(scope-> scope.getName().equals(OIDCLoginProtocolFactory.BASIC_SCOPE)) + .findFirst() + .ifPresent(scope-> { + ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), clientId).addDefaultClientScope(scope.getId()); + }); + } + + public void removeDefaultBasicClientScope(String clientId) { + realmsResouce().realm(REALM_NAME).getDefaultDefaultClientScopes() + .stream() + .filter(scope-> scope.getName().equals(OIDCLoginProtocolFactory.BASIC_SCOPE)) + .findFirst() + .ifPresent(scope-> { + ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), clientId).removeDefaultClientScope(scope.getId()); + }); + } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index bdb47b0acb..66717415b0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java @@ -378,7 +378,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { // create a user attr mapping for some claims that exist as properties in the tokens ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app"); - app.getProtocolMappers().createMapper(createClaimMapper("userid-as-sub", "userid", "sub", "String", true, true, true,false)).close(); + app.getProtocolMappers().createMapper(createClaimMapper("userid-as-sub", "userid", "sub", "String", false, true, true,false)).close(); app.getProtocolMappers().createMapper(createClaimMapper("useraud", "useraud", "aud", "String", true, true, true, true)).close(); app.getProtocolMappers().createMapper(createHardcodedClaim("website-hardcoded", "website", "http://localhost", "String", true, true, true)).close(); app.getProtocolMappers().createMapper(createHardcodedClaim("iat-hardcoded", "iat", "123", "long", true, false, true)).close(); @@ -394,7 +394,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { assertThat(Arrays.asList(idToken.getAudience()), hasItems("test-app", "other")); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); - assertEquals(user.firstAttribute("userid"), accessToken.getSubject()); + assertNotEquals(user.firstAttribute("userid"), accessToken.getSubject()); assertEquals("http://localhost", accessToken.getWebsite()); assertNotNull(accessToken.getAudience()); assertThat(Arrays.asList(accessToken.getAudience()), hasItems("test-app", "other")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index e5cdf7fc0b..45c6117aa9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -349,7 +349,6 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { assertTrue(rep.isActive()); assertEquals("test-user@localhost", rep.getUserName()); assertEquals("no-scope", rep.getClientId()); - assertEquals(loginEvent.getUserId(), rep.getSubject()); assertNull(rep.getScope()); } finally { testRealm.setClientScopes(preExistingClientScopes); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/LightWeightAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/LightWeightAccessTokenTest.java index 92eec32e73..fd455e6ef2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/LightWeightAccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/LightWeightAccessTokenTest.java @@ -29,6 +29,7 @@ import org.keycloak.common.Profile; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper; import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper; import org.keycloak.protocol.oidc.mappers.HardcodedClaim; @@ -268,7 +269,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { @Test public void testPolicyLightWeightFalseTest() throws Exception { setUseLightweightAccessTokenExecutor(); - ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, true); + ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, false); try { oauth.nonce("123456"); oauth.scope("address"); @@ -303,7 +304,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { @Test public void testPolicyLightWeightTrueTest() throws Exception { setUseLightweightAccessTokenExecutor(); - ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, true, true); + ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, true, false); try { oauth.nonce("123456"); oauth.scope("address"); @@ -329,7 +330,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { @Test public void testAlwaysUseLightWeightFalseTest() throws Exception { alwaysUseLightWeightAccessToken(true); - ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, true); + ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, false); try { oauth.nonce("123456"); oauth.scope("address"); @@ -364,7 +365,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { @Test public void testAlwaysUseLightWeightTrueTest() throws Exception { alwaysUseLightWeightAccessToken(true); - ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, true, true); + ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, true, false); try { oauth.nonce("123456"); oauth.scope("address"); @@ -388,6 +389,71 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { } } + @Test + public void testWithoutBasicClaim() throws Exception { + alwaysUseLightWeightAccessToken(true); + removeDefaultBasicClientScope(); + ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, false); + try { + oauth.clientId(TEST_CLIENT); + oauth.scope("address"); + + OAuthClient.AuthorizationEndpointResponse authsEndpointResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); + String accessToken = tokenResponse.getAccessToken(); + logger.debug("access token:" + accessToken); + assertAccessToken(oauth.verifyToken(accessToken), true, false,true); + + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + String introspectResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + logger.debug("tokenResponse:" + introspectResponse); + assertTokenIntrospectionResponse(JsonSerialization.readValue(introspectResponse, AccessToken.class), true, true, true); + + oauth.clientId(TEST_CLIENT); + alwaysUseLightWeightAccessToken(false); + oauth.doLogout(tokenResponse.getRefreshToken(), TEST_CLIENT_SECRET); + + + authsEndpointResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); + accessToken = tokenResponse.getAccessToken(); + logger.debug("access token:" + accessToken); + assertAccessToken(oauth.verifyToken(accessToken), true, true, true); + + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + introspectResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + logger.debug("tokenResponse:" + introspectResponse); + assertTokenIntrospectionResponse(JsonSerialization.readValue(introspectResponse, AccessToken.class), true, true, true); + + } finally { + deleteProtocolMappers(protocolMappers); + addDefaultBasicClientScope(); + } + } + + @Test + public void clientCredentialWithoutBasicClaims() throws Exception { + removeDefaultBasicClientScope(); + alwaysUseLightWeightAccessToken(true); + try { + oauth.nonce("123456"); + + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest(TEST_CLIENT_SECRET); + String accessToken = response.getAccessToken(); + logger.debug("accessToken:" + accessToken); + assertAccessToken(oauth.verifyToken(accessToken), false, false,false); + + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + logger.debug("tokenResponse:" + tokenResponse); + assertTokenIntrospectionResponse(JsonSerialization.readValue(tokenResponse, AccessToken.class), false, true, false); + } finally { + addDefaultBasicClientScope(); + alwaysUseLightWeightAccessToken(false); + } + } + private void removeSession(final String sessionId) { testingClient.testing().removeExpired(REALM_NAME); try { @@ -446,7 +512,6 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { Assert.assertNotNull(token.getIssuedFor()); Assert.assertNotNull(token.getScope()); Assert.assertNotNull(token.getIssuer()); - Assert.assertNotNull(token.getSubject()); if (isAuthCodeFlow) { Assert.assertNotNull(token.getSessionId()); } else { @@ -457,7 +522,9 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { private void assertBasicClaims(AccessToken token, boolean isAuthCodeFlow, boolean missing) { if (missing) { Assert.assertNull(token.getAuth_time()); + Assert.assertNull(token.getSubject()); } else { + Assert.assertNotNull(token.getSubject()); if (isAuthCodeFlow) { Assert.assertNotNull(token.getAuth_time()); } else { @@ -491,6 +558,26 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { return adminClient.realm(REALM_NAME); } + public void addDefaultBasicClientScope() { + testRealm().getDefaultDefaultClientScopes() + .stream() + .filter(scope-> scope.getName().equals(OIDCLoginProtocolFactory.BASIC_SCOPE)) + .findFirst() + .ifPresent(scope-> { + ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), TEST_CLIENT).addDefaultClientScope(scope.getId()); + }); + } + + public void removeDefaultBasicClientScope() { + testRealm().getDefaultDefaultClientScopes() + .stream() + .filter(scope-> scope.getName().equals(OIDCLoginProtocolFactory.BASIC_SCOPE)) + .findFirst() + .ifPresent(scope-> { + ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), TEST_CLIENT).removeDefaultClientScope(scope.getId()); + }); + } + private void setScopeProtocolMappers(boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean isIncludeLightweightAccessToken) { setScopeProtocolMapper(ACR_SCOPE, ACR, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); setScopeProtocolMapper(PROFILE_CLAIM, FULL_NAME, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken);