KEYCLOAK-18341 Support JWKS OAuth2 Client Metadata in the "by value" key loading method

This commit is contained in:
Yoshiyuki Tabata 2021-06-03 17:47:34 +09:00 committed by Marek Posolda
parent 3c19fae88b
commit b31b60fffe
13 changed files with 245 additions and 82 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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";

View file

@ -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()) {

View file

@ -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<ClientModel> 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");
}
}
}

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -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<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,
createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion)));
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, 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<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, createSignledRequestToken(privateKey, publicKey, Algorithm.PS256, assertion)));
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,
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<String, String> 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<String, String> 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);

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -11,7 +11,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="useJwksUrl">{{:: 'use-jwks-url' | translate}}</label>
<div class="col-sm-6">
<input ng-model="useJwksUrl" name="useJwksUrl" id="useJwksUrl" ng-click="switchChange()" onoffswitch
<input ng-model="useJwksUrl" name="useJwksUrl" id="useJwksUrl" ng-click="jwksUrlSwitchChange()" onoffswitch
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'use-jwks-url.tooltip' | translate}}</kc-tooltip>
@ -26,7 +26,25 @@
<kc-tooltip>{{:: 'jwks-url.tooltip' | translate}}</kc-tooltip>
</div>
<div data-ng-show="!useJwksUrl">
<div class="form-group">
<label class="col-md-2 control-label" for="useJwksString">{{:: 'use-jwks-string' | translate}}</label>
<div class="col-sm-6">
<input ng-model="useJwksString" name="useJwksString" id="useJwksString" ng-click="jwksStringSwitchChange()"
onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'use-jwks-string.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="useJwksString">
<label class="col-md-2 control-label" for="jwksString">{{:: 'jwks-string' | translate}}</label>
<div class="col-sm-10">
<textarea type="text" id="jwksString" name="jwksString" class="form-control" rows="5" kc-select-action="click"
data-ng-model="client.attributes['jwks.string']"></textarea>
</div>
<kc-tooltip>{{:: 'jwks-string.tooltip' | translate}}</kc-tooltip>
</div>
<div data-ng-show="!useJwksUrl && !useJwksString">
<div class="form-group" data-ng-show="signingKeyInfo.certificate">
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
@ -77,8 +95,8 @@
<div class="col-md-10 col-md-offset-2" data-ng-show="client.access.configure">
<button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">{{::
'gen-new-keys-and-cert' | translate}}</button>
<button data-ng-disabled="useJwksUrl" class="btn btn-default" type="submit" data-ng-click="importCertificate()">{{::
'import-certificate' | translate}}</button>
<button data-ng-disabled="useJwksUrl || useJwksString" class="btn btn-default" type="submit"
data-ng-click="importCertificate()">{{:: 'import-certificate' | translate}}</button>
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>