From d424af93d0ba36ebed532132c9d58a50dc565ff7 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Sun, 9 Apr 2023 15:55:01 +0900 Subject: [PATCH] Reference token with external store --- .../AbstractReferenceTypeTokenExecutor.java | 160 +++++++++++ ...ractReferenceTypeTokenExecutorFactory.java | 44 +++ .../executor/ReferenceTypeTokenExecutor.java | 242 +++++++++++++++++ .../ReferenceTypeTokenExecutorFactory.java | 67 +++++ ...ecutor.ClientPolicyExecutorProviderFactory | 3 +- .../rest/TestApplicationResourceProvider.java | 7 +- ...estApplicationResourceProviderFactory.java | 3 +- ...stingOIDCEndpointsApplicationResource.java | 55 +++- .../TestApplicationResourceUrls.java | 14 + .../TestOIDCEndpointsApplicationResource.java | 15 + .../policies/AbstractClientPoliciesTest.java | 13 +- .../client/policies/ClientPoliciesTest.java | 256 +++++++++++++++++- .../testsuite/util/ClientPoliciesUtil.java | 8 + .../base/src/test/resources/log4j.properties | 4 +- 14 files changed, 875 insertions(+), 16 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutor.java create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutorFactory.java create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutor.java create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutorFactory.java diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutor.java new file mode 100644 index 000000000000..1476515577f4 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutor.java @@ -0,0 +1,160 @@ +/* + * 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 java.util.function.Consumer; + +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.protocol.oidc.endpoints.TokenIntrospectionEndpoint; +import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyEvent; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.context.LogoutRequestContext; +import org.keycloak.services.clientpolicy.context.TokenIntrospectContext; +import org.keycloak.services.clientpolicy.context.TokenRefreshContext; +import org.keycloak.services.clientpolicy.context.TokenRefreshResponseContext; +import org.keycloak.services.clientpolicy.context.TokenResponseContext; +import org.keycloak.services.clientpolicy.context.TokenRevokeContext; +import org.keycloak.services.clientpolicy.context.UserInfoRequestContext; + +/** + * @author Takashi Norimatsu + */ +public abstract class AbstractReferenceTypeTokenExecutor implements ClientPolicyExecutorProvider { + + protected static Logger logger = Logger.getLogger(AbstractReferenceTypeTokenExecutor.class); + protected static final String ERR_PASS_THROUGH_INTENTIONALLY = "error_pass_through_intentionally"; + + protected KeycloakSession session; + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + + try { + ClientPolicyEvent event = context.getEvent(); + + if (event.equals(ClientPolicyEvent.TOKEN_RESPONSE)) { + + logger.trace("----- Token Response :: creating reference access/refresh tokens"); + + TokenResponseContext tokenResponseContext = (TokenResponseContext)context; + setReferenceTypeTokenBoundWithSelfcontainedTypeToken(tokenResponseContext.getAccessTokenResponseBuilder()); + + } else if (event.equals(ClientPolicyEvent.TOKEN_REFRESH_RESPONSE)) { + + logger.trace("----- Token Refresh :: creating reference access/refresh tokens"); + + TokenRefreshResponseContext tokenRefreshResponseContext = (TokenRefreshResponseContext)context; + setReferenceTypeTokenBoundWithSelfcontainedTypeToken(tokenRefreshResponseContext.getAccessTokenResponseBuilder()); + + } else if (event.equals(ClientPolicyEvent.TOKEN_REFRESH)) { + + logger.trace("----- Token Refresh :: replace an reference refresh token to an self-contained refresh token"); + + TokenRefreshContext tokenRefreshContext = (TokenRefreshContext)context; + setSelfcontainedTypeTokenBoundWithReferenceTypeToken(tokenRefreshContext.getParams().getFirst(OAuth2Constants.REFRESH_TOKEN), + t -> tokenRefreshContext.getParams().putSingle(OAuth2Constants.REFRESH_TOKEN, t), + null); + + } else if (event.equals(ClientPolicyEvent.TOKEN_INTROSPECT)) { + + logger.trace("----- Token Introspection :: replace an reference token (access or refresh) to an self-contained token"); + + TokenIntrospectContext tokenIntrospectContext = (TokenIntrospectContext)context; + setSelfcontainedTypeTokenBoundWithReferenceTypeToken(tokenIntrospectContext.getParams().getFirst(TokenIntrospectionEndpoint.PARAM_TOKEN), + t -> tokenIntrospectContext.getParams().putSingle(TokenIntrospectionEndpoint.PARAM_TOKEN, t), + null); + + } else if (event.equals(ClientPolicyEvent.USERINFO_REQUEST)) { + + logger.trace("----- UserInfo Request :: replace an reference access token to an self-contained access token"); + + UserInfoRequestContext userInfoRequestContext = (UserInfoRequestContext)context; + setSelfcontainedTypeTokenBoundWithReferenceTypeToken(userInfoRequestContext.getTokenForUserInfo().getToken(), + t -> userInfoRequestContext.getTokenForUserInfo().setToken(t), + null); + + } else if (event.equals(ClientPolicyEvent.TOKEN_REVOKE)) { + + logger.trace("----- Token Revocation :: replace an reference token (access or refresh) to an self-contained token"); + + TokenRevokeContext tokenRevokeContext = (TokenRevokeContext)context; + setSelfcontainedTypeTokenBoundWithReferenceTypeToken(tokenRevokeContext.getParams().getFirst(TokenRevocationEndpoint.PARAM_TOKEN), + t -> tokenRevokeContext.getParams().putSingle(TokenRevocationEndpoint.PARAM_TOKEN, t), + null); + + } else if (event.equals(ClientPolicyEvent.LOGOUT_REQUEST)) { + + logger.trace("----- Legacy Backchannel Logout :: replace an reference refresh token to an self-contained refresh token"); + + LogoutRequestContext logoutRequestContext = (LogoutRequestContext)context; + setSelfcontainedTypeTokenBoundWithReferenceTypeToken(logoutRequestContext.getParams().getFirst(OAuth2Constants.REFRESH_TOKEN), + t -> logoutRequestContext.getParams().putSingle(OAuth2Constants.REFRESH_TOKEN, t), + null); + + } + } catch (ClientPolicyException cpe) { + // not throw an exception intentionally when the error procedure is up to the caller endpoint class. + if (!ERR_PASS_THROUGH_INTENTIONALLY.contains(cpe.getError())) throw cpe; + } + } + + abstract protected String createReferenceTypeAccessToken(TokenManager.AccessTokenResponseBuilder builder); + + abstract protected String createReferenceTypeRefreshToken(TokenManager.AccessTokenResponseBuilder builder); + + abstract protected String getSelfcontainedTypeToken(String referenceTypeToken, ClientPolicyException exceptionOnInvalidToken) throws ClientPolicyException ; + + abstract protected void bindSelfcontainedTypeToken(String selfcontainedTypeToken, String referenceTypeToken) throws ClientPolicyException; + + private void setReferenceTypeTokenBoundWithSelfcontainedTypeToken(TokenManager.AccessTokenResponseBuilder builder) throws ClientPolicyException { + + String referenceTypeAccessToken = createReferenceTypeAccessToken(builder); + String referenceTypeRefreshToken = createReferenceTypeRefreshToken(builder); + + logger.tracev("----- referenceTypeAccessToken = {0}, referenceTypeRefreshToken = {1}", referenceTypeAccessToken, referenceTypeRefreshToken); + + AccessTokenResponse from = builder.build(); + + // access token + logger.trace("----- Binding reference access token"); + bindSelfcontainedTypeToken(from.getToken(), referenceTypeAccessToken); + + // refresh token + logger.trace("----- Binding reference refresh token"); + bindSelfcontainedTypeToken(from.getRefreshToken(), referenceTypeRefreshToken); + + from.setToken(referenceTypeAccessToken); + from.setRefreshToken(referenceTypeRefreshToken); + } + + private void setSelfcontainedTypeTokenBoundWithReferenceTypeToken(String referenceTypeToken, Consumer setSelfcontainedTypeToken, ClientPolicyException exceptionOnInvalidToken) throws ClientPolicyException { + + logger.tracev("----- referenceTypeToken = {0}", referenceTypeToken); + String selfcontainedTypeToken = getSelfcontainedTypeToken(referenceTypeToken, exceptionOnInvalidToken); + + logger.tracev("----- selfcontainedTypeToken = {0}", selfcontainedTypeToken); + setSelfcontainedTypeToken.accept(selfcontainedTypeToken); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutorFactory.java new file mode 100644 index 000000000000..c3d9fe1a13e7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/AbstractReferenceTypeTokenExecutorFactory.java @@ -0,0 +1,44 @@ +/* + * 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.Scope; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Takashi Norimatsu + */ +public abstract class AbstractReferenceTypeTokenExecutorFactory implements ClientPolicyExecutorProviderFactory { + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public boolean isSupported() { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutor.java new file mode 100644 index 000000000000..4981f84f46f6 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutor.java @@ -0,0 +1,242 @@ +/* + * 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 java.io.IOException; +import java.io.Serializable; +import java.util.Optional; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.Response.Status; + +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyException; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Takashi Norimatsu + */ +public class ReferenceTypeTokenExecutor extends AbstractReferenceTypeTokenExecutor { + + private static final Logger logger = Logger.getLogger(ReferenceTypeTokenExecutor.class); + + private Configuration configuration; + + public ReferenceTypeTokenExecutor(KeycloakSession session) { + this.session = session; + } + + @Override + public void setupConfiguration(ReferenceTypeTokenExecutor.Configuration config) { + this.configuration = Optional.ofNullable(config).orElse(createDefaultConfiguration()); + } + + @Override + public Class getExecutorConfigurationClass() { + return Configuration.class; + } + + public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation { + + @JsonProperty(ReferenceTypeTokenExecutorFactory.SELFCONTAINED_TYPE_TOKEN_BIND_ENDPOINT) + protected String selfcontainedTypeTokenBindEndpoint; + + @JsonProperty(ReferenceTypeTokenExecutorFactory.SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT) + protected String selfcontainedTypeTokenGetEndpoint; + + public String getSelfcontainedTypeTokenBindEndpoint() { + return selfcontainedTypeTokenBindEndpoint; + } + + public void setSelfcontainedTypeTokenBindEndpoint(String selfcontainedTypeTokenBindEndpoint) { + this.selfcontainedTypeTokenBindEndpoint = selfcontainedTypeTokenBindEndpoint; + } + + public String getSelfcontainedTypeTokenGetEndpoint() { + return selfcontainedTypeTokenGetEndpoint; + } + + public void setSelfcontainedTypeTokenGetEndpoint(String selfcontainedTypeTokenGetEndpoint) { + this.selfcontainedTypeTokenGetEndpoint = selfcontainedTypeTokenGetEndpoint; + } + } + + private Configuration createDefaultConfiguration() { + Configuration conf = new Configuration(); + return conf; + } + + @Override + public String getProviderId() { + return ReferenceTypeTokenExecutorFactory.PROVIDER_ID; + } + + @Override + protected String createReferenceTypeAccessToken(TokenManager.AccessTokenResponseBuilder builder) { + return builder.getAccessToken().getId(); + } + + @Override + protected String createReferenceTypeRefreshToken(TokenManager.AccessTokenResponseBuilder builder) { + return builder.getRefreshToken().getId(); + } + + @Override + protected String getSelfcontainedTypeToken(String referenceTypeToken, ClientPolicyException exceptionOnInvalidToken) throws ClientPolicyException { + + if (referenceTypeToken == null || referenceTypeToken.isEmpty()) { + logger.warnv("no reference type token."); + if (exceptionOnInvalidToken != null) { + throw exceptionOnInvalidToken; + } else { + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + } + + if (!isValidSelfcontainedTypeTokenStoreUrl(configuration.getSelfcontainedTypeTokenGetEndpoint())) { + logger.warnv("getting self-contained token failed due to invalid self-contained type token get endpoint configuration."); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + UriBuilder uri = UriBuilder.fromUri(configuration.getSelfcontainedTypeTokenGetEndpoint()) + .queryParam(ReferenceTypeTokenExecutorFactory.SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_QUERY_PARAM, referenceTypeToken); + final String url = uri.build().toString(); + ReferenceTypeTokenBindResponse response = null; + + try { + SimpleHttp simpleHttp = SimpleHttp.doGet(url, session); + SimpleHttp.Response res = simpleHttp.asResponse(); + + if (res.getStatus() == Status.BAD_REQUEST.getStatusCode()) { + logger.warnv("getting self-contained token failed. referenceTypeToken = {0}", referenceTypeToken); + if (exceptionOnInvalidToken != null) { + throw exceptionOnInvalidToken; + } else { + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + } else if (res.getStatus() != Status.OK.getStatusCode()) { + String error = Optional.ofNullable(res.asJson()).map(t->t.get(OAuth2Constants.ERROR_DESCRIPTION)).map(t->t.asText()).orElse(null); + logger.warnv("getting self-contained token failed due to internal problems. error = {0}", error); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + response = res.asJson(ReferenceTypeTokenBindResponse.class); + + } catch (IOException ioe) { + logger.warnv("getting self-contained token failed due to network errror. error = {0}", ioe.getMessage()); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + return response.getSelfcontainedTypeToken(); + } + + @Override + protected void bindSelfcontainedTypeToken(String selfcontainedTypeToken, String referenceTypeToken) throws ClientPolicyException { + + if (!isValidBindRequest(selfcontainedTypeToken, referenceTypeToken)) { + logger.warnv("invalid bind tokens request"); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + if (!isValidSelfcontainedTypeTokenStoreUrl(configuration.getSelfcontainedTypeTokenBindEndpoint())) { + logger.warnv("getting self-contained token failed due to invalid self-contained type token bind endopoint configuration."); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + ReferenceTypeTokenBindRequest request = new ReferenceTypeTokenBindRequest(); + request.setSelfcontainedTypeToken(selfcontainedTypeToken); + request.setReferenceTypeToken(referenceTypeToken); + + logger.tracev("----- BEFORE BIND referenceTypeToken = {0}, selfcontainedTypeToken = {1}", referenceTypeToken, selfcontainedTypeToken); + + try { + SimpleHttp simpleHttp = SimpleHttp.doPost(configuration.getSelfcontainedTypeTokenBindEndpoint(), session) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .json(request); + + if (simpleHttp.asStatus() != Status.NO_CONTENT.getStatusCode()) { + logger.warnv("binding reference type token with self-contained token failed. referenceTypeToken = {0}", referenceTypeToken); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + + } catch (IOException ioe) { + logger.warnv("binding reference type token with self-contained token failed due to network error error = {0}", ioe.getMessage()); + throw new ClientPolicyException(ERR_PASS_THROUGH_INTENTIONALLY); + } + } + + protected boolean isValidBindRequest(String selfcontainedTypeToken, String referenceTypeToken) { + if (selfcontainedTypeToken == null || selfcontainedTypeToken.isEmpty()) return false; + if (referenceTypeToken == null || referenceTypeToken.isEmpty()) return false; + return true; + } + + protected boolean isValidSelfcontainedTypeTokenStoreUrl(String url) { + if (url == null) return false; + if (!url.startsWith("http://") && !url.startsWith("https://")) return false; + return true; + } + + public static class ReferenceTypeTokenBindRequest implements Serializable { + + @JsonProperty("referenceTypeToken") + private String referenceTypeToken; + + @JsonProperty("selfcontainedTypeToken") + private String selfcontainedTypeToken; + + public String getReferenceTypeToken() { + return referenceTypeToken; + } + + public void setReferenceTypeToken(String referenceTypeToken) { + this.referenceTypeToken = referenceTypeToken; + } + + public String getSelfcontainedTypeToken() { + return selfcontainedTypeToken; + } + + public void setSelfcontainedTypeToken(String selfcontainedTypeToken) { + this.selfcontainedTypeToken = selfcontainedTypeToken; + } + } + + public static class ReferenceTypeTokenBindResponse implements Serializable { + + @JsonProperty("selfcontainedTypeToken") + private String selfcontainedTypeToken; + + public String getSelfcontainedTypeToken() { + return selfcontainedTypeToken; + } + + public void setSelfcontainedTypeToken(String selfcontainedTypeToken) { + this.selfcontainedTypeToken = selfcontainedTypeToken; + } + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutorFactory.java new file mode 100644 index 000000000000..8e5b6f6272c0 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ReferenceTypeTokenExecutorFactory.java @@ -0,0 +1,67 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Takashi Norimatsu + */ +public class ReferenceTypeTokenExecutorFactory extends AbstractReferenceTypeTokenExecutorFactory { + + public static final String PROVIDER_ID = "reference-type-token"; + + public static final String SELFCONTAINED_TYPE_TOKEN_BIND_ENDPOINT = "selfcontained-type-token-bind-endpoint"; + public static final String SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT = "selfcontained-token-get-endpoint"; + + public static final String SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_QUERY_PARAM = "reference_type_token"; + + private static final ProviderConfigProperty SELFCONTAINED_TYPE_TOKEN_BIND_ENDPOINT_PROPERTY = new ProviderConfigProperty( + SELFCONTAINED_TYPE_TOKEN_BIND_ENDPOINT, "Self-contained type Token Bind Endpoint", "An endpoint of an external token store for binding a reference type token with an self-contained type token.", + ProviderConfigProperty.STRING_TYPE, "https://localhost/bind-selfcontained-type-token"); + + private static final ProviderConfigProperty SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_PROPERTY = new ProviderConfigProperty( + SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT, "Self-contained type Token Get Endpoint", "An endpoint of an external token store for getting an self-contained type token bound with a reference type token.", + ProviderConfigProperty.STRING_TYPE, "https://localhost/get-selfcontained-type-token"); + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session) { + return new ReferenceTypeTokenExecutor(session); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "Convert self-contained type access and refresh tokens to reference type access and refresh tokens by using an external token store via its endpoints. Converted reference type access and refresh token are the jti claim of corresponding self-contained type access and refresh tokens."; + } + + @Override + public List getConfigProperties() { + return new ArrayList<>(Arrays.asList(SELFCONTAINED_TYPE_TOKEN_BIND_ENDPOINT_PROPERTY, SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_PROPERTY)); + } + +} 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 280afeee5820..457701fd748e 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 @@ -21,4 +21,5 @@ org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory -org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory +org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutorFactory diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java index 1b89b08026d1..8ea0d4bd0565 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestApplicationResourceProvider.java @@ -67,6 +67,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider { private final ConcurrentMap authenticationChannelRequests; private final ConcurrentMap cibaClientNotifications; private final ConcurrentMap intentClientBindings; + private final ConcurrentMap referenceTypeTokenConversions; private final HttpRequest request; @@ -78,7 +79,8 @@ public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue authenticationChannelRequests, ConcurrentMap cibaClientNotifications, - ConcurrentMap intentClientBindings) { + ConcurrentMap intentClientBindings, + ConcurrentMap referenceTypeTokenConversions) { this.session = session; this.adminLogoutActions = adminLogoutActions; this.backChannelLogoutTokens = backChannelLogoutTokens; @@ -89,6 +91,7 @@ public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue authenticationChannelRequests = new ConcurrentHashMap<>(); private ConcurrentMap cibaClientNotifications = new ConcurrentHashMap<>(); private ConcurrentMap intentClientBindings = new ConcurrentHashMap<>(); + private ConcurrentMap referenceTypeTokenConversions = new ConcurrentHashMap<>(); @Override public RealmResourceProvider create(KeycloakSession session) { return new TestApplicationResourceProvider(session, adminLogoutActions, - backChannelLogoutTokens, frontChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications, intentClientBindings); + backChannelLogoutTokens, frontChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications, intentClientBindings, referenceTypeTokenConversions); } @Override diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java index 15d80458c4b9..e79fbdcbf112 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -56,6 +56,8 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutor; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutorFactory; import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory; @@ -65,6 +67,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.ws.rs.GET; +import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -88,9 +91,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Stream; @@ -106,14 +106,17 @@ public class TestingOIDCEndpointsApplicationResource { private final ConcurrentMap authenticationChannelRequests; private final ConcurrentMap cibaClientNotifications; private final ConcurrentMap intentClientBindings; + private final ConcurrentMap referenceTypeTokenConversions; public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData, ConcurrentMap authenticationChannelRequests, ConcurrentMap cibaClientNotifications, - ConcurrentMap intentClientBindings) { + ConcurrentMap intentClientBindings, + ConcurrentMap referenceTypeTokenConversions) { this.clientData = oidcClientData; this.authenticationChannelRequests = authenticationChannelRequests; this.cibaClientNotifications = cibaClientNotifications; this.intentClientBindings = intentClientBindings; + this.referenceTypeTokenConversions = referenceTypeTokenConversions; } @GET @@ -768,4 +771,48 @@ public IntentClientBindCheckExecutor.IntentBindCheckResponse checkIntentClientBo } return response; } + + @POST + @Path("/bind-selfcontained-type-token") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Response bindSelfcontainedTypeToken(ReferenceTypeTokenExecutor.ReferenceTypeTokenBindRequest request) { + if (!isValidBindSelfcontainedTypeTokenRequest(request)) { + return Response.status(Status.BAD_REQUEST).build(); + } + referenceTypeTokenConversions.put(request.getReferenceTypeToken(), request.getSelfcontainedTypeToken()); + return Response.noContent().build(); + } + + private boolean isValidBindSelfcontainedTypeTokenRequest(ReferenceTypeTokenExecutor.ReferenceTypeTokenBindRequest request) { + if (request == null) return false; + String referenceTypeToken = request.getReferenceTypeToken(); + String selfcontainedTypeToken = request.getSelfcontainedTypeToken(); + if (referenceTypeToken == null || referenceTypeToken.isEmpty()) return false; + if (selfcontainedTypeToken == null || selfcontainedTypeToken.isEmpty()) return false; + if (referenceTypeTokenConversions.containsKey(referenceTypeToken)) return false; + return true; + } + + @GET + @Path("/get-selfcontained-type-token") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public ReferenceTypeTokenExecutor.ReferenceTypeTokenBindResponse getSelfcontainedTypeToken(@QueryParam(ReferenceTypeTokenExecutorFactory.SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_QUERY_PARAM) String referenceTypeToken) { + if (referenceTypeToken == null || referenceTypeToken.isEmpty()) throw new BadRequestException("invalid reference type token"); + + if (referenceTypeToken.equals("ThrowInternalServerError")) { + // invoke internal server error on token store + throw new InternalServerErrorException("internal server error"); + } + + ReferenceTypeTokenExecutor.ReferenceTypeTokenBindResponse res = new ReferenceTypeTokenExecutor.ReferenceTypeTokenBindResponse(); + if (referenceTypeTokenConversions.containsKey(referenceTypeToken)) { + res.setSelfcontainedTypeToken(referenceTypeTokenConversions.get(referenceTypeToken)); + } else { + throw new BadRequestException("no self-contained found"); + } + return res; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java index 71aea7c4dc7f..a2d526b44087 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestApplicationResourceUrls.java @@ -65,4 +65,18 @@ public static String checkIntentClientBoundUri() { return builder.build().toString(); } + + public static String bindSelfcontainedTypeAccessTokenUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "bindSelfcontainedTypeToken"); + + return builder.build().toString(); + } + + public static String getSelfcontainedTypeAccessTokenUri() { + UriBuilder builder = oidcClientEndpoints() + .path(TestOIDCEndpointsApplicationResource.class, "getSelfcontainedTypeToken"); + + return builder.build().toString(); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java index 8a621de2157a..7298ed12c727 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestOIDCEndpointsApplicationResource.java @@ -20,6 +20,8 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.protocol.oidc.grants.ciba.endpoints.ClientNotificationEndpointRequest; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutor; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutorFactory; import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor; import org.keycloak.testsuite.rest.representation.TestAuthenticationChannelRequest; @@ -170,4 +172,17 @@ void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clie @Produces(MediaType.APPLICATION_JSON) @NoCache IntentClientBindCheckExecutor.IntentBindCheckResponse checkIntentClientBound(IntentClientBindCheckExecutor.IntentBindCheckRequest request); + + @POST + @Path("/bind-selfcontained-type-token") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @NoCache + Response bindSelfcontainedTypeToken(ReferenceTypeTokenExecutor.ReferenceTypeTokenBindRequest request); + + @GET + @Path("/get-selfcontained-type-token") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + ReferenceTypeTokenExecutor.ReferenceTypeTokenBindResponse getSelfcontainedTypeToken(@QueryParam(ReferenceTypeTokenExecutorFactory.SELFCONTAINED_TYPE_TOKEN_GET_ENDPOINT_QUERY_PARAM) String referenceTypeToken); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java index 5689ce963e47..a550290c8351 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/AbstractClientPoliciesTest.java @@ -208,7 +208,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { protected ClientRegistration reg; - private static final ObjectMapper objectMapper = new ObjectMapper(); + protected static final ObjectMapper objectMapper = new ObjectMapper(); protected static final String CLIENT_NAME = "Zahlungs-App"; protected static final String TEST_USER_NAME = "test-user@localhost"; @@ -653,6 +653,17 @@ protected void doIntrospectAccessToken(OAuthClient.AccessTokenResponse tokenRes, events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); } + protected void doIntrospectRefreshToken(OAuthClient.AccessTokenResponse tokenRes, String username, String clientId, String clientSecret) throws IOException { + String tokenResponse = oauth.introspectRefreshTokenWithClientCredential(clientId, clientSecret, tokenRes.getRefreshToken()); + JsonNode jsonNode = objectMapper.readTree(tokenResponse); + assertEquals(true, jsonNode.get("active").asBoolean()); + TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class); + assertEquals(true, rep.isActive()); + assertEquals(clientId, rep.getClientId()); + assertEquals(clientId, rep.getIssuedFor()); + events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); + } + protected void doTokenRevoke(String refreshToken, String clientId, String clientSecret, String userId, boolean isOfflineAccess) throws IOException { oauth.clientId(clientId); oauth.doTokenRevoke(refreshToken, "refresh_token", clientSecret); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesTest.java index df6dff43c014..2328661b4720 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/policies/ClientPoliciesTest.java @@ -24,6 +24,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; @@ -34,6 +35,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateContextConditionConfig; import static org.keycloak.testsuite.util.ClientPoliciesUtil.createConsentRequiredExecutorConfig; import static org.keycloak.testsuite.util.ClientPoliciesUtil.createFullScopeDisabledExecutorConfig; +import static org.keycloak.testsuite.util.ClientPoliciesUtil.createReferenceTypeTokenExecutorConfig; import static org.keycloak.testsuite.util.ClientPoliciesUtil.createHolderOfKeyEnforceExecutorConfig; import static org.keycloak.testsuite.util.ClientPoliciesUtil.createIntentClientBindCheckExecutorConfig; import static org.keycloak.testsuite.util.ClientPoliciesUtil.createPKCEEnforceExecutorConfig; @@ -51,7 +53,12 @@ import java.util.Map; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; import org.hamcrest.Matchers; import org.jboss.logging.Logger; import org.junit.Assert; @@ -83,13 +90,18 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper; import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.ClaimsRepresentation; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.UserInfo; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; +import org.keycloak.representations.oidc.TokenMetadataRepresentation; import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory; @@ -101,6 +113,8 @@ import org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecutorFactory; import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutorFactory; import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFactory; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutor.ReferenceTypeTokenBindResponse; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutorFactory; import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFactory; import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory; import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory; @@ -115,6 +129,7 @@ import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExceptionConditionFactory; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder; import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder; @@ -125,10 +140,13 @@ import org.keycloak.testsuite.util.RoleBuilder; import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.testsuite.util.UserInfoClientUtil; import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.MediaType; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; @@ -1186,11 +1204,11 @@ public void testRejectImplicitGrantExecutor() throws Exception { // register policies json = (new ClientPoliciesBuilder()).addPolicy( (new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Az Elso Politika", Boolean.TRUE) - .addCondition(AnyClientConditionFactory.PROVIDER_ID, - createAnyClientConditionConfig()) - .addProfile(PROFILE_NAME) - .toRepresentation() - ).toString(); + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); updatePolicies(json); try { @@ -1225,5 +1243,233 @@ private void testProhibitedImplicitOrHybridFlow(boolean isOpenid, String respons assertEquals(expectedError, oauth.getCurrentFragment().get(OAuth2Constants.ERROR)); assertEquals(expectedErrorDescription, oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION)); } + + @Test + public void testReferenceTypeToken() throws Exception { + // register profiles + String json = (new ClientProfilesBuilder()).addProfile( + (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Eichte profil") + .addExecutor(ReferenceTypeTokenExecutorFactory.PROVIDER_ID, + createReferenceTypeTokenExecutorConfig(TestApplicationResourceUrls.bindSelfcontainedTypeAccessTokenUri(), + TestApplicationResourceUrls.getSelfcontainedTypeAccessTokenUri())) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // register policies + json = (new ClientPoliciesBuilder()).addPolicy( + (new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Test Policy", Boolean.TRUE) + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); + updatePolicies(json); + + String clientId = generateSuffixedName(CLIENT_NAME); + String clientSecret = "secret"; + createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setSecret(clientSecret); + }); + + // token request + OAuthClient.AccessTokenResponse accessTokenResponse = successfulLogin(clientId, clientSecret); + verifySelfcontainedTypeTokenBinding(clientId, accessTokenResponse); + + // access token introspection + doIntrospectAccessToken(accessTokenResponse, TEST_USER_NAME, clientId, clientSecret); + + // userinfo request + Client client = AdminClientUtil.createResteasyClient(); + Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken(), MediaType.APPLICATION_JSON); + UserInfo userInfo = UserInfoClientUtil.testSuccessfulUserInfoResponse(response, TEST_USER_NAME, TEST_USER_NAME); + logger.infov("userInfo.getName() = {0}, serInfo.getPreferredUsername() = {1}, userInfo.getEmail() = {2}", + userInfo.getName(), userInfo.getPreferredUsername(), userInfo.getEmail()); + events.expect(EventType.USER_INFO_REQUEST).client(clientId).user(userInfo.getSub()).session(accessTokenResponse.getSessionState()).clearDetails().assertEvent(); + + // token refresh + accessTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), clientSecret); + assertEquals(Status.OK.getStatusCode(), accessTokenResponse.getStatusCode()); + verifySelfcontainedTypeTokenBinding(clientId, accessTokenResponse); + events.expect(EventType.REFRESH_TOKEN).client(clientId).user(userInfo.getSub()).session(accessTokenResponse.getSessionState()).clearDetails().assertEvent(); + + // access token revocation + try (CloseableHttpResponse revokeRes = oauth.doTokenRevoke(accessTokenResponse.getAccessToken(), "access_token", clientSecret)) { + events.expect(EventType.REVOKE_GRANT).client(clientId).user(userInfo.getSub()).session((String)null).clearDetails().assertEvent(); + verifyTokenIntrospectionDeactivated(clientId, clientSecret, accessTokenResponse); + } + + // refresh token introspection + doIntrospectRefreshToken(accessTokenResponse, TEST_USER_NAME, clientId, clientSecret); + + // refresh token revocation + try (CloseableHttpResponse revokeRes = oauth.doTokenRevoke(accessTokenResponse.getRefreshToken(), "refresh_token", clientSecret)) { + events.expect(EventType.REVOKE_GRANT).client(clientId).user(userInfo.getSub()).session((String)null).clearDetails().assertEvent(); + verifyTokenIntrospectionDeactivated(clientId, clientSecret, accessTokenResponse); + } + + // legacy backchannel logout + accessTokenResponse = successfulLogin(clientId, clientSecret); + oauth.doLogout(accessTokenResponse.getRefreshToken(), clientSecret); + events.expectLogout(accessTokenResponse.getSessionState()).client(clientId).clearDetails().assertEvent(); + } + + @Test + public void testInvalidReferenceTypeToken() throws Exception { + + String clientId = generateSuffixedName(CLIENT_NAME); + String clientSecret = "secret"; + createClientByAdmin(clientId, (ClientRepresentation clientRep) -> { + clientRep.setSecret(clientSecret); + }); + + // ordinary self-contained tokens + verifyErrorResponseOnInvalidTokens(clientId, clientSecret); + + // register profiles + String json = (new ClientProfilesBuilder()).addProfile( + (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Eichte profil") + .addExecutor(ReferenceTypeTokenExecutorFactory.PROVIDER_ID, + createReferenceTypeTokenExecutorConfig(TestApplicationResourceUrls.bindSelfcontainedTypeAccessTokenUri(), + TestApplicationResourceUrls.getSelfcontainedTypeAccessTokenUri())) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // create policies but not yet register + json = (new ClientPoliciesBuilder()).addPolicy( + (new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Test Policy", Boolean.TRUE) + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); + + // reference type tokens + updatePolicies(json); + verifyErrorResponseOnInvalidTokens(clientId, clientSecret); + + // invoke internal server error in token store outside keycloak + // token refresh + String invalidAccessToken = "ThrowInternalServerError"; + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doRefreshTokenRequest(invalidAccessToken, clientSecret); + assertEquals(Status.BAD_REQUEST.getStatusCode(), accessTokenResponse.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, accessTokenResponse.getError()); + assertEquals("Invalid refresh token", accessTokenResponse.getErrorDescription()); + events.expect(EventType.REFRESH_TOKEN_ERROR).client(clientId).user((String)null).session((String)null).clearDetails().error(OAuthErrorException.INVALID_TOKEN).assertEvent(); + + /* TBD + // re-register invalid configuration of executor + json = (new ClientProfilesBuilder()).addProfile( + (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Eichte profil") + .addExecutor(ReferenceTypeTokenExecutorFactory.PROVIDER_ID, + createReferenceTypeTokenExecutorConfig("ftp://", TestApplicationResourceUrls.getSelfcontainedTypeAccessTokenUri())) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // invoke internal server error in token store outside keycloak + // token request + oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD); + EventRepresentation loginEvent = events.expectLogin().client(clientId).assertEvent(); + String sessionId = loginEvent.getSessionId(); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + accessTokenResponse = oauth.doAccessTokenRequest(code, clientSecret); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), accessTokenResponse.getStatusCode()); + assertEquals(OAuthErrorException.SERVER_ERROR, accessTokenResponse.getError()); + assertEquals("Internal problem", accessTokenResponse.getErrorDescription()); + events.expect(EventType.CODE_TO_TOKEN_ERROR).client(clientId).session(sessionId).error(OAuthErrorException.SERVER_ERROR).clearDetails().assertEvent(); + */ + } + + private void verifySelfcontainedTypeTokenBinding(String clientId, OAuthClient.AccessTokenResponse accessTokenResponse) throws Exception { + ReferenceTypeTokenBindResponse referenceTypeTokenBindResponse = testingClient.testApp().oidcClientEndpoints().getSelfcontainedTypeToken(accessTokenResponse.getAccessToken()); + IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken()); + AccessToken accessToken = oauth.verifyToken(referenceTypeTokenBindResponse.getSelfcontainedTypeToken()); + assertEquals(accessToken.getId(), accessTokenResponse.getAccessToken()); + assertEquals(idToken.getSubject(), accessToken.getSubject()); + assertEquals(idToken.getSessionId(), accessToken.getSessionId()); + assertEquals(idToken.getSessionState(), accessToken.getSessionState()); + assertEquals(clientId, accessToken.getIssuedFor()); + + referenceTypeTokenBindResponse = testingClient.testApp().oidcClientEndpoints().getSelfcontainedTypeToken(accessTokenResponse.getRefreshToken()); + RefreshToken refreshToken = oauth.parseRefreshToken(referenceTypeTokenBindResponse.getSelfcontainedTypeToken()); + assertEquals(refreshToken.getId(), accessTokenResponse.getRefreshToken()); + assertEquals(idToken.getSessionId(), refreshToken.getSessionId()); + assertEquals(idToken.getSessionState(), refreshToken.getSessionState()); + assertEquals(clientId, refreshToken.getIssuedFor()); + } + + private void verifyTokenIntrospectionDeactivated(String clientId, String clientSecret, OAuthClient.AccessTokenResponse accessTokenResponse) throws Exception { + String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(clientId, clientSecret, accessTokenResponse.getAccessToken()); + TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class); + assertFalse(tokenMetadataRepresentation.isActive()); + events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); + } + private void verifyErrorResponseOnInvalidTokens(String clientId, String clientSecret) throws Exception { + String invalidAccessToken = "-kclSldVjiiWARAradjvww^-dav["; + String invalidRefreshToken = "ghaoeiv-aeli38"; + + oauth.clientId(clientId); + + // access token introspection + String tokenResponse = oauth.introspectAccessTokenWithClientCredential(clientId, clientSecret, invalidAccessToken); + JsonNode jsonNode = objectMapper.readTree(tokenResponse); + assertEquals(false, jsonNode.get("active").asBoolean()); + events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); + + // refresh token introspection + tokenResponse = oauth.introspectRefreshTokenWithClientCredential(clientId, clientSecret, invalidRefreshToken); + jsonNode = objectMapper.readTree(tokenResponse); + assertEquals(false, jsonNode.get("active").asBoolean()); + events.expect(EventType.INTROSPECT_TOKEN).client(clientId).user((String)null).clearDetails().assertEvent(); + + // userinfo request + Client client = AdminClientUtil.createResteasyClient(); + Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, invalidAccessToken, MediaType.APPLICATION_JSON); + assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatusInfo().getStatusCode()); + events.expect(EventType.USER_INFO_REQUEST_ERROR).client((String)null).user((String)null).session((String)null).error(OAuthErrorException.INVALID_TOKEN).clearDetails().assertEvent(); + + // token refresh + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doRefreshTokenRequest(invalidRefreshToken, clientSecret); + assertEquals(Status.BAD_REQUEST.getStatusCode(), accessTokenResponse.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, accessTokenResponse.getError()); + assertEquals("Invalid refresh token", accessTokenResponse.getErrorDescription()); + events.expect(EventType.REFRESH_TOKEN_ERROR).client(clientId).user((String)null).session((String)null).clearDetails().error(OAuthErrorException.INVALID_TOKEN).assertEvent(); + + // access token revocation + try (CloseableHttpResponse closableHttpResponse = oauth.doTokenRevoke(invalidAccessToken, "access_token", clientSecret)) { + assertEquals(Status.OK.getStatusCode(), closableHttpResponse.getStatusLine().getStatusCode()); + jsonNode = objectMapper.readTree(IOUtils.toString(closableHttpResponse.getEntity().getContent(), "UTF-8")); + assertEquals(OAuthErrorException.INVALID_TOKEN, jsonNode.get(OAuth2Constants.ERROR).asText()); + assertEquals("Invalid token", jsonNode.get(OAuth2Constants.ERROR_DESCRIPTION).asText()); + } + events.expect(EventType.REVOKE_GRANT_ERROR).client(clientId).user((String)null).session((String)null).clearDetails().error(OAuthErrorException.INVALID_TOKEN).assertEvent(); + + // refresh token revocation + try (CloseableHttpResponse closableHttpResponse = oauth.doTokenRevoke(invalidRefreshToken, "refresh_token", clientSecret)) { + assertEquals(Status.OK.getStatusCode(), closableHttpResponse.getStatusLine().getStatusCode()); + jsonNode = objectMapper.readTree(IOUtils.toString(closableHttpResponse.getEntity().getContent(), "UTF-8")); + assertEquals(OAuthErrorException.INVALID_TOKEN, jsonNode.get(OAuth2Constants.ERROR).asText()); + assertEquals("Invalid token", jsonNode.get(OAuth2Constants.ERROR_DESCRIPTION).asText()); + } + events.expect(EventType.REVOKE_GRANT_ERROR).client(clientId).user((String)null).session((String)null).clearDetails().error(OAuthErrorException.INVALID_TOKEN).assertEvent(); + + // legacy backchannel logout + accessTokenResponse = successfulLogin(clientId, clientSecret); + try (CloseableHttpResponse closableHttpResponse = oauth.doLogout(invalidRefreshToken, clientSecret)) { + assertEquals(Status.BAD_REQUEST.getStatusCode(), closableHttpResponse.getStatusLine().getStatusCode()); + jsonNode = objectMapper.readTree(IOUtils.toString(closableHttpResponse.getEntity().getContent(), "UTF-8")); + assertEquals(OAuthErrorException.INVALID_GRANT, jsonNode.get(OAuth2Constants.ERROR).asText()); + assertEquals("Invalid refresh token", jsonNode.get(OAuth2Constants.ERROR_DESCRIPTION).asText()); + } + events.expect(EventType.LOGOUT_ERROR).client(clientId).user((String)null).session((String)null).clearDetails().error(OAuthErrorException.INVALID_TOKEN).assertEvent(); + + // for next tests, logout successfully + try (CloseableHttpResponse closableHttpResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), clientSecret)) { + } + events.expect(EventType.LOGOUT).client(clientId).session(accessTokenResponse.getSessionState()).clearDetails().assertEvent(); + + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java index 509f061c8983..0f865ff025eb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientPoliciesUtil.java @@ -40,6 +40,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesCondition; import org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutor; import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutor; +import org.keycloak.services.clientpolicy.executor.ReferenceTypeTokenExecutor; import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutor; import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutor; import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutor; @@ -235,6 +236,13 @@ public static IntentClientBindCheckExecutor.Configuration createIntentClientBind return config; } + public static ReferenceTypeTokenExecutor.Configuration createReferenceTypeTokenExecutorConfig(String selfcontainedTypeTokenBindEndpoint, String selfcontainedTypeTokenGetEndpoint) { + ReferenceTypeTokenExecutor.Configuration config = new ReferenceTypeTokenExecutor.Configuration(); + config.setSelfcontainedTypeTokenBindEndpoint(selfcontainedTypeTokenBindEndpoint); + config.setSelfcontainedTypeTokenGetEndpoint(selfcontainedTypeTokenGetEndpoint); + return config; + } + public static class ClientPoliciesBuilder { private final ClientPoliciesRepresentation policiesRep; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties index 0779b047af53..d01602eb484b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties @@ -78,8 +78,8 @@ log4j.logger.org.apache.directory.server.core=warn # log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace # log4j.logger.org.keycloak.keys.infinispan=trace log4j.logger.org.keycloak.services.clientregistration.policy=debug -# log4j.logger.org.keycloak.services.clientpolicy=trace -# log4j.logger.org.keycloak.testsuite.clientpolicy=trace + log4j.logger.org.keycloak.services.clientpolicy=trace + log4j.logger.org.keycloak.testsuite.clientpolicy=trace # log4j.logger.org.keycloak.protocol.ciba=trace