From 05dfac75ca41a76f6507ed7f0c9aa3c714fee834 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Wed, 16 Dec 2020 11:47:47 +0900 Subject: [PATCH] KEYCLOAK-14202 Client Policy - Executor : Enforce secure signature algorithm for Signed JWT client authentication Co-authored-by: Andrii Murashkin --- ...gAlgorithmForSignedJwtEnforceExecutor.java | 99 ++++++ ...thmForSignedJwtEnforceExecutorFactory.java | 65 ++++ ...ecutor.ClientPolicyExecutorProviderFactory | 3 +- .../client/ClientPolicyBasicsTest.java | 285 ++++++++++++++++++ 4 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutor.java create mode 100644 services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.java diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutor.java new file mode 100644 index 0000000000..7759e9daec --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutor.java @@ -0,0 +1,99 @@ +/* + * 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.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.Algorithm; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.ClientPolicyLogger; + +public class SecureSigningAlgorithmForSignedJwtEnforceExecutor implements ClientPolicyExecutorProvider { + + private static final Logger logger = Logger.getLogger(SecureSigningAlgorithmForSignedJwtEnforceExecutor.class); + + private final KeycloakSession session; + private final ComponentModel componentModel; + + public SecureSigningAlgorithmForSignedJwtEnforceExecutor(KeycloakSession session, ComponentModel componentModel) { + this.session = session; + this.componentModel = componentModel; + } + + @Override + public String getName() { + return componentModel.getName(); + } + + @Override + public String getProviderId() { + return componentModel.getProviderId(); + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case TOKEN_REQUEST: + case TOKEN_REFRESH: + case TOKEN_REVOKE: + case TOKEN_INTROSPECT: + case LOGOUT_REQUEST: + HttpRequest req = session.getContext().getContextObject(HttpRequest.class); + String clientAssertion = req.getDecodedFormParameters().getFirst(OAuth2Constants.CLIENT_ASSERTION); + JWSInput jws = null; + try { + jws = new JWSInput(clientAssertion); + } catch (JWSInputException e) { + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not allowed input format."); + } + String alg = jws.getHeader().getAlgorithm().name(); + verifySecureSigningAlgorithm(alg); + break; + default: + return; + } + } + + private void verifySecureSigningAlgorithm(String signatureAlgorithm) throws ClientPolicyException { + if (signatureAlgorithm == null) { + ClientPolicyLogger.log(logger, "Signing algorithm not specified explicitly."); + return; + } + + // Please change also SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.getHelpText() if you are changing any algorithms here. + switch (signatureAlgorithm) { + case Algorithm.PS256: + case Algorithm.PS384: + case Algorithm.PS512: + case Algorithm.ES256: + case Algorithm.ES384: + case Algorithm.ES512: + ClientPolicyLogger.log(logger, "Passed. signatureAlgorithm = " + signatureAlgorithm); + return; + } + ClientPolicyLogger.log(logger, "NOT allowed signatureAlgorithm = " + signatureAlgorithm); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not allowed signature algorithm."); + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.java new file mode 100644 index 0000000000..e04ab50d5c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.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 SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory implements ClientPolicyExecutorProviderFactory { + + public static final String PROVIDER_ID = "securesignalgjwt-enforce-executor"; + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session, ComponentModel model) { + return new SecureSigningAlgorithmForSignedJwtEnforceExecutor(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 refuses the client whose JWT token signature algorithms are considered not to be secure. It accepts ES256, ES384, ES512, PS256, PS384 and PS512."; + } + + @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 a1e2eb90d1..5b1bdac232 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 @@ -4,4 +4,5 @@ org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFacto org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory -org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory +org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory \ 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 eb0c6e600a..78cd5bd0a1 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 @@ -25,18 +25,37 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import javax.ws.rs.BadRequestException; import javax.ws.rs.core.Response; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; import org.hamcrest.Matchers; import org.jboss.logging.Logger; import org.junit.After; @@ -46,6 +65,7 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.adapters.AdapterUtils; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; @@ -55,13 +75,21 @@ import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.ClientRegistration; import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.common.Profile; +import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; +import org.keycloak.common.util.UriUtils; +import org.keycloak.constants.ServiceUrlConstants; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; import org.keycloak.models.utils.KeycloakModelUtils; @@ -70,6 +98,7 @@ import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; @@ -102,6 +131,7 @@ import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFa import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory; +import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; @@ -110,6 +140,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls; import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource; +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.OAuthClient; @@ -1235,6 +1266,260 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { } } + @Test + public void testSecureSigningAlgorithmForSignedJwtEnforceExecutor() throws Exception { + // policy including client role condition + 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, new ArrayList<>(Arrays.asList("sample-client-role-alpha", "sample-client-role-zeta"))); + }); + registerCondition("ClientRolesCondition", policyName); + logger.info("... Registered Condition : " + "ClientRolesCondition"); + + createExecutor("SecureSigningAlgorithmForSignedJwtEnforceExecutor", SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory.PROVIDER_ID, null, + (ComponentRepresentation provider) -> { + }); + + registerExecutor("SecureSigningAlgorithmForSignedJwtEnforceExecutor", policyName); + logger.info("... Registered Executor : SecureSigningAlgorithmForSignedJwtEnforceExecutor"); + + // crate a client with client role + String clientAlphaId = "Alpha-App"; + String clientAlphaSecret = "secretAlpha"; + String cAlphaId = null; + + cAlphaId = createClientByAdmin(clientAlphaId, (ClientRepresentation clientRep) -> { + clientRep.setDefaultRoles(Arrays.asList("sample-client-role-alpha", "sample-client-role-common").toArray(new String[2])); + clientRep.setSecret(clientAlphaSecret); + clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID); + clientRep.setAttributes(new HashMap<>()); + clientRep.getAttributes().put(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, org.keycloak.crypto.Algorithm.ES256); + }); + + try { + ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientAlphaId); + ClientRepresentation clientRep = clientResource.toRepresentation(); + + KeyPair keyPair = setupJwks(org.keycloak.crypto.Algorithm.ES256, clientRep, clientResource); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + + successfulLoginAndLogoutWithSignedJWT(clientAlphaId, privateKey, publicKey); + } finally { + deleteClientByAdmin(cAlphaId); + } + } + + private CloseableHttpResponse sendRequest(String requestUrl, List parameters) throws Exception { + CloseableHttpClient client = new DefaultHttpClient(); + try { + HttpPost post = new HttpPost(requestUrl); + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + post.setEntity(formEntity); + return client.execute(post); + } finally { + oauth.closeClient(client); + } + } + + private void successfulLoginAndLogoutWithSignedJWT(String clientId, PrivateKey privateKey, PublicKey publicKey) throws Exception { + String signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + + oauth.clientId(clientId); + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin() + .client(clientId) + .assertEvent(); + String sessionId = loginEvent.getSessionId(); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + //obtain access token + OAuthClient.AccessTokenResponse response = doAccessTokenRequestWithSignedJWT(code, signedJwt); + + assertEquals(200, response.getStatusCode()); + oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken()); + assertEquals(sessionId, refreshToken.getSessionState()); + assertEquals(sessionId, refreshToken.getSessionState()); + events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()) + .client(clientId) + .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) + .assertEvent(); + + //refresh token + signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + OAuthClient.AccessTokenResponse refreshedResponse = doRefreshTokenRequestWithSignedJWT(response.getRefreshToken(), signedJwt); + assertEquals(200, refreshedResponse.getStatusCode()); + + //introspect token + signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + HttpResponse tokenIntrospectionResponse = doTokenIntrospectionWithSignedJWT("access_token", refreshedResponse.getAccessToken(), signedJwt); + assertEquals(200, tokenIntrospectionResponse.getStatusLine().getStatusCode()); + + //revoke token + signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + HttpResponse revokeTokenResponse = doTokenRevokeWithSignedJWT("refresh_toke", refreshedResponse.getRefreshToken(), signedJwt); + assertEquals(200, revokeTokenResponse.getStatusLine().getStatusCode()); + + signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + OAuthClient.AccessTokenResponse tokenRes = doRefreshTokenRequestWithSignedJWT(refreshedResponse.getRefreshToken(), signedJwt); + assertEquals(400, tokenRes.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, tokenRes.getError()); + + //logout + signedJwt = createSignedRequestToken(clientId, getRealmInfoUrl(), privateKey, publicKey, org.keycloak.crypto.Algorithm.ES256); + HttpResponse logoutResponse = doLogoutWithSignedJWT(refreshedResponse.getRefreshToken(), signedJwt); + assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); + + } + + private KeyPair setupJwks(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { + // generate and register client keypair + TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); + oidcClientEndpointsResource.generateKeys(algorithm); + Map generatedKeys = oidcClientEndpointsResource.getKeysAsBase64(); + KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm); + + // use and set jwks_url + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(true); + String jwksUrl = TestApplicationResourceUrls.clientJwksUri(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(jwksUrl); + clientResource.update(clientRepresentation); + + // set time offset, so that new keys are downloaded + setTimeOffset(20); + + return keyPair; + } + + private KeyPair getKeyPairFromGeneratedBase64(Map generatedKeys, String algorithm) throws Exception { + // It seems that PemUtils.decodePrivateKey, decodePublicKey can only treat RSA type keys, not EC type keys. Therefore, these are not used. + String privateKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY); + String publicKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY); + PrivateKey privateKey = decodePrivateKey(Base64.decode(privateKeyBase64), algorithm); + PublicKey publicKey = decodePublicKey(Base64.decode(publicKeyBase64), algorithm); + return new KeyPair(publicKey, privateKey); + } + + private static PrivateKey decodePrivateKey(byte[] der, String algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der); + String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm); + KeyFactory kf = KeyFactory.getInstance(keyAlg, "BC"); + return kf.generatePrivate(spec); + } + + private static PublicKey decodePublicKey(byte[] der, String algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(der); + String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm); + KeyFactory kf = KeyFactory.getInstance(keyAlg, "BC"); + return kf.generatePublic(spec); + } + + private String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl); + String kid = KeyUtils.createKeyId(publicKey); + SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm); + return new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer); + } + + private OAuthClient.AccessTokenResponse doAccessTokenRequestWithSignedJWT(String code, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri())); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private OAuthClient.AccessTokenResponse doRefreshTokenRequestWithSignedJWT(String refreshToken, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + CloseableHttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters); + return new OAuthClient.AccessTokenResponse(response); + } + + private HttpResponse doTokenIntrospectionWithSignedJWT(String tokenType, String tokenToIntrospect, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair("token", tokenToIntrospect)); + parameters.add(new BasicNameValuePair("token_type_hint", tokenType)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + return sendRequest(oauth.getTokenIntrospectionUrl(), parameters); + } + + private HttpResponse doTokenRevokeWithSignedJWT(String tokenType, String tokenToIntrospect, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair("token", tokenToIntrospect)); + parameters.add(new BasicNameValuePair("token_type_hint", tokenType)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + return sendRequest(oauth.getTokenRevocationUrl(), parameters); + } + + private HttpResponse doLogoutWithSignedJWT(String refreshToken, String signedJwt) throws Exception { + List parameters = new LinkedList<>(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt)); + + return sendRequest(oauth.getLogoutUrl().build(), parameters); + } + + private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { + JsonWebToken reqToken = new JsonWebToken(); + reqToken.id(AdapterUtils.generateId()); + reqToken.issuer(clientId); + reqToken.subject(clientId); + reqToken.audience(realmInfoUrl); + + int now = Time.currentTime(); + reqToken.issuedAt(now); + reqToken.expiration(now + 10); + reqToken.notBefore(now); + + return reqToken; + } + + private static String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm) { + String keyAlg = null; + switch (jwaAlgorithm) { + case org.keycloak.crypto.Algorithm.RS256: + case org.keycloak.crypto.Algorithm.RS384: + case org.keycloak.crypto.Algorithm.RS512: + case org.keycloak.crypto.Algorithm.PS256: + case org.keycloak.crypto.Algorithm.PS384: + case org.keycloak.crypto.Algorithm.PS512: + keyAlg = KeyType.RSA; + break; + case org.keycloak.crypto.Algorithm.ES256: + case org.keycloak.crypto.Algorithm.ES384: + case org.keycloak.crypto.Algorithm.ES512: + keyAlg = KeyType.EC; + break; + default : + throw new RuntimeException("Unsupported signature algorithm"); + } + return keyAlg; + } + + private String getRealmInfoUrl() { + String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth"; + return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString(); + } + private AuthorizationEndpointRequestObject createValidRequestObjectForSecureRequestObjectExecutor(String clientId) throws URISyntaxException { AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject(); requestObject.id(KeycloakModelUtils.generateId());