/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.testsuite.client; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.ws.rs.core.UriBuilder; 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.junit.Before; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.authentication.JWTClientCredentialsProvider; import org.keycloak.client.registration.Auth; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.keys.PublicKeyStorageUtils; import org.keycloak.keys.loader.PublicKeyStorageManager; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.testsuite.Assert; 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.util.OAuthClient; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTest { private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y="; private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; @Override public void addTestRealms(List<RealmRepresentation> testRealms) { super.addTestRealms(testRealms); testRealms.get(0).setPrivateKey(PRIVATE_KEY); testRealms.get(0).setPublicKey(PUBLIC_KEY); } @Before public void before() throws Exception { super.before(); ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10)); reg.auth(Auth.token(token)); } private OIDCClientRepresentation createRep() { OIDCClientRepresentation client = new OIDCClientRepresentation(); client.setClientName("RegistrationAccessTokenTest"); client.setClientUri(OAuthClient.APP_ROOT); client.setRedirectUris(Collections.singletonList(oauth.getRedirectUri())); return client; } @Test public void createClientWithJWKS_generatedKid() throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate keys for client TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys(); JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); clientRep.setJwks(keySet); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod()); Assert.assertNull(response.getClientSecret()); Assert.assertNull(response.getClientSecretExpiresAt()); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID); } // The "kid" is null in the signed JWT. This is backwards compatibility test as in versions prior to 2.3.0, the "kid" wasn't set by JWTClientCredentialsProvider @Test public void createClientWithJWKS_nullKid() throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate keys for client TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys(); JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); clientRep.setJwks(keySet); OIDCClientRepresentation response = reg.oidc().create(clientRep); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, null); } // The "kid" is set manually to some custom value @Test public void createClientWithJWKS_customKid() throws Exception { OIDCClientRepresentation response = createClientWithManuallySetKid("a1"); Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem(); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, "a1"); } private OIDCClientRepresentation createClientWithManuallySetKid(String kid) throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate keys for client TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); oidcClientEndpointsResource.generateKeys(); JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); // Override kid with custom value keySet.getKeys()[0].setKeyId(kid); clientRep.setJwks(keySet); return reg.oidc().create(clientRep); } @Test public void testTwoClientsWithSameKid() throws Exception { // Create client with manually set "kid" OIDCClientRepresentation response = createClientWithManuallySetKid("a1"); // Create client2 OIDCClientRepresentation clientRep2 = createRep(); clientRep2.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep2.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate some random keys for client2 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); PublicKey client2PublicKey = generator.generateKeyPair().getPublic(); // Set client2 with manually set "kid" to be same like kid of client1 (but keys for both clients are different) JSONWebKeySet keySet = new JSONWebKeySet(); keySet.setKeys(new JWK[]{JWKBuilder.create().kid("a1").rs256(client2PublicKey)}); clientRep2.setJwks(keySet); clientRep2 = reg.oidc().create(clientRep2); // Authenticate client1 Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem(); assertAuthenticateClientSuccess(generatedKeys, response, "a1"); // Assert item in publicKey cache for client1 String expectedCacheKey = PublicKeyStorageUtils.getClientModelCacheKey(REALM_NAME, response.getClientId()); Assert.assertTrue(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey)); // Assert it's not possible to authenticate as client2 with the same "kid" like client1 assertAuthenticateClientError(generatedKeys, clientRep2, "a1"); } @Test public void testPublicKeyCacheInvalidatedWhenUpdatingClient() throws Exception { OIDCClientRepresentation response = createClientWithManuallySetKid("a1"); Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem(); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, "a1"); // Assert item in publicKey cache for client1 String expectedCacheKey = PublicKeyStorageUtils.getClientModelCacheKey(REALM_NAME, response.getClientId()); Assert.assertTrue(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey)); // Update client with some bad JWKS_URI response.setJwksUri("http://localhost:4321/non-existent"); reg.auth(Auth.token(response.getRegistrationAccessToken())) .oidc().update(response); // Assert item not any longer for client1 Assert.assertFalse(testingClient.testing().cache(InfinispanConnectionProvider.KEYS_CACHE_NAME).contains(expectedCacheKey)); // Assert it's not possible to authenticate as client1 assertAuthenticateClientError(generatedKeys, response, "a1"); } @Test public void createClientWithJWKSURI() throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate keys for client TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys(); clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri()); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod()); Assert.assertNull(response.getClientSecret()); Assert.assertNull(response.getClientSecretExpiresAt()); Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri()); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID); } @Test public void createClientWithJWKSURI_rotateClientKeys() throws Exception { OIDCClientRepresentation clientRep = createRep(); clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS)); clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT); // Generate keys for client TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys(); clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri()); OIDCClientRepresentation response = reg.oidc().create(clientRep); Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod()); Assert.assertNull(response.getClientSecret()); Assert.assertNull(response.getClientSecretExpiresAt()); Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri()); // Tries to authenticate client with privateKey JWT assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID); // Add new key to the jwks Map<String, String> generatedKeys2 = oidcClientEndpointsResource.generateKeys(); // Error should happen. KeyStorageProvider won't yet download new keys because of timeout assertAuthenticateClientError(generatedKeys2, response, KEEP_GENERATED_KID); setTimeOffset(20); // Now new keys should be successfully downloaded assertAuthenticateClientSuccess(generatedKeys2, response, KEEP_GENERATED_KID); } // Client auth with signedJWT - helper methods private void assertAuthenticateClientSuccess(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception { KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys); String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(200, accessTokenResponse.getStatusCode()); AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken()); Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]); } private void assertAuthenticateClientError(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception { KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys); String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid); OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt); Assert.assertEquals(400, accessTokenResponse.getStatusCode()); Assert.assertNull(accessTokenResponse.getAccessToken()); Assert.assertNotNull(accessTokenResponse.getError()); } private KeyPair getKeyPairFromGeneratedPems(Map<String, String> generatedKeys) { String privateKeyPem = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY); String publicKeyPem = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY); PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem); PublicKey publicKey = KeycloakModelUtils.getPublicKey(publicKeyPem); return new KeyPair(publicKey, privateKey); } private static final String KEEP_GENERATED_KID = "KEEP_GENERATED_KID"; private String getClientSignedJWT(String clientId, KeyPair keyPair, final String kid) { String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString(); // Use token-endpoint as audience as OIDC conformance testsuite is using it too. JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() { @Override public String createSignedRequestToken(String clientId, String realmInfoUrl) { if (KEEP_GENERATED_KID.equals(kid)) { return super.createSignedRequestToken(clientId, realmInfoUrl); } else { JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl); return new JWSBuilder() .kid(kid) .jsonContent(jwt) .rsa256(keyPair.getPrivate()); } } @Override protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl); String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString(); jwt.audience(tokenEndpointUrl); return jwt; } }; jwtProvider.setupKeyPair(keyPair); jwtProvider.setTokenTimeout(10); return jwtProvider.createSignedRequestToken(clientId, realmInfoUrl); } private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception { List<NameValuePair> parameters = new LinkedList<NameValuePair>(); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); 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.getServiceAccountUrl(), parameters); return new OAuthClient.AccessTokenResponse(response); } private CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> 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); } } }