KEYCLOAK-18341 Support JWKS OAuth2 Client Metadata in the "by value" key loading method
This commit is contained in:
parent
3c19fae88b
commit
b31b60fffe
13 changed files with 245 additions and 82 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,22 +239,27 @@ 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();
|
||||
|
@ -260,17 +267,17 @@ public class DescriptionConverter {
|
|||
rep.setKid(publicKeyJWk.getKeyId());
|
||||
CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, rep, JWTClientAuthenticator.ATTR_PREFIX);
|
||||
|
||||
configWrapper.setUseJwksUrl(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
} 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()) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,10 +424,14 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
|||
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
// Revert jwks_url settings
|
||||
// Revert jwks settings
|
||||
if (useJwksUri) {
|
||||
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||
} else {
|
||||
revertJwksSettings(clientRepresentation, clientResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectGrantRequestSuccess() throws Exception {
|
||||
|
@ -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,8 +896,8 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
|||
ClientRepresentation clientRepresentation = app2;
|
||||
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||
clientRepresentation = clientResource.toRepresentation();
|
||||
|
||||
KeyPair keyPair = setupJwks(Algorithm.PS256, clientRepresentation, clientResource);
|
||||
try {
|
||||
KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
|
||||
PublicKey publicKey = keyPair.getPublic();
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
|
||||
|
@ -880,13 +906,18 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
|||
|
||||
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)));
|
||||
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());
|
||||
}
|
||||
} finally {
|
||||
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -895,7 +926,8 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
|||
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||
clientRepresentation = clientResource.toRepresentation();
|
||||
|
||||
KeyPair keyPair = setupJwks(Algorithm.PS256, clientRepresentation, clientResource);
|
||||
try {
|
||||
KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
|
||||
PublicKey publicKey = keyPair.getPublic();
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
|
||||
|
@ -904,13 +936,18 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
|||
|
||||
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)));
|
||||
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());
|
||||
}
|
||||
} finally {
|
||||
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue