From fe06df67c201f7574d0fe7767c530d655017ac6a Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Tue, 26 Mar 2024 16:59:58 +0100 Subject: [PATCH] New default client scope for 'basic' claims with 'auth_time' protocol mapper Closes #27623 Signed-off-by: Giuseppe Graziano --- .../topics/clients/con-protocol-mappers.adoc | 2 +- .../topics/changes/changes-25_0_0.adoc | 10 +++ .../migration/migrators/MigrateTo25_0_0.java | 64 +++++++++++++++++++ .../datastore/DefaultMigrationManager.java | 4 +- .../keycloak/migration/MigrationProvider.java | 8 +++ .../AccessTokenIntrospectionProvider.java | 1 - .../oidc/OIDCLoginProtocolFactory.java | 27 ++++++++ .../keycloak/protocol/oidc/TokenManager.java | 7 -- .../mappers/OIDCAttributeMapperHelper.java | 8 ++- .../migration/DefaultMigrationProvider.java | 5 ++ .../exportimport/ExportImportUtil.java | 4 +- .../migration/AbstractMigrationTest.java | 9 +++ .../JsonFileImport1903MigrationTest.java | 1 + .../JsonFileImport198MigrationTest.java | 1 + .../JsonFileImport255MigrationTest.java | 1 + .../JsonFileImport343MigrationTest.java | 1 + .../JsonFileImport483MigrationTest.java | 1 + .../JsonFileImport903MigrationTest.java | 1 + .../testsuite/migration/MigrationTest.java | 1 + .../oidc/LightWeightAccessTokenTest.java | 37 ++++++----- .../oidc/OIDCWellKnownProviderTest.java | 2 +- .../migration-realm-19.0.3.json | 34 ++++++++-- 22 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo25_0_0.java 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 410faf3e89..1c4a857dcd 100644 --- a/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc +++ b/docs/documentation/server_admin/topics/clients/con-protocol-mappers.adoc @@ -75,7 +75,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`, `auth_time`, `jti`, `iss`, `sub`, `typ`, `azp`, `nonce`, `session_state`, `sid`, `scope`, `cnf` + `exp`, `iat`, `jti`, `iss`, `sub`, `typ`, `azp`, `nonce`, `session_state`, `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 9ef5f354a1..8f7a92db80 100644 --- a/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc @@ -68,3 +68,13 @@ How kcadm parses and handles options and parameters has changed. Error messages = Removing custom user attribute indexes When searching for users by user attribute, Keycloak no longer searches for user attribute names forcing lower case comparisons. This means Keycloak's native index on the user attribute table will now be used when searching. If you have created your own index based on `lower(name)`to speed up searches, you can now remove it. + += New default client scope `basic` + +The new client scope named `basic` is added as a realm "default" client scope and hence will be added to all newly created clients. The client scope is also automatically added to all existing clients during migration. + +This scope contains preconfigured protocol mappers for the following claims: + + * `auth_time` + +This helps to reduce even more the number of claims in a lightweight access token, but also gives the chance to configure claims that were always added automatically. \ No newline at end of file diff --git a/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo25_0_0.java b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo25_0_0.java new file mode 100644 index 0000000000..139d59f7f8 --- /dev/null +++ b/model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo25_0_0.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 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.migration.migrators; + + +import org.jboss.logging.Logger; +import org.keycloak.migration.MigrationProvider; +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.RealmRepresentation; + +/** + * @author Giuseppe Graziano + */ +public class MigrateTo25_0_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("25.0.0"); + + private static final Logger LOG = Logger.getLogger(MigrateTo25_0_0.class); + + @Override + public ModelVersion getVersion() { + return VERSION; + } + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealmsStream().forEach(realm -> migrateRealm(session, realm)); + } + + @Override + public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { + migrateRealm(session, realm); + } + protected void migrateRealm(KeycloakSession session, RealmModel realm) { + MigrationProvider migrationProvider = session.getProvider(MigrationProvider.class); + + // create 'basic' client scope in the realm. + ClientScopeModel basicScope = migrationProvider.addOIDCBasicClientScope(realm); + + //add basic scope to existing clients + realm.getClientsStream().forEach(c-> c.addClientScope(basicScope, true)); + } +} + diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java index fbb4d65d95..d63cee32a6 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultMigrationManager.java @@ -38,6 +38,7 @@ import org.keycloak.migration.migrators.MigrateTo21_0_0; import org.keycloak.migration.migrators.MigrateTo22_0_0; import org.keycloak.migration.migrators.MigrateTo23_0_0; import org.keycloak.migration.migrators.MigrateTo24_0_0; +import org.keycloak.migration.migrators.MigrateTo25_0_0; import org.keycloak.migration.migrators.MigrateTo2_0_0; import org.keycloak.migration.migrators.MigrateTo2_1_0; import org.keycloak.migration.migrators.MigrateTo2_2_0; @@ -113,7 +114,8 @@ public class DefaultMigrationManager implements MigrationManager { new MigrateTo21_0_0(), new MigrateTo22_0_0(), new MigrateTo23_0_0(), - new MigrateTo24_0_0() + new MigrateTo24_0_0(), + new MigrateTo25_0_0() }; private final KeycloakSession session; diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java index b86704865c..2bcef409f3 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationProvider.java @@ -77,4 +77,12 @@ public interface MigrationProvider extends Provider { * @return created or already existing client scope 'acr' */ void addOIDCAcrClientScope(RealmModel realm); + + /** + * Add 'basic' client scope or return it if already exists + * + * @param realm + * @return created or already existing client scope 'basic' + */ + ClientScopeModel addOIDCBasicClientScope(RealmModel realm); } 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 9b318112bd..31f03f1009 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java @@ -164,7 +164,6 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi newToken.issuer(token.getIssuer()); newToken.setNonce(token.getNonce()); newToken.setScope(token.getScope()); - newToken.setAuth_time(token.getAuth_time()); newToken.setSessionState(token.getSessionState()); // In the case of a refresh token, aud is a basic claim. 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 f9a34d47b5..03d3251e49 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -48,6 +48,7 @@ import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.ServicesLogger; +import org.keycloak.services.managers.AuthenticationManager; import java.util.HashMap; import java.util.HashSet; @@ -97,6 +98,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { public static final String WEB_ORIGINS_SCOPE = "web-origins"; public static final String MICROPROFILE_JWT_SCOPE = "microprofile-jwt"; public static final String ACR_SCOPE = "acr"; + public static final String BASIC_SCOPE = "basic"; public static final String PROFILE_SCOPE_CONSENT_TEXT = "${profileScopeConsentText}"; public static final String EMAIL_SCOPE_CONSENT_TEXT = "${emailScopeConsentText}"; @@ -220,6 +222,11 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { model = AcrProtocolMapper.create(ACR, true, true, true); builtins.put(ACR, model); } + + model = UserSessionNoteMapper.createClaimMapper(IDToken.AUTH_TIME, AuthenticationManager.AUTH_TIME, + IDToken.AUTH_TIME, "long", + true, true, false, true); + builtins.put(BASIC_SCOPE, model); } private void createUserAttributeMapper(String name, String attrName, String claimName, String type) { @@ -298,6 +305,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { addWebOriginsClientScope(newRealm); addMicroprofileJWTClientScope(newRealm); addAcrClientScope(newRealm); + addBasicClientScope(newRealm); if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) { ClientScopeModel organizationScope = newRealm.addClientScope(OAuth2Constants.ORGANIZATION); @@ -404,6 +412,25 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { } } + public ClientScopeModel addBasicClientScope(RealmModel newRealm) { + ClientScopeModel basicScope = KeycloakModelUtils.getClientScopeByName(newRealm, BASIC_SCOPE); + if (basicScope == null) { + basicScope = newRealm.addClientScope(BASIC_SCOPE); + basicScope.setDescription("OpenID Connect scope for add all basic claims to the token"); + basicScope.setDisplayOnConsentScreen(false); + basicScope.setIncludeInTokenScope(false); + basicScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + basicScope.addProtocolMapper(builtins.get(BASIC_SCOPE)); + + newRealm.addDefaultClientScope(basicScope, true); + + logger.debugf("Client scope '%s' created in the realm '%s'.", BASIC_SCOPE, newRealm.getName()); + } else { + logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", BASIC_SCOPE, newRealm.getName()); + } + return basicScope; + } + @Override protected void addDefaults(ClientModel client) { } 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 42c7160741..d0c379eeb4 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -991,12 +991,6 @@ public class TokenManager { token.setAcr(acr); } - String authTime = session.getNote(AuthenticationManager.AUTH_TIME); - if (authTime != null) { - token.setAuthTime(Integer.parseInt(authTime)); - } - - token.setSessionState(session.getId()); ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(realm, OAuth2Constants.OFFLINE_ACCESS); boolean offlineTokenRequested = offlineAccessScope == null ? false @@ -1195,7 +1189,6 @@ public class TokenManager { idToken.issuedFor(accessToken.getIssuedFor()); idToken.issuer(accessToken.getIssuer()); idToken.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class)); - idToken.setAuthTime(accessToken.getAuthTime()); idToken.setSessionState(accessToken.getSessionState()); idToken.expiration(accessToken.getExpiration()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java index 625d0e9240..89fe171120 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java @@ -110,6 +110,13 @@ public class OIDCAttributeMapperHelper { tmpToken.put(IDToken.ACR, (claim, mapperName, token, value) -> { token.setAcr(value.toString()); }); + tmpToken.put(IDToken.AUTH_TIME, (claim, mapperName, token, value) -> { + try { + token.setAuth_time(Long.parseLong(value.toString())); + } catch (NumberFormatException ignored){ + + } + }); tmpToken.put("aud", (claim, mapperName, token, value) -> { if (value instanceof Collection) { String[] audiences = ((Collection) value).stream().map(Object::toString).toArray(String[]::new); @@ -129,7 +136,6 @@ public class OIDCAttributeMapperHelper { tmpToken.put("iss", notAllowedInToken); tmpToken.put("scope", notAllowedInToken); tmpToken.put(IDToken.NONCE, notAllowedInToken); - tmpToken.put(IDToken.AUTH_TIME, notAllowedInToken); tmpToken.put(IDToken.SESSION_STATE, notAllowedInToken); tokenPropertySetters = Collections.unmodifiableMap(tmpToken); diff --git a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java index 74a8d5b0b2..e1fb959617 100755 --- a/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java +++ b/services/src/main/java/org/keycloak/services/migration/DefaultMigrationProvider.java @@ -107,6 +107,11 @@ public class DefaultMigrationProvider implements MigrationProvider { getOIDCLoginProtocolFactory().addAcrClientScope(realm); } + @Override + public ClientScopeModel addOIDCBasicClientScope(RealmModel realm) { + return getOIDCLoginProtocolFactory().addBasicClientScope(realm); + } + @Override public void close() { } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index a11e48c592..4fe1185d9f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -724,6 +724,7 @@ public class ExportImportUtil { OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE, OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE, OIDCLoginProtocolFactory.ACR_SCOPE, + OIDCLoginProtocolFactory.BASIC_SCOPE, SamlProtocolFactory.SCOPE_ROLE_LIST )); @@ -745,7 +746,8 @@ public class ExportImportUtil { OAuth2Constants.SCOPE_EMAIL, OIDCLoginProtocolFactory.ROLES_SCOPE, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE, - OIDCLoginProtocolFactory.ACR_SCOPE + OIDCLoginProtocolFactory.ACR_SCOPE, + OIDCLoginProtocolFactory.BASIC_SCOPE )); Set optionalClientScopes = realm.getDefaultOptionalClientScopes() diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 0aff7a62c2..e6c2078e93 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -418,6 +418,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { } } + protected void testMigrationTo25_0_0() { + // check that all expected scopes exist in the migrated realm. + testRealmDefaultClientScopes(migrationRealm); + } + protected void testDeleteAccount(RealmResource realm) { ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); ClientResource accountResource = realm.clients().get(accountClient.getId()); @@ -1110,6 +1115,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMigrationTo24_0_0(testUserProfileMigration, testLdapUseTruststoreSpiMigration); } + protected void testMigrationTo25_x() { + testMigrationTo25_0_0(); + } + protected void testMigrationTo7_x(boolean supportedAuthzServices) { if (supportedAuthzServices) { testDecisionStrategySetOnResourceServer(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java index 2daa2d14a3..39c83f5b82 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1903MigrationTest.java @@ -68,6 +68,7 @@ public class JsonFileImport1903MigrationTest extends AbstractJsonFileImportMigra testMigrationTo22_x(); testMigrationTo23_x(true); testMigrationTo24_x(true, true); + testMigrationTo25_0_0(); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java index 68b2433d0c..3e903071c0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java @@ -79,6 +79,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo22_x(); testMigrationTo23_x(false); testMigrationTo24_x(false); + testMigrationTo25_0_0(); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java index 4a2d2fbe28..74c3aa0030 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java @@ -73,6 +73,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo22_x(); testMigrationTo23_x(false); testMigrationTo24_x(false); + testMigrationTo25_0_0(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java index e38a8058cd..6085b2120b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java @@ -68,6 +68,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo22_x(); testMigrationTo23_x(false); testMigrationTo24_x(false); + testMigrationTo25_0_0(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java index 0b04026431..ff5efa54a3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java @@ -62,6 +62,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo22_x(); testMigrationTo23_x(false); testMigrationTo24_x(false); + testMigrationTo25_0_0(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java index f6a8f3fcb6..ae363b77b0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java @@ -55,6 +55,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo22_x(); testMigrationTo23_x(false); testMigrationTo24_x(false); + testMigrationTo25_0_0(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 22c5edbadb..58b0bc754a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -70,5 +70,6 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo22_x(); testMigrationTo23_x(true); testMigrationTo24_x(true, true); + testMigrationTo25_0_0(); } } 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 a24158e46c..b61c6d62f4 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 @@ -137,7 +137,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, false); + assertAccessToken(oauth.verifyToken(accessToken), true, false, false); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -158,7 +158,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, false); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -180,7 +180,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, false); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -204,7 +204,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); logger.debug("idtoken:" + response.getIdToken()); - assertAccessToken(oauth.verifyToken(accessToken), true, false); + assertAccessToken(oauth.verifyToken(accessToken), true, false, false); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); removeSession(ctx.userSessionId); @@ -227,7 +227,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest(TEST_CLIENT_SECRET); String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), false, false); + assertAccessToken(oauth.verifyToken(accessToken), false, false, true); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -249,7 +249,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); logger.debug("accessToken:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, false); + assertAccessToken(oauth.verifyToken(accessToken), true, false, false); response = oauth.doTokenExchange(TEST, accessToken, null, TEST_CLIENT, TEST_CLIENT_SECRET); String exchangedTokenString = response.getAccessToken(); logger.debug("exchangedTokenString:" + exchangedTokenString); @@ -276,7 +276,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { 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(); - assertAccessToken(oauth.verifyToken(accessToken), true, false); + assertAccessToken(oauth.verifyToken(accessToken), true, false, true); logger.debug("lightweight access token:" + accessToken); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); @@ -292,7 +292,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); accessToken = tokenResponse.getAccessToken(); logger.debug("access token:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, false); } finally { deleteProtocolMappers(protocolMappers); } @@ -312,7 +312,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); String accessToken = tokenResponse.getAccessToken(); logger.debug("access token:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, true); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String introspectResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -337,7 +337,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { 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(); - assertAccessToken(oauth.verifyToken(accessToken), true, false); + assertAccessToken(oauth.verifyToken(accessToken), true, false, true); logger.debug("lightweight access token:" + accessToken); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); @@ -353,7 +353,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); accessToken = tokenResponse.getAccessToken(); logger.debug("access token:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, false); } finally { deleteProtocolMappers(protocolMappers); } @@ -373,7 +373,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(authsEndpointResponse.getCode(), TEST_CLIENT_SECRET); String accessToken = tokenResponse.getAccessToken(); logger.debug("access token:" + accessToken); - assertAccessToken(oauth.verifyToken(accessToken), true, true); + assertAccessToken(oauth.verifyToken(accessToken), true, true, true); oauth.clientId(RESOURCE_SERVER_CLIENT_ID); String introspectResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); @@ -441,10 +441,8 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { Assert.assertNotNull(token.getType()); if (isAuthCodeFlow) { Assert.assertNotNull(token.getSessionId()); - Assert.assertNotNull(token.getAuth_time()); } else { Assert.assertNull(token.getSessionId()); - Assert.assertNull(token.getAuth_time()); } Assert.assertNotNull(token.getIssuedFor()); Assert.assertNotNull(token.getScope()); @@ -452,16 +450,25 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { Assert.assertNotNull(token.getSubject()); } + private void assertBasicClaims(AccessToken token, boolean missing) { + if (missing) { + Assert.assertNull(token.getAuth_time()); + } else { + Assert.assertNotNull(token.getAuth_time()); + } + } + private void assertIntrospectClaims(AccessToken token) { Assert.assertNotNull(token.getOtherClaims().get("client_id")); Assert.assertNotNull(token.getOtherClaims().get("active")); Assert.assertNotNull(token.getOtherClaims().get("token_type")); } - private void assertAccessToken(AccessToken token, boolean isAuthCodeFlow, boolean isAddToAccessToken) { + private void assertAccessToken(AccessToken token, boolean isAuthCodeFlow, boolean isAddToAccessToken, boolean missingBasicClaims) { Assert.assertNull(token.getNonce()); assertMapperClaims(token, isAddToAccessToken, isAuthCodeFlow); assertInitClaims(token, isAuthCodeFlow); + assertBasicClaims(token, missingBasicClaims); } private void assertTokenIntrospectionResponse(AccessToken token, boolean isAuthCodeFlow) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index 11e0b9c236..c19152e27d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -421,7 +421,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { private void assertScopesSupportedMatchesWithRealm(OIDCConfigurationRepresentation oidcConfig) { Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS, - OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS, OIDCLoginProtocolFactory.ACR_SCOPE, + OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS, OIDCLoginProtocolFactory.ACR_SCOPE, OIDCLoginProtocolFactory.BASIC_SCOPE, OIDCLoginProtocolFactory.ROLES_SCOPE, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE, OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json index 4301f7cd72..037f5b587b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-19.0.3.json @@ -1158,8 +1158,32 @@ "consentRequired" : false, "config" : { } } ] - } ], - "defaultDefaultClientScopes" : [ "roles", "profile", "role_list", "email", "web-origins" ], + }, { + "id": "6e26ea6e-8d9e-4bb4-a335-df23baea8a89", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "7b7b4a03-9b67-4a0c-a154-e36d8b2e251c", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes" : [ "roles", "profile", "role_list", "email", "web-origins", "acr" ], "defaultOptionalClientScopes" : [ "phone", "offline_access", "address", "microprofile-jwt" ], "browserSecurityHeaders" : { "contentSecurityPolicyReportOnly" : "", @@ -2892,7 +2916,7 @@ "config" : { } } ] } ], - "defaultDefaultClientScopes" : [ "web-origins", "roles", "role_list", "email", "profile" ], + "defaultDefaultClientScopes" : [ "web-origins", "roles", "role_list", "email", "profile", "acr" ], "defaultOptionalClientScopes" : [ "address", "offline_access", "microprofile-jwt", "phone" ], "browserSecurityHeaders" : { "contentSecurityPolicyReportOnly" : "", @@ -2953,7 +2977,7 @@ "id": "88cef18c-bcd8-40d2-9e7d-d257298317f2", "providerId": "declarative-user-profile", "subComponents": {}, - "config": { + "config": { "config-piece-0" : [ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}}},{\"name\":\"email\",\"displayName\":\"${email}\",\"permissions\":{\"edit\":[\"admin\",\"user\"],\"view\":[\"admin\",\"user\"]},\"validations\":{\"email\":{},\"length\":{\"max\":255},\"pattern\":{\"pattern\":\"[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~.-]+@example.nl\",\"error-message\":\"Invalid domain selected\"}},\"annotations\":{\"\":\"\"},\"required\":{\"roles\":[\"user\"]},\"group\":null},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}}},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}}}]}" ], "config-pieces-count" : [ "1" ] } @@ -5140,7 +5164,7 @@ } } ] } ], - "defaultDefaultClientScopes" : [ "profile", "role_list", "roles", "web-origins", "email" ], + "defaultDefaultClientScopes" : [ "profile", "role_list", "roles", "web-origins", "email", "acr" ], "defaultOptionalClientScopes" : [ "offline_access", "phone", "microprofile-jwt", "address" ], "browserSecurityHeaders" : { "contentSecurityPolicyReportOnly" : "",