diff --git a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyException.java b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyException.java index d5849b4890..ed5fe0d524 100644 --- a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyException.java +++ b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyException.java @@ -17,10 +17,16 @@ package org.keycloak.services.clientpolicy; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.keycloak.OAuthErrorException; + public class ClientPolicyException extends Exception { - private String error; + private String error = OAuthErrorException.INVALID_REQUEST; private String errorDetail; + private Status errorStatus = Response.Status.BAD_REQUEST; public ClientPolicyException(String error, String errorDetail) { super(error); @@ -28,12 +34,26 @@ public class ClientPolicyException extends Exception { setErrorDetail(errorDetail); } + public ClientPolicyException(String error, String errorDetail, Status errorStatus) { + super(error); + setError(error); + setErrorDetail(errorDetail); + setErrorStatus(errorStatus); + } + public ClientPolicyException(String error, String errorDetail, Throwable throwable) { super(throwable); setError(error); setErrorDetail(errorDetail); } + public ClientPolicyException(String error, String errorDetail, Status errorStatus, Throwable throwable) { + super(throwable); + setError(error); + setErrorDetail(errorDetail); + setErrorStatus(errorStatus); + } + public String getError() { return error; } @@ -50,6 +70,14 @@ public class ClientPolicyException extends Exception { this.errorDetail = errorDetail; } + public Status getErrorStatus() { + return errorStatus; + } + + public void setErrorStatus(Status errorStatus) { + this.errorStatus = errorStatus; + } + /** * If {@link ClientPolicyException} is used to notify the event so that it needs not to have stack trace. * @return always null diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index c32dd13a7f..49e1198c9b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -212,7 +212,7 @@ public class LogoutEndpoint { try { session.clientPolicy().triggerOnEvent(new LogoutRequestContext(form)); } catch (ClientPolicyException cpe) { - throw new ErrorResponseException(Errors.INVALID_REQUEST, cpe.getErrorDetail(), Response.Status.BAD_REQUEST); + throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } RefreshToken token = null; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 01a7ee3d00..8e13e2281b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -539,7 +539,7 @@ public class TokenEndpoint { session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams)); } catch (ClientPolicyException cpe) { event.error(cpe.getError()); - throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST); + throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } AccessTokenResponse res; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index 33c9818203..36cd33ba32 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -52,6 +52,7 @@ import org.keycloak.services.clientpolicy.TokenRevokeContext; import org.keycloak.services.managers.UserSessionCrossDCManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.Cors; +import org.keycloak.services.util.MtlsHoKTokenUtil; import org.keycloak.util.TokenUtil; /** @@ -102,7 +103,7 @@ public class TokenRevocationEndpoint { session.clientPolicy().triggerOnEvent(new TokenRevokeContext(formParams)); } catch (ClientPolicyException cpe) { event.error(cpe.getError()); - throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST); + throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } checkToken(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index c53b528c4c..1d429ed72a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -43,6 +43,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.AccessToken; +import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.Urls; import org.keycloak.services.clientpolicy.ClientPolicyException; @@ -136,7 +137,7 @@ public class UserInfoEndpoint { try { session.clientPolicy().triggerOnEvent(new UserInfoRequestContext(tokenString)); } catch (ClientPolicyException cpe) { - throw new ErrorResponseException(Errors.INVALID_REQUEST, cpe.getErrorDetail(), Response.Status.BAD_REQUEST); + throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus()); } EventBuilder event = new EventBuilder(realm, session, clientConnection) diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutor.java new file mode 100644 index 0000000000..85975855e7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutor.java @@ -0,0 +1,162 @@ +/* + * Copyright 2020 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.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.component.ComponentModel; +import org.keycloak.events.Errors; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.services.clientpolicy.*; +import org.keycloak.services.util.MtlsHoKTokenUtil; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +public class HolderOfKeyEnforceExecutor extends AbstractAugumentingClientRegistrationPolicyExecutor { + + private final KeycloakSession session; + private final ComponentModel componentModel; + + public HolderOfKeyEnforceExecutor(KeycloakSession session, ComponentModel componentModel) { + super(session, componentModel); + this.session = session; + this.componentModel = componentModel; + } + + @Override + public String getName() { + return componentModel.getName(); + } + + @Override + public String getProviderId() { + return componentModel.getProviderId(); + } + + @Override + protected void augment(ClientRepresentation rep) { + if (Boolean.parseBoolean(componentModel.getConfig().getFirst(AbstractAugumentingClientRegistrationPolicyExecutor.IS_AUGMENT))) { + OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setUseMtlsHoKToken(true); + } + } + + @Override + protected void validate(ClientRepresentation rep) throws ClientPolicyException { + boolean useMtlsHokToken = OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).isUseMtlsHokToken(); + if (!useMtlsHokToken) { + throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid client metadata: MTLS token in disabled"); + } + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + super.executeOnEvent(context); + HttpRequest request = session.getContext().getContextObject(HttpRequest.class); + + switch (context.getEvent()) { + + case TOKEN_REQUEST: + AccessToken.CertConf certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session); + if (certConf == null) { + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding"); + } + break; + + case TOKEN_REFRESH: + checkTokenRefresh((TokenRefreshContext) context, request); + break; + + case TOKEN_REVOKE: + checkTokenRevoke((TokenRevokeContext) context, request); + break; + + case USERINFO_REQUEST: + checkUserInfo((UserInfoRequestContext) context, request); + break; + + case LOGOUT_REQUEST: + checkLogout((LogoutRequestContext) context, request); + break; + } + } + + private void checkLogout(LogoutRequestContext context, HttpRequest request) throws ClientPolicyException { + MultivaluedMap formParameters = context.getParams(); + String encodedRefreshToken = formParameters.getFirst(OAuth2Constants.REFRESH_TOKEN); + + RefreshToken refreshToken = session.tokens().decode(encodedRefreshToken, RefreshToken.class); + if (refreshToken == null) { + // this executor does not treat this error case. + return; + } + + if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) { + throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED); + } + } + + private void checkUserInfo(UserInfoRequestContext context, HttpRequest request) throws ClientPolicyException { + String encodedAccessToken = context.getTokenString(); + + AccessToken accessToken = session.tokens().decode(encodedAccessToken, AccessToken.class); + if (accessToken == null) { + // this executor does not treat this error case. + return; + } + + if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(accessToken, request, session)) { + throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED); + } + } + + private void checkTokenRevoke(TokenRevokeContext context, HttpRequest request) throws ClientPolicyException { + MultivaluedMap revokeParameters = context.getParams(); + String encodedRevokeToken = revokeParameters.getFirst("token"); + + RefreshToken refreshToken = session.tokens().decode(encodedRevokeToken, RefreshToken.class); + if (refreshToken == null) { + // this executor does not treat this error case. + return; + } + + if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) { + throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED); + } + } + + private void checkTokenRefresh(TokenRefreshContext context, HttpRequest request) throws ClientPolicyException { + MultivaluedMap formParameters = context.getParams(); + String encodedRefreshToken = formParameters.getFirst(OAuth2Constants.REFRESH_TOKEN); + + RefreshToken refreshToken = session.tokens().decode(encodedRefreshToken, RefreshToken.class); + if (refreshToken == null) { + // this executor does not treat this error case. + return; + } + + if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) { + throw new ClientPolicyException(Errors.NOT_ALLOWED, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC, Response.Status.UNAUTHORIZED); + } + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutorFactory.java new file mode 100644 index 0000000000..1800f7948f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforceExecutorFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 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.component.ComponentModel; +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 HolderOfKeyEnforceExecutorFactory implements ClientPolicyExecutorProviderFactory { + + public static final String PROVIDER_ID = "holder-of-key-enforce-executor"; + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session, ComponentModel model) { + return new HolderOfKeyEnforceExecutor(session, model); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "It prohibits the client whose MTLS certificate does not match with the certificate thumbprint from the tokens."; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + +} 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 5b1bdac232..852431ab14 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 @@ -5,4 +5,5 @@ org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory -org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory +org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java index aee37291e6..a9fededbaa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java @@ -124,6 +124,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesCondi import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory; import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider; +import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory; @@ -144,6 +145,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject; import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory; +import org.keycloak.testsuite.util.MutualTLSUtils; import org.keycloak.testsuite.util.OAuthClient; import com.fasterxml.jackson.databind.JsonNode; @@ -1351,6 +1353,162 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { } } + @Test + public void testHolderOfKeyEnforceExecutor() throws Exception { + String policyName = "MyPolicy"; + createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null); + logger.info("... Created Policy : " + policyName); + + createCondition("ClientRolesCondition", ClientRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + setConditionClientRoles(provider, Collections.singletonList("sample-client-role")); + }); + registerCondition("ClientRolesCondition", policyName); + logger.info("... Registered Condition : ClientRolesCondition"); + + createExecutor("HolderOfKeyEnforceExecutor", HolderOfKeyEnforceExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + setExecutorAugmentActivate(provider); + }); + registerExecutor("HolderOfKeyEnforceExecutor", policyName); + logger.info("... Registered Executor : HolderOfKeyEnforceExecutor"); + + String clientName = "Zahlungs-App"; + String userPassword = "password"; + String clientId = createClientDynamically(clientName, (OIDCClientRepresentation clientRep) -> { + clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.TLS_CLIENT_AUTH); + }); + + try { + checkMtlsFlow(userPassword); + } finally { + deleteClientByAdmin(clientId); + } + } + + private void checkMtlsFlow(String password) throws IOException { + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), "test-app"); + ClientRepresentation clientRep = clientResource.toRepresentation(); + clientRep.setDefaultRoles(new String[]{"sample-client-role"}); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseMtlsHoKToken(true); + + clientResource.update(clientRep); + + // Check login. + OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("test-user@localhost", password); + Assert.assertNull(loginResponse.getError()); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + // Check token obtaining. + OAuthClient.AccessTokenResponse accessTokenResponse; + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + accessTokenResponse = oauth.doAccessTokenRequest(code, password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(200, accessTokenResponse.getStatusCode()); + + // Check token refresh. + OAuthClient.AccessTokenResponse accessTokenResponseRefreshed; + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(200, accessTokenResponseRefreshed.getStatusCode()); + + // Check token introspection. + String tokenResponse; + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + tokenResponse = oauth.introspectTokenWithClientCredential(TEST_CLIENT, password, "access_token", accessTokenResponse.getAccessToken(), client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + Assert.assertNotNull(tokenResponse); + TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); + Assert.assertTrue(tokenMetadataRepresentation.isActive()); + + // Check token revoke. + CloseableHttpResponse tokenRevokeResponse; + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + tokenRevokeResponse = oauth.doTokenRevoke(accessTokenResponse.getRefreshToken(), "refresh_token", password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(200, tokenRevokeResponse.getStatusLine().getStatusCode()); + + // Check logout. + CloseableHttpResponse logoutResponse; + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + + assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); + + // Check login. + loginResponse = oauth.doLogin("test-user@localhost", password); + Assert.assertNull(loginResponse.getError()); + + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + // Check token obtaining without certificate + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) { + accessTokenResponse = oauth.doAccessTokenRequest(code, password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(400, accessTokenResponse.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, accessTokenResponse.getError()); + + // Check frontchannel logout and login. + oauth.openLogout(); + loginResponse = oauth.doLogin("test-user@localhost", password); + Assert.assertNull(loginResponse.getError()); + + code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + // Check token obtaining. + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + accessTokenResponse = oauth.doAccessTokenRequest(code, password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(200, accessTokenResponse.getStatusCode()); + + // Check token refresh with other certificate + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) { + accessTokenResponseRefreshed = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(401, accessTokenResponseRefreshed.getStatusCode()); + assertEquals(Errors.NOT_ALLOWED, accessTokenResponseRefreshed.getError()); + + // Check token revoke with other certificate + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) { + tokenRevokeResponse = oauth.doTokenRevoke(accessTokenResponse.getRefreshToken(), "refresh_token", password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(401, tokenRevokeResponse.getStatusLine().getStatusCode()); + + // Check logout without certificate + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) { + logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + assertEquals(401, logoutResponse.getStatusLine().getStatusCode()); + + // Check logout. + try (CloseableHttpClient client = MutualTLSUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) { + logoutResponse = oauth.doLogout(accessTokenResponse.getRefreshToken(), password, client); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + private CloseableHttpResponse sendRequest(String requestUrl, List parameters) throws Exception { CloseableHttpClient client = new DefaultHttpClient(); try { @@ -2065,4 +2223,4 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { private void setExecutorAugmentedClientAuthMethod(ComponentRepresentation provider, String augmentedClientAuthMethod) { provider.getConfig().putSingle(SecureClientAuthEnforceExecutorFactory.CLIENT_AUTHNS_AUGMENT, augmentedClientAuthMethod); } -} +} \ No newline at end of file