diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java index 9a35d7bd4d..1820d9e7be 100644 --- a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -37,6 +37,7 @@ import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.services.util.ResolveRelative; import org.keycloak.util.JWKSUtils; +import org.keycloak.util.JsonSerialization; import java.security.PublicKey; import java.security.cert.X509Certificate; @@ -74,6 +75,9 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { jwksUrl = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), jwksUrl); JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl); return JWKSUtils.getKeyWrappersForUse(jwks, keyUse); + } else if (config.isUseJwksString()) { + JSONWebKeySet jwks = JsonSerialization.readValue(config.getJwksString(), JSONWebKeySet.class); + return JWKSUtils.getKeyWrappersForUse(jwks, keyUse); } else if (keyUse == JWK.Use.SIG) { try { CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java index de802666a4..23317fbdcc 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -125,6 +125,24 @@ public class OIDCAdvancedConfigWrapper { setAttribute(OIDCConfigAttributes.JWKS_URL, jwksUrl); } + public boolean isUseJwksString() { + String useJwksString = getAttribute(OIDCConfigAttributes.USE_JWKS_STRING); + return Boolean.parseBoolean(useJwksString); + } + + public void setUseJwksString(boolean useJwksString) { + String val = String.valueOf(useJwksString); + setAttribute(OIDCConfigAttributes.USE_JWKS_STRING, val); + } + + public String getJwksString() { + return getAttribute(OIDCConfigAttributes.JWKS_STRING); + } + + public void setJwksString(String jwksString) { + setAttribute(OIDCConfigAttributes.JWKS_STRING, jwksString); + } + public boolean isExcludeSessionStateFromAuthResponse() { String excludeSessionStateFromAuthResponse = getAttribute(OIDCConfigAttributes.EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE); return Boolean.parseBoolean(excludeSessionStateFromAuthResponse); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java index 9f2aeafd05..f37c89c339 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java @@ -35,6 +35,10 @@ public final class OIDCConfigAttributes { public static final String USE_JWKS_URL = "use.jwks.url"; + public static final String JWKS_STRING = "jwks.string"; + + public static final String USE_JWKS_STRING = "use.jwks.string"; + public static final String EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE = "exclude.session.state.from.auth.response"; public static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens"; diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index af0706597c..f8a713bd2a 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -47,10 +47,12 @@ import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.services.clientregistration.ClientRegistrationException; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.util.JWKSUtils; +import org.keycloak.util.JsonSerialization; import org.keycloak.utils.StringUtil; import com.google.common.collect.Streams; +import java.io.IOException; import java.net.URI; import java.security.PublicKey; import java.util.ArrayList; @@ -237,40 +239,45 @@ public class DescriptionConverter { } private static boolean setPublicKey(OIDCClientRepresentation clientOIDC, ClientRepresentation clientRep) { - if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) { - return false; - } - - if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) { - throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks"); - } - OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep); if (clientOIDC.getJwks() != null) { + if (clientOIDC.getJwksUri() != null) { + throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks"); + } + JSONWebKeySet keySet = clientOIDC.getJwks(); JWK publicKeyJWk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); + + try { + configWrapper.setJwksString(JsonSerialization.writeValueAsPrettyString(clientOIDC.getJwks())); + } catch (IOException e) { + throw new ClientRegistrationException("Illegal jwks format"); + } + configWrapper.setUseJwksString(true); + configWrapper.setUseJwksUrl(false); + if (publicKeyJWk == null) { return false; - } else { - PublicKey publicKey = JWKParser.create(publicKeyJWk).toPublicKey(); - String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); - CertificateRepresentation rep = new CertificateRepresentation(); - rep.setPublicKey(publicKeyPem); - rep.setKid(publicKeyJWk.getKeyId()); - CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, rep, JWTClientAuthenticator.ATTR_PREFIX); - - configWrapper.setUseJwksUrl(false); - - return true; } - } else { + PublicKey publicKey = JWKParser.create(publicKeyJWk).toPublicKey(); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); + CertificateRepresentation rep = new CertificateRepresentation(); + rep.setPublicKey(publicKeyPem); + rep.setKid(publicKeyJWk.getKeyId()); + CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, rep, JWTClientAuthenticator.ATTR_PREFIX); + + return true; + } else if (clientOIDC.getJwksUri() != null) { configWrapper.setUseJwksUrl(true); configWrapper.setJwksUrl(clientOIDC.getJwksUri()); + configWrapper.setUseJwksString(false); return true; } - } + return false; + + } public static OIDCClientRepresentation toExternalResponse(KeycloakSession session, ClientRepresentation client, URI uri) { OIDCClientRepresentation response = new OIDCClientRepresentation(); @@ -318,6 +325,13 @@ public class DescriptionConverter { if (config.isUseJwksUrl()) { response.setJwksUri(config.getJwksUrl()); } + if (config.isUseJwksString()) { + try { + response.setJwks(JsonSerialization.readValue(config.getJwksString(), JSONWebKeySet.class)); + } catch (IOException e) { + throw new ClientRegistrationException("Illegal jwks format"); + } + } // KEYCLOAK-6771 Certificate Bound Token // https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5 if (config.isUseMtlsHokToken()) { diff --git a/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java b/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java index 2e9ffa647e..87961f24dd 100644 --- a/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java +++ b/services/src/main/java/org/keycloak/validation/DefaultClientValidationProvider.java @@ -20,6 +20,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.protocol.ProtocolMapperConfigException; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.grants.ciba.CibaClientValidation; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper; import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils; import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator; @@ -115,6 +116,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider validateUrls(context); validatePairwiseInClientModel(context); new CibaClientValidation(context).validate(); + validateJwks(context); return context.toResult(); } @@ -217,4 +219,12 @@ public class DefaultClientValidationProvider implements ClientValidationProvider } } + private void validateJwks(ValidationContext context) { + ClientModel client = context.getObjectToValidate(); + + if (Boolean.parseBoolean(client.getAttribute(OIDCConfigAttributes.USE_JWKS_URL)) + && Boolean.parseBoolean(client.getAttribute(OIDCConfigAttributes.USE_JWKS_STRING))) { + context.addError("jwksUrl", "Illegal to use both jwks_uri and jwks_string", "duplicatedJwksSettings"); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java index f19beb9699..68c9329955 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java @@ -372,7 +372,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { // Utilities for Request Object retrieved by reference from jwks_uri - protected KeyPair setupJwks(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { + protected KeyPair setupJwksUrl(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { // generate and register client keypair TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); oidcClientEndpointsResource.generateKeys(algorithm); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index dc62536888..bb8a079d7d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -2013,7 +2013,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId); ClientRepresentation clientRep = clientResource.toRepresentation(); - KeyPair keyPair = setupJwks(org.keycloak.crypto.Algorithm.ES256, clientRep, clientResource); + KeyPair keyPair = setupJwksUrl(org.keycloak.crypto.Algorithm.ES256, clientRep, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -2103,7 +2103,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), clientId); ClientRepresentation clientRep = clientResource.toRepresentation(); - KeyPair keyPair = setupJwks(org.keycloak.crypto.Algorithm.RS256, clientRep, clientResource); + KeyPair keyPair = setupJwksUrl(org.keycloak.crypto.Algorithm.RS256, clientRep, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java index b04fec9c8f..8908f5a143 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java @@ -232,6 +232,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe // Update client with some bad JWKS_URI response.setJwksUri("http://localhost:4321/non-existent"); + response.setJwks(null); reg.auth(Auth.token(response.getRegistrationAccessToken())) .oidc().update(response); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 26bf6e30b6..40e44fa8c7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -61,6 +61,7 @@ import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; @@ -88,6 +89,7 @@ import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; import java.io.ByteArrayInputStream; import java.io.File; @@ -297,18 +299,33 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { } @Test - public void testCodeToTokenRequestSuccessES256() throws Exception { - testCodeToTokenRequestSuccess(Algorithm.ES256); + public void testCodeToTokenRequestSuccessES256usingJwksUri() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.ES256, true); } @Test - public void testCodeToTokenRequestSuccessRS256() throws Exception { - testCodeToTokenRequestSuccess(Algorithm.RS256); + public void testCodeToTokenRequestSuccessES256usingJwks() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.ES256, false); } @Test - public void testCodeToTokenRequestSuccessPS256() throws Exception { - testCodeToTokenRequestSuccess(Algorithm.PS256); + public void testCodeToTokenRequestSuccessRS256usingJwksUri() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.RS256, true); + } + + @Test + public void testCodeToTokenRequestSuccessRS256usingJwks() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.RS256, false); + } + + @Test + public void testCodeToTokenRequestSuccessPS256usingJwksUri() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.PS256, true); + } + + @Test + public void testCodeToTokenRequestSuccessPS256usingJwks() throws Exception { + testCodeToTokenRequestSuccess(Algorithm.PS256, false); } @Test @@ -328,7 +345,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setTokenEndpointAuthSigningAlg(Algorithm.ES256); clientResource.update(clientRep); - testCodeToTokenRequestSuccess(Algorithm.ES256); + testCodeToTokenRequestSuccess(Algorithm.ES256, true); } catch (Exception e) { Assert.fail(); } finally { @@ -352,7 +369,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { String clientSignedToken; try { // setup Jwks - KeyPair keyPair = setupJwks(alg, clientRepresentation, clientResource); + KeyPair keyPair = setupJwksUrl(alg, clientRepresentation, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -370,17 +387,22 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { return clientSignedToken; } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); } } - private void testCodeToTokenRequestSuccess(String algorithm) throws Exception { + private void testCodeToTokenRequestSuccess(String algorithm, boolean useJwksUri) throws Exception { ClientRepresentation clientRepresentation = app2; ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId()); clientRepresentation = clientResource.toRepresentation(); try { // setup Jwks - KeyPair keyPair = setupJwks(algorithm, clientRepresentation, clientResource); + KeyPair keyPair; + if (useJwksUri) { + keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource); + } else { + keyPair = setupJwks(algorithm, clientRepresentation, clientResource); + } PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -402,8 +424,12 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID) .assertEvent(); } finally { - // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + // Revert jwks settings + if (useJwksUri) { + revertJwksUriSettings(clientRepresentation, clientResource); + } else { + revertJwksSettings(clientRepresentation, clientResource); + } } } @@ -438,7 +464,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { try { // setup Jwks String signingAlgorithm = Algorithm.PS256; - KeyPair keyPair = setupJwks(signingAlgorithm, false, clientRepresentation, clientResource); + KeyPair keyPair = setupJwksUrl(signingAlgorithm, false, clientRepresentation, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -449,7 +475,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { assertEquals(200, response.getStatusCode()); } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -461,7 +487,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { try { // send a JWS using the default algorithm String signingAlgorithm = Algorithm.RS256; - KeyPair keyPair = setupJwks(signingAlgorithm, false, clientRepresentation, clientResource); + KeyPair keyPair = setupJwksUrl(signingAlgorithm, false, clientRepresentation, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); oauth.clientId("client2"); @@ -483,7 +509,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { assertEquals("invalid signature algorithm", response.getErrorDescription()); } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setTokenEndpointAuthSigningAlg(null); clientResource.update(clientRepresentation); } @@ -510,7 +536,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { clientRepresentation = clientResource.toRepresentation(); try { // setup Jwks - KeyPair keyPair = setupJwks(algorithm, clientRepresentation, clientResource); + KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -536,7 +562,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .assertEvent(); } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -870,22 +896,27 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { ClientRepresentation clientRepresentation = app2; ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId()); clientRepresentation = clientResource.toRepresentation(); + try { + KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl()); - KeyPair keyPair = setupJwks(Algorithm.PS256, clientRepresentation, clientResource); - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl()); + assertion.audience(endpointUrl); - assertion.audience(endpointUrl); + List parameters = new LinkedList(); + 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, + createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion))); - List parameters = new LinkedList(); - 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, createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion))); - - try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) { - OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); - assertNotNull(response.getAccessToken()); + try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) { + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + assertNotNull(response.getAccessToken()); + } + } finally { + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -895,21 +926,27 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId()); clientRepresentation = clientResource.toRepresentation(); - KeyPair keyPair = setupJwks(Algorithm.PS256, clientRepresentation, clientResource); - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl()); + try { + KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource); + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl()); - assertion.audience("https://as.other.org"); + assertion.audience("https://as.other.org"); - List parameters = new LinkedList(); - 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, createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion))); + List parameters = new LinkedList(); + 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, + createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion))); - try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) { - OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); - assertNull(response.getAccessToken()); + try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) { + OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); + assertNull(response.getAccessToken()); + } + } finally { + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -1092,7 +1129,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { clientRepresentation = clientResource.toRepresentation(); try { // setup Jwks - KeyPair keyPair = setupJwks(algorithm, clientRepresentation, clientResource); + KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); @@ -1118,7 +1155,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .assertEvent(); } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -1133,7 +1170,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { clientRepresentation = clientResource.toRepresentation(); try { // setup Jwks - setupJwks(algorithm, clientRepresentation, clientResource); + setupJwksUrl(algorithm, clientRepresentation, clientResource); // test oauth.clientId("client2"); @@ -1151,7 +1188,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { .assertEvent(); } finally { // Revert jwks_url settings - revertJwksSettings(clientRepresentation, clientResource); + revertJwksUriSettings(clientRepresentation, clientResource); } } @@ -1319,11 +1356,11 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { return keyStore; } - private KeyPair setupJwks(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { - return setupJwks(algorithm, true, clientRepresentation, clientResource); + private KeyPair setupJwksUrl(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { + return setupJwksUrl(algorithm, true, clientRepresentation, clientResource); } - private KeyPair setupJwks(String algorithm, boolean advertiseJWKAlgorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { + private KeyPair setupJwksUrl(String algorithm, boolean advertiseJWKAlgorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception { // generate and register client keypair TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); oidcClientEndpointsResource.generateKeys(algorithm, advertiseJWKAlgorithm); @@ -1342,12 +1379,39 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest { return keyPair; } - private void revertJwksSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) { + 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 + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(true); + JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation) + .setJwksString(JsonSerialization.writeValueAsString(keySet)); + clientResource.update(clientRepresentation); + + // set time offset, so that new keys are downloaded + setTimeOffset(20); + + return keyPair; + } + + private void revertJwksUriSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) { OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(false); OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(null); clientResource.update(clientRepresentation); } + private void revertJwksSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) { + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(false); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksString(null); + clientResource.update(clientRepresentation); + } + 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); diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index dfc2b88324..436b2c1014 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -487,9 +487,13 @@ kid.tooltip=KID (Key ID) of the client public key from imported JWKS. token-endpoint-auth-signing-alg=Signature Algorithm token-endpoint-auth-signing-alg.tooltip=JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm. use-jwks-url=Use JWKS URL -use-jwks-url.tooltip=If the switch is on, client public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when client generates new keypair. If the switch is off, public key (or certificate) from the Keycloak DB is used, so when client keypair changes, you always need to import new key (or certificate) to the Keycloak DB as well. +use-jwks-url.tooltip=If the switch is on, client public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when client generates new keypair. If the switch is off, public key (or certificate) from the Keycloak DB is used, so when client keypair changes, you always need to import new key (or certificate) to the Keycloak DB as well. This switch is mutually exclusive with the switch "Use JWKS". jwks-url=JWKS URL jwks-url.tooltip=URL where client keys in JWK format are stored. See JWK specification for more details. If you use Keycloak client adapter with "jwt" credential, you can use URL of your app with '/k_jwks' suffix. For example 'http://www.myhost.com/myapp/k_jwks' . +use-jwks-string=Use JWKS +use-jwks-string.tooltip=If the switch is on, client public keys will be configurable in JWKS. This switch is mutually exclusive with the switch "Use JWKS URL". +jwks-string=JWKS +jwks-string.tooltip=Client keys in JWK format. See JWK specification for more details. pkce-enabled=Use PKCE pkce-enabled.tooltip=Use PKCE (Proof of Key-code exchange) for IdP Brokering pkce-method=PKCE Method diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties index e3f8ccaf36..d26b8cc19f 100644 --- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties @@ -41,6 +41,8 @@ pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI. pairwiseFailedToGetRedirectURIs=Failed to get redirect URIs from the Sector Identifier URI. pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI. +duplicatedJwksSettings=The "Use JWKS" switch and the switch "Use JWKS URL" cannot be ON at the same time. + error-invalid-value=Invalid value. error-invalid-blank=Please specify value. error-empty=Please specify value. diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 3c9e3eb596..ff9dfce604 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -619,8 +619,26 @@ module.controller('ClientOidcKeyCtrl', function($scope, $location, realm, client } } - $scope.switchChange = function() { + if ($scope.client.attributes["use.jwks.string"]) { + if ($scope.client.attributes["use.jwks.string"] == "true") { + $scope.useJwksString = true; + } else { + $scope.useJwksString = false; + } + } + + $scope.jwksUrlSwitchChange = function() { $scope.changed = true; + if ($scope.useJwksUrl == false) { + $scope.useJwksString = false; + } + } + + $scope.jwksStringSwitchChange = function() { + $scope.changed = true; + if ($scope.useJwksString == false) { + $scope.useJwksUrl = false; + } } $scope.save = function() { @@ -631,6 +649,12 @@ module.controller('ClientOidcKeyCtrl', function($scope, $location, realm, client $scope.client.attributes["use.jwks.url"] = "false"; } + if ($scope.useJwksString == true) { + $scope.client.attributes["use.jwks.string"] = "true"; + } else { + $scope.client.attributes["use.jwks.string"] = "false"; + } + Client.update({ realm : realm.realm, client : client.id diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-oidc-keys.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-oidc-keys.html index ba4a8036f8..c1d4d09a31 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-oidc-keys.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-oidc-keys.html @@ -11,7 +11,7 @@
-
{{:: 'use-jwks-url.tooltip' | translate}} @@ -26,7 +26,25 @@ {{:: 'jwks-url.tooltip' | translate}}
-
+
+ +
+ +
+ {{:: 'use-jwks-string.tooltip' | translate}} +
+ +
+ +
+ +
+ {{:: 'jwks-string.tooltip' | translate}} +
+ +
@@ -77,8 +95,8 @@
- +