From 67e73d3d4e1f87f210230d4751c114cdd863d049 Mon Sep 17 00:00:00 2001 From: shigeyuki kabano Date: Thu, 14 Dec 2023 19:07:22 +0900 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFEnhancing=20Lightweight=20access=20tok?= =?UTF-8?q?en=20M2(keycloak#25716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes keycloak#23724 Signed-off-by: shigeyuki kabano --- .../admin/messages/messages_en.properties | 4 +- .../java/org/keycloak/models/Constants.java | 1 + .../mappers/AbstractOIDCProtocolMapper.java | 11 +- .../AllowedWebOriginsProtocolMapper.java | 5 +- .../AudienceResolveProtocolMapper.java | 4 +- .../mappers/OIDCAttributeMapperHelper.java | 18 + .../UseLightweightAccessTokenExecutor.java | 51 +++ ...LightweightAccessTokenExecutorFactory.java | 70 ++++ ...ecutor.ClientPolicyExecutorProviderFactory | 3 +- .../oidc/LightWeightAccessTokenTest.java | 312 ++++++++++++------ 10 files changed, 378 insertions(+), 101 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutor.java create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutorFactory.java diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 5d9af57f06..3287ad4652 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -2968,4 +2968,6 @@ referralHelp=Specifies if LDAP referrals should be followed or ignored. Please n authenticatorRefConfig.value.help=Add a custom reference name for the authenticator. When this authenticator is successfully completed during an authentication flow, the Authentication Method Reference (AMR) protocol mapper will use this value to populate the amr claim of the generated tokens. Note, the AMR protocol must be configured for the given client to populate the AMR claim authenticatorRefConfig.value.label=Authenticator Reference authenticatorRefConfig.maxAge.help=The max age in seconds that the authenticator reference value is good for in an SSO session. When the Authentication Method Reference (AMR) protocol mapper is used, the AMR will only be considered valid and populated in the token if the authenticator execution was completed within the specified max age. -authenticatorRefConfig.maxAge.label=Authenticator Reference Max Age \ No newline at end of file +authenticatorRefConfig.maxAge.label=Authenticator Reference Max Age +includeInLightweight.label=Add to lightweight access token +includeInLightweight.tooltip=Should the claim be added to the lightweight access token? diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index ade8f51012..d949f40cf3 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -163,4 +163,5 @@ public final class Constants { public static final String SESSION_NOTE_LIGHTWEIGHT_USER = "keycloak.userModel"; + public static final String USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED = "client.use.lightweight.access.token.enabled"; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java index 7eaecdaa37..37bb385963 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java @@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.Config; import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ProtocolMapperModel; @@ -80,10 +81,16 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { return token; } + boolean getShouldUseLightweightToken(KeycloakSession session) { + Object attributeValue = session.getAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED); + return (attributeValue != null) ? (boolean) attributeValue : false; + } + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - - if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)){ + boolean shouldUseLightweightToken = getShouldUseLightweightToken(session); + boolean includeInAccessToken = shouldUseLightweightToken ? OIDCAttributeMapperHelper.includeInLightweightAccessToken(mappingModel) : OIDCAttributeMapperHelper.includeInAccessToken(mappingModel); + if (!includeInAccessToken){ return token; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java index 23310d6426..85d3e70334 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java @@ -83,8 +83,9 @@ public class AllowedWebOriginsProtocolMapper extends AbstractOIDCProtocolMapper @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - - if (!includeInAccessToken(mappingModel)){ + boolean shouldUseLightweightToken = getShouldUseLightweightToken(session); + boolean includeInAccessToken = shouldUseLightweightToken ? OIDCAttributeMapperHelper.includeInLightweightAccessToken(mappingModel) : includeInAccessToken(mappingModel); + if (!includeInAccessToken){ return token; } setWebOrigin(token, session, clientSessionCtx); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java index e4fd065922..055a2d5d79 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceResolveProtocolMapper.java @@ -88,7 +88,9 @@ public class AudienceResolveProtocolMapper extends AbstractOIDCProtocolMapper im @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { - if (!includeInAccessToken(mappingModel)){ + boolean shouldUseLightweightToken = getShouldUseLightweightToken(session); + boolean includeInAccessToken = shouldUseLightweightToken ? OIDCAttributeMapperHelper.includeInLightweightAccessToken(mappingModel) : includeInAccessToken(mappingModel); + if (!includeInAccessToken){ return token; } setAudience(token, clientSessionCtx, session); 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 c15037cc82..3538044ed1 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 @@ -71,6 +71,12 @@ public class OIDCAttributeMapperHelper { public static final String INCLUDE_IN_INTROSPECTION_LABEL = "includeInIntrospection.label"; public static final String INCLUDE_IN_INTROSPECTION_HELP_TEXT = "includeInIntrospection.tooltip"; + public static final String INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN = "lightweight.claim"; + + public static final String INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN_LABEL = "includeInLightweight.label"; + + public static final String INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN_HELP_TEXT = "includeInLightweight.tooltip"; + private static final Logger logger = Logger.getLogger(OIDCAttributeMapperHelper.class); /** @@ -392,6 +398,10 @@ public class OIDCAttributeMapperHelper { return "true".equals(includeInIntrospection); } + public static boolean includeInLightweightAccessToken(ProtocolMapperModel mappingModel) { + return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN)); + } + public static void addAttributeConfig(List configProperties, Class protocolMapperClass) { addTokenClaimNameConfig(configProperties); addJsonTypeConfig(configProperties); @@ -443,6 +453,14 @@ public class OIDCAttributeMapperHelper { property.setDefaultValue("true"); property.setHelpText(INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT); configProperties.add(property); + + ProviderConfigProperty property2 = new ProviderConfigProperty(); + property2.setName(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN); + property2.setLabel(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN_LABEL); + property2.setType(ProviderConfigProperty.BOOLEAN_TYPE); + property2.setDefaultValue("false"); + property2.setHelpText(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN_HELP_TEXT); + configProperties.add(property2); } if (UserInfoTokenMapper.class.isAssignableFrom(protocolMapperClass)) { diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutor.java new file mode 100644 index 0000000000..f12e88c179 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutor.java @@ -0,0 +1,51 @@ +/* + * 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.services.clientpolicy.executor; + +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; + +public class UseLightweightAccessTokenExecutor implements ClientPolicyExecutorProvider { + private final KeycloakSession session; + + public UseLightweightAccessTokenExecutor(KeycloakSession session) { + this.session = session; + } + + @Override + public String getProviderId() { + return UseLightweightAccessTokenExecutorFactory.PROVIDER_ID; + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case TOKEN_REQUEST: + case TOKEN_REFRESH: + case RESOURCE_OWNER_PASSWORD_CREDENTIALS_REQUEST: + case SERVICE_ACCOUNT_TOKEN_REQUEST: + case BACKCHANNEL_TOKEN_REQUEST: + case DEVICE_TOKEN_REQUEST: + session.setAttribute(Constants.USE_LIGHTWEIGHT_ACCESS_TOKEN_ENABLED, true); + break; + } + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutorFactory.java new file mode 100644 index 0000000000..e5b0784676 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/UseLightweightAccessTokenExecutorFactory.java @@ -0,0 +1,70 @@ +/* + * 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.services.clientpolicy.executor; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.Collections; +import java.util.List; + +public class UseLightweightAccessTokenExecutorFactory implements ClientPolicyExecutorProviderFactory { + public static final String PROVIDER_ID = "use-lightweight-access-token"; + + @Override + public String getHelpText() { + return "Use lightweight access token"; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session) { + return new UseLightweightAccessTokenExecutor(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public boolean isSupported() { + return true; + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory index 81c292527b..4155a7ca85 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory @@ -22,4 +22,5 @@ org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutor org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory -org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory +org.keycloak.services.clientpolicy.executor.UseLightweightAccessTokenExecutorFactory 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 63762cbd79..4ac04c5cbc 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 @@ -1,6 +1,25 @@ +/* + * 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.testsuite.oidc; import jakarta.ws.rs.NotFoundException; +import org.jboss.logging.Logger; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ClientScopeResource; @@ -23,15 +42,17 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory; +import org.keycloak.services.clientpolicy.executor.UseLightweightAccessTokenExecutorFactory; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.client.policies.AbstractClientPoliciesTest; import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.ClientPoliciesUtil; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.ProtocolMapperUtil; import org.keycloak.util.JsonSerialization; -import org.wildfly.common.Assert; import java.io.IOException; import java.util.ArrayList; @@ -62,25 +83,29 @@ import static org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper.INCLUDED import static org.keycloak.protocol.oidc.mappers.HardcodedClaim.CLAIM_VALUE; import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN; import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION; +import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN; import static org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper.PAIRWISE_SUB_ALGORITHM_SALT; import static org.keycloak.protocol.oidc.mappers.RoleNameMapper.NEW_ROLE_NAME; import static org.keycloak.protocol.oidc.mappers.RoleNameMapper.ROLE_CONFIG; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig; @EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true) -public class LightWeightAccessTokenTest extends AbstractKeycloakTest { - +public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest { + private static final Logger logger = Logger.getLogger(LightWeightAccessTokenTest.class); + private static String RESOURCE_SERVER_CLIENT_ID = "resource-server"; + private static String RESOURCE_SERVER_CLIENT_PASSWORD = "password"; @Before public void clientConfiguration() { - ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true).setServiceAccountsEnabled(true); - ClientManager.realm(adminClient.realm("test")).clientId("resource-server").directAccessGrant(true); + ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(TEST_CLIENT).directAccessGrant(true).setServiceAccountsEnabled(true); + ClientManager.realm(adminClient.realm(REALM_NAME)).clientId(RESOURCE_SERVER_CLIENT_ID).directAccessGrant(true); } @Override public void addTestRealms(List testRealms) { RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); - UserRepresentation user = findUser(realm, "test-user@localhost"); + UserRepresentation user = findUser(realm, TEST_USER_NAME); Map> attributes = new HashMap<>(){{ put("street", Arrays.asList("1 My Street")); put("locality", Arrays.asList("Cardiff")); @@ -89,8 +114,8 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { }}; user.setAttributes(attributes); user.setGroups(Arrays.asList("/topGroup/level2group")); - ClientRepresentation confApp = KeycloakModelUtils.createClient(realm, "resource-server"); - confApp.setSecret("password"); + ClientRepresentation confApp = KeycloakModelUtils.createClient(realm, RESOURCE_SERVER_CLIENT_ID); + confApp.setSecret(RESOURCE_SERVER_CLIENT_PASSWORD); confApp.setServiceAccountsEnabled(Boolean.TRUE); testRealms.add(realm); } @@ -99,7 +124,6 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { for (UserRepresentation user : testRealm.getUsers()) { if (user.getUsername().equals(userName)) return user; } - return null; } @@ -109,15 +133,15 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { try { oauth.nonce("123456"); oauth.scope("address"); - oauth.clientId("test-app"); - OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password").tokenResponse; + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); + logger.debug("accessToken:" + accessToken); assertAccessToken(oauth.verifyToken(accessToken), true, false); - oauth.clientId("resource-server"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", accessToken); - System.out.println("tokenResponse:" + tokenResponse); + 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), true, true, false); } finally { deleteProtocolMappers(protocolMappers); @@ -130,15 +154,15 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { try { oauth.nonce("123456"); oauth.scope("address"); - oauth.clientId("test-app"); - OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password").tokenResponse; + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); + logger.debug("accessToken:" + accessToken); assertAccessToken(oauth.verifyToken(accessToken), true, true); - oauth.clientId("resource-server"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", accessToken); - System.out.println("tokenResponse:" + tokenResponse); + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + logger.debug("tokenResponse:" + tokenResponse); // Most of the claims should not be included in introspectionResponse as introspectionMapper was disabled assertTokenIntrospectionResponse(JsonSerialization.readValue(tokenResponse, AccessToken.class), true, false, false); } finally { @@ -152,15 +176,15 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { try { oauth.nonce("123456"); oauth.scope("address"); - oauth.clientId("test-app"); - OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password").tokenResponse; + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); + logger.debug("accessToken:" + accessToken); assertAccessToken(oauth.verifyToken(accessToken), true, true); - oauth.clientId("resource-server"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", accessToken); - System.out.println("tokenResponse:" + tokenResponse); + 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), true, true, false); } finally { deleteProtocolMappers(protocolMappers); @@ -174,33 +198,24 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { oauth.nonce("123456"); oauth.scope("openid address offline_access"); - oauth.clientId("test-app"); - TokenResponseContext ctx = browserLogin("password", "test-user@localhost", "password"); + oauth.clientId(TEST_CLIENT); + TokenResponseContext ctx = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD); OAuthClient.AccessTokenResponse response = ctx.tokenResponse; String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); - System.out.println("idtoken:" + response.getIdToken()); + logger.debug("accessToken:" + accessToken); + logger.debug("idtoken:" + response.getIdToken()); assertAccessToken(oauth.verifyToken(accessToken), true, false); - oauth.clientId("resource-server"); + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); removeSession(ctx.userSessionId); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", accessToken); - System.out.println("tokenResponse:" + tokenResponse); + String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + logger.debug("tokenResponse:" + tokenResponse); assertTokenIntrospectionResponse(JsonSerialization.readValue(tokenResponse, AccessToken.class), true, true, false); } finally { deleteProtocolMappers(protocolMappers); } } - private void removeSession(final String sessionId) { - testingClient.testing().removeExpired("test"); - try { - testingClient.testing().removeUserSession("test", sessionId); - } catch (NotFoundException nfe) { - // Ignore - } - } - @Test public void clientCredentialTest() throws Exception { ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, false); @@ -208,15 +223,15 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { oauth.nonce("123456"); oauth.scope("address"); - oauth.clientId("test-app"); - OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("password"); + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest(TEST_CLIENT_SECRET); String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); + logger.debug("accessToken:" + accessToken); assertAccessToken(oauth.verifyToken(accessToken), false, false); - oauth.clientId("resource-server"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", accessToken); - System.out.println("tokenResponse:" + tokenResponse); + 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); } finally { deleteProtocolMappers(protocolMappers); @@ -230,24 +245,94 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { oauth.nonce("123456"); oauth.scope("address"); - oauth.clientId("test-app"); - OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password").tokenResponse; + oauth.clientId(TEST_CLIENT); + OAuthClient.AccessTokenResponse response = browserLogin(TEST_CLIENT_SECRET, TEST_USER_NAME, TEST_USER_PASSWORD).tokenResponse; String accessToken = response.getAccessToken(); - System.out.println("accessToken:" + accessToken); + logger.debug("accessToken:" + accessToken); assertAccessToken(oauth.verifyToken(accessToken), true, false); - response = oauth.doTokenExchange(TEST, accessToken, null, "test-app", "password"); + response = oauth.doTokenExchange(TEST, accessToken, null, TEST_CLIENT, TEST_CLIENT_SECRET); String exchangedTokenString = response.getAccessToken(); - System.out.println("exchangedTokenString:" + exchangedTokenString); + logger.debug("exchangedTokenString:" + exchangedTokenString); - oauth.clientId("resource-server"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("resource-server", "password", exchangedTokenString); - System.out.println("tokenResponse:" + tokenResponse); + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + String tokenResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, exchangedTokenString); + logger.debug("tokenResponse:" + tokenResponse); assertTokenIntrospectionResponse(JsonSerialization.readValue(tokenResponse, AccessToken.class), true, true, true); } finally { deleteProtocolMappers(protocolMappers); } } + @Test + public void testPolicyLightWeightFalseTest() throws Exception { + setUseLightweightAccessTokenExecutor(); + ProtocolMappersResource protocolMappers = setProtocolMappers(true, true, false, true); + try { + oauth.nonce("123456"); + oauth.scope("address"); + + oauth.clientId(TEST_CLIENT); + + 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); + logger.debug("lightweight access token:" + accessToken); + + oauth.clientId(RESOURCE_SERVER_CLIENT_ID); + String introspectResponse = oauth.introspectAccessTokenWithClientCredential(RESOURCE_SERVER_CLIENT_ID, RESOURCE_SERVER_CLIENT_PASSWORD, accessToken); + assertTokenIntrospectionResponse(JsonSerialization.readValue(introspectResponse, AccessToken.class), true, true, false); + logger.debug("tokenResponse:" + introspectResponse); + + oauth.clientId(TEST_CLIENT); + deletePolicy(POLICY_NAME); + 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); + } finally { + deleteProtocolMappers(protocolMappers); + } + } + + @Test + public void testPolicyLightWeightTrueTest() throws Exception { + setUseLightweightAccessTokenExecutor(); + ProtocolMappersResource protocolMappers = setProtocolMappers(false, true, true, true); + try { + oauth.nonce("123456"); + oauth.scope("address"); + + oauth.clientId(TEST_CLIENT); + + 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, 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, false); + + } finally { + deleteProtocolMappers(protocolMappers); + } + } + + private void removeSession(final String sessionId) { + testingClient.testing().removeExpired(REALM_NAME); + try { + testingClient.testing().removeUserSession(REALM_NAME, sessionId); + } catch (NotFoundException nfe) { + // Ignore + } + } + private void assertMapperClaims(AccessToken token, boolean isAddMapperResponseFlag, boolean isAuthCodeFlow) { if (isAddMapperResponseFlag) { if (isAuthCodeFlow) { @@ -269,21 +354,21 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { Assert.assertNotNull(token.getPreferredUsername()); } else { if (isAuthCodeFlow) { - Assert.assertTrue(token.getName() == null); - Assert.assertTrue(token.getGivenName() == null); - Assert.assertTrue(token.getFamilyName() == null); - Assert.assertTrue(token.getAddress() == null); - Assert.assertTrue(token.getEmail() == null); - Assert.assertTrue(token.getOtherClaims().get("user-session-note") == null); - Assert.assertTrue(token.getOtherClaims().get("test-claim") == null); - Assert.assertTrue(token.getOtherClaims().get("group-name") == null); + Assert.assertNull(token.getName()); + Assert.assertNull(token.getGivenName()); + Assert.assertNull(token.getFamilyName()); + Assert.assertNull(token.getAddress()); + Assert.assertNull(token.getEmail()); + Assert.assertNull(token.getOtherClaims().get("user-session-note")); + Assert.assertNull(token.getOtherClaims().get("test-claim")); + Assert.assertNull(token.getOtherClaims().get("group-name")); } - Assert.assertTrue(token.getAcr() == null); - Assert.assertTrue(token.getAllowedOrigins() == null); - Assert.assertTrue(token.getRealmAccess() == null); + Assert.assertNull(token.getAcr()); + Assert.assertNull(token.getAllowedOrigins()); + Assert.assertNull(token.getRealmAccess()); Assert.assertTrue(token.getResourceAccess().isEmpty()); - Assert.assertTrue(token.getEmailVerified() == null); - Assert.assertTrue(token.getPreferredUsername() == null); + Assert.assertNull(token.getEmailVerified()); + Assert.assertNull(token.getPreferredUsername()); } } @@ -296,12 +381,13 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { Assert.assertNotNull(token.getSessionId()); Assert.assertNotNull(token.getAuth_time()); } else { - Assert.assertTrue(token.getSessionId() == null); - Assert.assertTrue(token.getAuth_time() == null); + Assert.assertNull(token.getSessionId()); + Assert.assertNull(token.getAuth_time()); } Assert.assertNotNull(token.getIssuedFor()); Assert.assertNotNull(token.getScope()); Assert.assertNotNull(token.getIssuer()); + Assert.assertNotNull(token.getSubject()); } private void assertIntrospectClaims(AccessToken token) { @@ -314,7 +400,7 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { if (isAuthCodeFlow && !exchangeToken) { Assert.assertNotNull(token.getNonce()); } else { - Assert.assertTrue(token.getNonce() == null); + Assert.assertNull(token.getNonce()); } } @@ -336,25 +422,25 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { } protected RealmResource testRealm() { - return adminClient.realm("test"); + return adminClient.realm(REALM_NAME); } - private void setScopeProtocolMappers(boolean isIncludeAccessToken, boolean isIncludeIntrospection) { - setScopeProtocolMapper(ACR_SCOPE, ACR, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(PROFILE_CLAIM, FULL_NAME, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(EMAIL, EMAIL, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(EMAIL, EMAIL_VERIFIED, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(PROFILE_CLAIM, GIVEN_NAME, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(PROFILE_CLAIM, FAMILY_NAME, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(PROFILE_CLAIM, USERNAME, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(WEB_ORIGINS_SCOPE, ALLOWED_WEB_ORIGINS, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(ROLES_SCOPE, REALM_ROLES, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(ROLES_SCOPE, CLIENT_ROLES, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(ROLES_SCOPE, AUDIENCE_RESOLVE, isIncludeAccessToken, isIncludeIntrospection); - setScopeProtocolMapper(ADDRESS, ADDRESS, isIncludeAccessToken, isIncludeIntrospection); + private void setScopeProtocolMappers(boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean isIncludeLightweightAccessToken) { + setScopeProtocolMapper(ACR_SCOPE, ACR, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(PROFILE_CLAIM, FULL_NAME, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(EMAIL, EMAIL, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(EMAIL, EMAIL_VERIFIED, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(PROFILE_CLAIM, GIVEN_NAME, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(PROFILE_CLAIM, FAMILY_NAME, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(PROFILE_CLAIM, USERNAME, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(WEB_ORIGINS_SCOPE, ALLOWED_WEB_ORIGINS, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(ROLES_SCOPE, REALM_ROLES, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(ROLES_SCOPE, CLIENT_ROLES, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(ROLES_SCOPE, AUDIENCE_RESOLVE, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + setScopeProtocolMapper(ADDRESS, ADDRESS, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); } - private void setScopeProtocolMapper(String scopeName, String mapperName, boolean isIncludeAccessToken, boolean isIncludeIntrospection) { + private void setScopeProtocolMapper(String scopeName, String mapperName, boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean isIncludeLightweightAccessToken) { ClientScopeResource scope = ApiUtil.findClientScopeByName(testRealm(), scopeName); ProtocolMapperRepresentation protocolMapper = ApiUtil.findProtocolMapperByName(scope, mapperName); Map config = protocolMapper.getConfig(); @@ -368,31 +454,49 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { } else { config.put(INCLUDE_IN_INTROSPECTION, "false"); } + if (isIncludeLightweightAccessToken) { + config.put(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN, "true"); + } else { + config.put(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN, "false"); + } scope.getProtocolMappers().update(protocolMapper.getId(), protocolMapper); } private ProtocolMappersResource setProtocolMappers(boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean setPairWise) { - setScopeProtocolMappers(isIncludeAccessToken, isIncludeIntrospection); + setScopeProtocolMappers(isIncludeAccessToken, isIncludeIntrospection, false); List protocolMapperList = new ArrayList<>(); - setExistingProtocolMappers(protocolMapperList, isIncludeAccessToken, isIncludeIntrospection, setPairWise); - ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers(); + setExistingProtocolMappers(protocolMapperList, isIncludeAccessToken, isIncludeIntrospection, false, setPairWise); + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), TEST_CLIENT).getProtocolMappers(); protocolMappers.createMapper(protocolMapperList); return protocolMappers; } - private void setExistingProtocolMappers(List protocolMapperList, boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean setPairWise) { + private ProtocolMappersResource setProtocolMappers(boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean isIncludeLightweightAccessToken, boolean setPairWise) { + setScopeProtocolMappers(isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken); + List protocolMapperList = new ArrayList<>(); + setExistingProtocolMappers(protocolMapperList, isIncludeAccessToken, isIncludeIntrospection, isIncludeLightweightAccessToken, setPairWise); + ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), TEST_CLIENT).getProtocolMappers(); + protocolMappers.createMapper(protocolMapperList); + return protocolMappers; + } + + private void setExistingProtocolMappers(List protocolMapperList, boolean isIncludeAccessToken, boolean isIncludeIntrospection, boolean isIncludeLightweightAccessToken, boolean setPairWise) { Map config = new HashMap<>(); if (isIncludeAccessToken) { config.put(INCLUDE_IN_ACCESS_TOKEN, "true"); } else { config.put(INCLUDE_IN_ACCESS_TOKEN, "false"); } - if (isIncludeIntrospection) { config.put(INCLUDE_IN_INTROSPECTION, "true"); } else { config.put(INCLUDE_IN_INTROSPECTION, "false"); } + if (isIncludeLightweightAccessToken) { + config.put(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN, "true"); + } else { + config.put(INCLUDE_IN_LIGHTWEIGHT_ACCESS_TOKEN, "false"); + } ProtocolMapperRepresentation audienceProtocolMapper = createClaimMapper("audience", AudienceProtocolMapper.PROVIDER_ID, new HashMap<>(config) {{ put(INCLUDED_CLIENT_AUDIENCE, "account-console"); @@ -469,4 +573,24 @@ public class LightWeightAccessTokenTest extends AbstractKeycloakTest { this.tokenResponse = tokenResponse; } } + + private void setUseLightweightAccessTokenExecutor() throws Exception { + // register profiles + String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile( + (new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Use Lightweight Access Token") + .addExecutor(UseLightweightAccessTokenExecutorFactory.PROVIDER_ID, null) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // register policies + json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy( + (new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Use Lightweight Access Token Policy", Boolean.TRUE) + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); + updatePolicies(json); + } }