keycloak-aplcache
Changes
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 16(+16 -0)
services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java 49(+43 -6)
services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverterFactory.java 60(+60 -0)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 87(+81 -6)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java 10(+5 -5)
services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory 2(+1 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java 7(+7 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 2(+1 -1)
Details
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWK.java b/core/src/main/java/org/keycloak/jose/jwk/JWK.java
index ce153a4..e75963a 100755
--- a/core/src/main/java/org/keycloak/jose/jwk/JWK.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWK.java
@@ -37,8 +37,20 @@ public class JWK {
public static final String PUBLIC_KEY_USE = "use";
- public static final String SIG_USE = "sig";
- public static final String ENCRYPTION_USE = "enc";
+ public enum Use {
+ SIG("sig"),
+ ENCRYPTION("enc");
+
+ private String str;
+
+ Use(String str) {
+ this.str = str;
+ }
+
+ public String asString() {
+ return str;
+ }
+ }
@JsonProperty(KEY_ID)
private String keyId;
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
index 1bad9cf..7a31f72 100755
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
@@ -67,7 +67,7 @@ public class JWKParser {
public PublicKey toPublicKey() {
String algorithm = jwk.getKeyType();
- if (RSAPublicJWK.RSA.equals(algorithm)) {
+ if (isAlgorithmSupported(algorithm)) {
BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));
@@ -81,4 +81,8 @@ public class JWKParser {
}
}
+ public boolean isAlgorithmSupported(String algorithm) {
+ return RSAPublicJWK.RSA.equals(algorithm);
+ }
+
}
diff --git a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
index 26082db..d7f9939 100644
--- a/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/oidc/OIDCClientRepresentation.java
@@ -18,6 +18,7 @@
package org.keycloak.representations.oidc;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import org.keycloak.jose.jwk.JSONWebKeySet;
import java.util.List;
@@ -61,7 +62,7 @@ public class OIDCClientRepresentation {
private String jwks_uri;
- private String jwks;
+ private JSONWebKeySet jwks;
private String sector_identifier_uri;
@@ -240,11 +241,11 @@ public class OIDCClientRepresentation {
this.jwks_uri = jwks_uri;
}
- public String getJwks() {
+ public JSONWebKeySet getJwks() {
return jwks;
}
- public void setJwks(String jwks) {
+ public void setJwks(JSONWebKeySet jwks) {
this.jwks = jwks;
}
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index 062dc9b..e346fe6 100755
--- a/core/src/test/java/org/keycloak/JsonParserTest.java
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -135,6 +135,14 @@ public class JsonParserTest {
Assert.assertEquals(3600, clientRep.getDefaultMaxAge().intValue());
Assert.assertEquals(1, clientRep.getRedirectUris().size());
Assert.assertEquals("https://op.certification.openid.net:60720/authz_cb", clientRep.getRedirectUris().get(0));
+ Assert.assertNull(clientRep.getJwks());
+ }
+
+ @Test
+ public void testReadOIDCClientRepWithJWKS() throws IOException {
+ String stringRep = "{\"token_endpoint_auth_method\": \"private_key_jwt\", \"subject_type\": \"public\", \"jwks_uri\": null, \"jwks\": {\"keys\": [{\"use\": \"enc\", \"e\": \"AQAB\", \"d\": \"lZQv0_81euRLeUYU84Aodh0ar7ymDlzWP5NMra4Jklkb-lTBWkI-u4RMsPqGYyW3KHRoL_pgzZXSzQx8RLQfER6timRWb--NxMMKllZubByU3RqH2ooNuocJurspYiXkznPW1Mg9DaNXL0C2hwWPQHTeUVISpjgi5TCOV1ccWVyksFruya_VNL1CIByB-L0GL1rqbKv32cDwi2A3_jJa61cpzfLSIBe-lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N-poleV8mBfMqBB5fWwy_ZTFCpmQ5AywGmctaik_wNhMoWuA4tUfY6_1LdKld-5Cjq55eLtuJjtvuQ\", \"n\": \"tx3Hjdbc19lkTiohbJrNj4jf2_90MEE122CRrwtFu6saDywKcG7Bi7w2FMAK2oTkuWfqhWRb5BEGmnSXdiCEPO5d-ytqP3nwlZXHaCDYscpP8bB4YLhvCn7R8Efw6gwQle24QPRP3lYoFeuUbDUq7GKA5SfaZUvWoeWjqyLIaBspKQsC26_Umx1E4IXLrMSL6nkRnrYcVZBAXrYCeTP1XtsV38_lZVJfHSaJaUy4PKaj3yvgm93EV2CXybPti7CCMXZ34VqqWiF64pQjZsPu3ZTr7ha_TTQq499-zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q\", \"q\": \"1q-r-bmMFbIzrLK2U3elksZq8CqUqZxlSfkGMZuVkxgYMS-e4FPzEp2iirG-eO11aa0cpMMoBdTnVdGJ_ZUR93w0lGf9XnQAJqxP7eOsrUoiW4VWlWH4WfOiLgpO-pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc\", \"p\": \"2lrYPppRbcQWu4LtWN6tOVUrtCOPv1eLTKTc7q8vCMcem1Ox5QFB7KnUtNZ5Ni7wnZUeVDfimNebtjNsGvDSrpgIlo9dEnFBQsQIkzZ2SkoYfgmF8hNdi6P-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc\", \"kid\": \"a0\", \"kty\": \"RSA\"}, {\"use\": \"sig\", \"e\": \"AQAB\", \"d\": \"DodXDEtkovWWGsMEXYy_nEEMCWyROMOebCnCv0ey3i4M4bh2dmwqgz0e-IKQAFlGiMkidGL1lNbq0uFS04FbuRAR06dYw1cbrNbDdhrWFxKTd1L5D9p-x-gW-YDWhpI8rUGRa76JXkOSxZUbg09_QyUd99CXAHh-FXi_ZkIKD8hK6FrAs68qhLf8MNkUv63DTduw7QgeFfQivdopePxyGuMk5n8veqwsUZsklQkhNlTYQqeM1xb2698ZQcNYkl0OssEsSJKRjXt-LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ\", \"n\": \"zfZzttF7HmnTYwSMPdxKs5AoczbNS2mOPz-tN1g4ljqI_F1DG8cgQDcN_VDufxoFGRERo2FK6WEN41LhbGEyP6uL6wW6Cy29qE9QZcvY5mXrncndRSOkNcMizvuEJes_fMYrmP_lPiC6kWiqItTk9QBWqJfiYKhCx9cSDXsBmJXn3KWQCVHvj1ANFWW0CWLMKlWN-_NMNLIWJN_pEAocTZMzxSFBK1b5_5J8ZS7hfWRF6MQmjsJcz2jzA21SQZNpre3kwnTGRSwo05sAS-TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw\", \"q\": \"5E5XKK5njT-zzRqqTeY2tgP9PJBACeaH_xQRHZ_1ydE7tVd7HdgdaEHfQ1jvKIHFkknWWOBAY1mlBc4YDirLShB_voShD8C-Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU\", \"p\": \"5vJHCSM3H3q4RltYzENC9RyZZV8EUmpkv9moyguT5t-BUGA-T4W_FGIxzOPXRWOckIplKkoDKhavUeNmTZMCUcue0nkICSJpvNE4Nb2p5PZk_QqSdQNvCasQtdojEG0AmfVD85SU551CYxJdLdDFOqyK2entpMr8lhokem189As\", \"kid\": \"a1\", \"kty\": \"RSA\"}, {\"d\": \"S4_OufhLBgXFMgIDMI1zlVe2uCExpcEAQ80J_lXfS8I\", \"use\": \"sig\", \"crv\": \"P-256\", \"kty\": \"EC\", \"y\": \"DBdNyq30mXmUs_BIvKMqaTTNO7HDhCi0YiC8GciwNYk\", \"x\": \"cYwzBoyjRjxj334bRTqanONf7DUYK-6TgiuN0DixJAk\", \"kid\": \"a2\"}, {\"d\": \"33TnYgdJtWAiVosKqUnz0zSmvWTbsx5-6pceynW6Xck\", \"use\": \"enc\", \"crv\": \"P-256\", \"kty\": \"EC\", \"y\": \"Cula95Eix1Ia77St3OULe6-UKWs5I06nmdfUzhXUQTs\", \"x\": \"wk8HBVxNNzj1gJBxPmmx9XYW1L61ObBGzxpRa6_OqWU\", \"kid\": \"a3\"}]}, \"application_type\": \"web\", \"contacts\": [\"roland.hedberg@umu.se\"], \"post_logout_redirect_uris\": [\"https://op.certification.openid.net:60784/logout\"], \"redirect_uris\": [\"https://op.certification.openid.net:60784/authz_cb\"], \"response_types\": [\"code\"], \"require_auth_time\": true, \"grant_types\": [\"authorization_code\"], \"default_max_age\": 3600}";
+ OIDCClientRepresentation clientRep = JsonSerialization.readValue(stringRep, OIDCClientRepresentation.class);
+ Assert.assertNotNull(clientRep.getJwks());
}
}
diff --git a/server-spi/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java b/server-spi/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
index 9c3e098..8e3e658 100644
--- a/server-spi/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
+++ b/server-spi/src/main/java/org/keycloak/authentication/ClientAuthenticatorFactory.java
@@ -19,6 +19,7 @@ package org.keycloak.authentication;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.provider.ProviderConfigProperty;
@@ -60,4 +61,12 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
*/
Map<String, Object> getAdapterConfiguration(ClientModel client);
+ /**
+ * Get authentication methods for the specified protocol
+ *
+ * @param loginProtocol corresponds to {@link org.keycloak.protocol.LoginProtocolFactory#getId}
+ * @return name of supported client authenticator methods in the protocol specific "language"
+ */
+ Set<String> getProtocolAuthenticatorMethods(String loginProtocol);
+
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index 957e35d..4516de4 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -19,9 +19,12 @@ package org.keycloak.authentication.authenticators.client;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
@@ -33,6 +36,7 @@ import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ServicesLogger;
@@ -179,4 +183,16 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
public String getId() {
return PROVIDER_ID;
}
+
+ @Override
+ public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+ if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
+ Set<String> results = new LinkedHashSet<>();
+ results.add(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
+ results.add(OIDCLoginProtocol.CLIENT_SECRET_POST);
+ return results;
+ } else {
+ return Collections.emptySet();
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index 2ee2fcb..fcda410 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -22,9 +22,11 @@ import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -39,6 +41,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
@@ -59,6 +62,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
public static final String PROVIDER_ID = "client-jwt";
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
+ public static final String PUBLIC_KEY_ATTR = "jwt.credential.publicKey";
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
@@ -116,15 +120,12 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
}
// Get client key and validate signature
- String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR);
- if (encodedCertificate == null) {
- Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + clientId + "' doesn't have certificate configured");
- context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
+ PublicKey clientPublicKey = getSignatureValidationKey(client, context);
+ if (clientPublicKey == null) {
+ // Error response already set to context
return;
}
- X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
- PublicKey clientPublicKey = clientCert.getPublicKey();
boolean signatureValid;
try {
signatureValid = RSAProvider.verify(jws, clientPublicKey);
@@ -159,6 +160,31 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
}
}
+ protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
+ String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR);
+ String encodedPublicKey = client.getAttribute(PUBLIC_KEY_ATTR);
+ if (encodedCertificate == null && encodedPublicKey == null) {
+ Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured");
+ context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
+ return null;
+ }
+
+ // TODO: Needs to be improved. Maybe just publicKey should be saved and existing clients migrated from certificate to publicKey...
+ if (encodedCertificate != null && encodedPublicKey != null) {
+ Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured");
+ context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
+ return null;
+ }
+
+ // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
+ if (encodedCertificate != null) {
+ X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
+ return clientCert.getPublicKey();
+ } else {
+ return KeycloakModelUtils.getPublicKey(encodedPublicKey);
+ }
+ }
+
@Override
public String getDisplayType() {
return "Signed Jwt";
@@ -209,4 +235,15 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
public String getId() {
return PROVIDER_ID;
}
+
+ @Override
+ public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+ if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
+ Set<String> results = new HashSet<>();
+ results.add(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+ return results;
+ } else {
+ return Collections.emptySet();
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
index a268ebd..82c2cdd 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -16,14 +16,14 @@
*/
package org.keycloak.broker.oidc;
-import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.jose.jwk.JWK;
-import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
+import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.protocol.oidc.utils.JWKSUtils;
+import org.keycloak.services.ServicesLogger;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -36,6 +36,8 @@ import java.util.Map;
*/
public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory<OIDCIdentityProvider> {
+ private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
public static final String PROVIDER_ID = "oidc";
@Override
@@ -59,7 +61,7 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
}
protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
- OIDCConfigurationRepresentation rep = null;
+ OIDCConfigurationRepresentation rep;
try {
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
} catch (IOException e) {
@@ -72,31 +74,24 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
config.setTokenUrl(rep.getTokenEndpoint());
config.setUserInfoUrl(rep.getUserinfoEndpoint());
if (rep.getJwksUri() != null) {
- String uri = rep.getJwksUri();
- String keySetString = null;
- try {
- keySetString = SimpleHttp.doGet(uri).asString();
- JSONWebKeySet keySet = JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
- for (JWK jwk : keySet.getKeys()) {
- JWKParser parse = JWKParser.create(jwk);
- if (parse.getJwk().getPublicKeyUse().equals(JWK.SIG_USE) && keyTypeSupported(jwk.getKeyType())) {
- PublicKey key = parse.toPublicKey();
- config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
- config.setValidateSignature(true);
- break;
- }
-
- }
- } catch (IOException e) {
- throw new RuntimeException("Failed to query JWKSet from: " + uri, e);
- }
-
+ sendJwksRequest(rep, config);
}
return config.getConfig();
}
- protected static boolean keyTypeSupported(String type) {
- return type != null && type.equals("RSA");
+ protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
+ try {
+ JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri());
+ PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
+ if (key == null) {
+ logger.supportedJwkNotFound(JWK.Use.SIG.asString());
+ } else {
+ config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
+ config.setValidateSignature(true);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to query JWKSet from: " + rep.getJwksUri(), e);
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
index c55b482..392b3ce 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
@@ -17,11 +17,8 @@
package org.keycloak.protocol.oidc;
-import org.keycloak.Config;
import org.keycloak.exportimport.ClientDescriptionConverter;
-import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.oidc.DescriptionConverter;
@@ -32,46 +29,28 @@ import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class OIDCClientDescriptionConverter implements ClientDescriptionConverter, ClientDescriptionConverterFactory {
+public class OIDCClientDescriptionConverter implements ClientDescriptionConverter {
- public static final String ID = "openid-connect";
+ private final KeycloakSession session;
- @Override
- public boolean isSupported(String description) {
- description = description.trim();
- return (description.startsWith("{") && description.endsWith("}") && description.contains("\"redirect_uris\""));
+ public OIDCClientDescriptionConverter(KeycloakSession session) {
+ this.session = session;
}
+
@Override
public ClientRepresentation convertToInternal(String description) {
try {
OIDCClientRepresentation clientOIDC = JsonSerialization.readValue(description, OIDCClientRepresentation.class);
- return DescriptionConverter.toInternal(clientOIDC);
+ return DescriptionConverter.toInternal(session, clientOIDC);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
- public ClientDescriptionConverter create(KeycloakSession session) {
- return this;
- }
-
- @Override
- public void init(Config.Scope config) {
- }
-
- @Override
- public void postInit(KeycloakSessionFactory factory) {
- }
-
- @Override
public void close() {
}
- @Override
- public String getId() {
- return ID;
- }
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverterFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverterFactory.java
new file mode 100644
index 0000000..fe29f50
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverterFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.Config;
+import org.keycloak.exportimport.ClientDescriptionConverter;
+import org.keycloak.exportimport.ClientDescriptionConverterFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCClientDescriptionConverterFactory implements ClientDescriptionConverterFactory {
+
+ public static final String ID = "openid-connect";
+
+ @Override
+ public boolean isSupported(String description) {
+ description = description.trim();
+ return (description.startsWith("{") && description.endsWith("}") && description.contains("\"redirect_uris\""));
+ }
+
+ @Override
+ public ClientDescriptionConverter create(KeycloakSession session) {
+ return new OIDCClientDescriptionConverter(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 7ad2354..3c4c3aa 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -77,6 +77,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String PROMPT_VALUE_CONSENT = "consent";
public static final String PROMPT_VALUE_SELECT_ACCOUNT = "select_account";
+ // Client authentication methods
+ public static final String CLIENT_SECRET_BASIC = "client_secret_basic";
+ public static final String CLIENT_SECRET_POST = "client_secret_post";
+ public static final String CLIENT_SECRET_JWT = "client_secret_jwt";
+ public static final String PRIVATE_KEY_JWT = "private_key_jwt";
+
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
protected KeycloakSession session;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index c5e86ea..7afec32 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -19,7 +19,6 @@ package org.keycloak.protocol.oidc;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
-import org.keycloak.OAuth2Constants;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
@@ -31,7 +30,7 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
-import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
+import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.RealmsResource;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index 050795a..eb5b5b4 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -18,12 +18,15 @@
package org.keycloak.protocol.oidc;
import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.IDToken;
import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
@@ -33,6 +36,8 @@ import org.keycloak.wellknown.WellKnownProvider;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
+
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -51,9 +56,6 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_RESPONSE_MODES_SUPPORTED = list("query", "fragment", "form_post");
- // Should be rather retrieved dynamically based on available ClientAuthenticator providers?
- public static final List<String> DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED = list("client_secret_basic", "client_secret_post", "private_key_jwt");
-
public static final List<String> DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
// The exact list depends on protocolMappers
@@ -93,7 +95,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED);
- config.setTokenEndpointAuthMethodsSupported(DEFAULT_CLIENT_AUTH_METHODS_SUPPORTED);
+ config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
config.setTokenEndpointAuthSigningAlgValuesSupported(DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED);
config.setClaimsSupported(DEFAULT_CLAIMS_SUPPORTED);
@@ -120,4 +122,16 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
return s;
}
+ private List<String> getClientAuthMethodsSupported() {
+ List<String> result = new ArrayList<>();
+
+ List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(ClientAuthenticator.class);
+ for (ProviderFactory factory : providerFactories) {
+ ClientAuthenticatorFactory clientAuthFactory = (ClientAuthenticatorFactory) factory;
+ result.addAll(clientAuthFactory.getProtocolAuthenticatorMethods(OIDCLoginProtocol.LOGIN_PROTOCOL));
+ }
+
+ return result;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
index 6e5a309..a7ba7c3 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
@@ -17,15 +17,20 @@
package org.keycloak.protocol.oidc.utils;
+import java.util.List;
import java.util.Map;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.WebApplicationException;
@@ -70,6 +75,18 @@ public class AuthorizeClientUtil {
return processor;
}
+ public static ClientAuthenticatorFactory findClientAuthenticatorForOIDCAuthMethod(KeycloakSession session, String oidcAuthMethod) {
+ List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(ClientAuthenticator.class);
+ for (ProviderFactory factory : providerFactories) {
+ ClientAuthenticatorFactory clientAuthFactory = (ClientAuthenticatorFactory) factory;
+ if (clientAuthFactory.getProtocolAuthenticatorMethods(OIDCLoginProtocol.LOGIN_PROTOCOL).contains(oidcAuthMethod)) {
+ return clientAuthFactory;
+ }
+ }
+
+ return null;
+ }
+
public static class ClientAuthResult {
private final ClientModel client;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java
new file mode 100644
index 0000000..c856a81
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.utils;
+
+import java.io.IOException;
+import java.security.PublicKey;
+
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKParser;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JWKSUtils {
+
+ public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException {
+ String keySetString = SimpleHttp.doGet(jwksURI).asString();
+ return JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
+ }
+
+
+ public static PublicKey getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
+ for (JWK jwk : keySet.getKeys()) {
+ JWKParser parser = JWKParser.create(jwk);
+ if (parser.getJwk().getPublicKeyUse().equals(requestedUse.asString()) && parser.isAlgorithmSupported(jwk.getKeyType())) {
+ return parser.toPublicKey();
+ }
+ }
+
+ return null;
+ }
+}
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 b797282..fc7f86c 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -18,24 +18,37 @@
package org.keycloak.services.clientregistration.oidc;
import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.ClientAuthenticator;
+import org.keycloak.authentication.ClientAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
+import org.keycloak.protocol.oidc.utils.JWKSUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationException;
+import java.io.IOException;
import java.net.URI;
+import java.security.PublicKey;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DescriptionConverter {
- public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) throws ClientRegistrationException {
+ public static ClientRepresentation toInternal(KeycloakSession session, OIDCClientRepresentation clientOIDC) throws ClientRegistrationException {
ClientRepresentation client = new ClientRepresentation();
client.setClientId(clientOIDC.getClientId());
client.setName(clientOIDC.getClientName());
@@ -60,17 +73,79 @@ public class DescriptionConverter {
throw new ClientRegistrationException(iae.getMessage(), iae);
}
+ String authMethod = clientOIDC.getTokenEndpointAuthMethod();
+ ClientAuthenticatorFactory clientAuthFactory;
+ if (authMethod == null) {
+ clientAuthFactory = (ClientAuthenticatorFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, KeycloakModelUtils.getDefaultClientAuthenticatorType());
+ } else {
+ clientAuthFactory = AuthorizeClientUtil.findClientAuthenticatorForOIDCAuthMethod(session, authMethod);
+ }
+
+ if (clientAuthFactory == null) {
+ throw new ClientRegistrationException("Not found clientAuthenticator for requested token_endpoint_auth_method");
+ }
+ client.setClientAuthenticatorType(clientAuthFactory.getId());
+
+ // Externalize to ClientAuthenticator itself?
+ if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) {
+
+ PublicKey publicKey = retrievePublicKey(clientOIDC);
+ if (publicKey == null) {
+ throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
+ }
+
+ String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
+ if (client.getAttributes() == null) {
+ client.setAttributes(new HashMap<>());
+ }
+ client.getAttributes().put(JWTClientAuthenticator.PUBLIC_KEY_ATTR, publicKeyPem);
+ }
+
return client;
}
- public static OIDCClientRepresentation toExternalResponse(ClientRepresentation client, URI uri) {
+
+ private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) {
+ if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
+ throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config");
+ }
+
+ if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
+ throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks");
+ }
+
+ JSONWebKeySet keySet;
+ if (clientOIDC.getJwks() != null) {
+ keySet = clientOIDC.getJwks();
+ } else {
+ try {
+ keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri());
+ } catch (IOException ioe) {
+ throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe);
+ }
+ }
+
+ return JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
+ }
+
+
+ public static OIDCClientRepresentation toExternalResponse(KeycloakSession session, ClientRepresentation client, URI uri) {
OIDCClientRepresentation response = new OIDCClientRepresentation();
response.setClientId(client.getClientId());
- response.setClientSecret(client.getSecret());
- response.setClientSecretExpiresAt(0);
+
+ ClientAuthenticatorFactory clientAuth = (ClientAuthenticatorFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, client.getClientAuthenticatorType());
+ Set<String> oidcClientAuthMethods = clientAuth.getProtocolAuthenticatorMethods(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ if (oidcClientAuthMethods != null && !oidcClientAuthMethods.isEmpty()) {
+ response.setTokenEndpointAuthMethod(oidcClientAuthMethods.iterator().next());
+ }
+
+ if (client.getClientAuthenticatorType().equals(ClientIdAndSecretAuthenticator.PROVIDER_ID)) {
+ response.setClientSecret(client.getSecret());
+ response.setClientSecretExpiresAt(0);
+ }
+
response.setClientName(client.getName());
response.setClientUri(client.getBaseUrl());
- response.setClientSecret(client.getSecret());
response.setRedirectUris(client.getRedirectUris());
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
response.setRegistrationClientUri(uri.toString());
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
index 84dfc78..8bc1ecb 100755
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
@@ -53,10 +53,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
}
try {
- ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
client = create(client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
- clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
clientOIDC.setClientIdIssuedAt(Time.currentTime());
return Response.created(uri).entity(clientOIDC).build();
} catch (ClientRegistrationException cre) {
@@ -70,7 +70,7 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
@Produces(MediaType.APPLICATION_JSON)
public Response getOIDC(@PathParam("clientId") String clientId) {
ClientRepresentation client = get(clientId);
- OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(client, session.getContext().getUri().getRequestUri());
+ OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(session, client, session.getContext().getUri().getRequestUri());
return Response.ok(clientOIDC).build();
}
@@ -79,10 +79,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
@Consumes(MediaType.APPLICATION_JSON)
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
try {
- ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
client = update(clientId, client);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
- clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ clientOIDC = DescriptionConverter.toExternalResponse(session, client, uri);
return Response.ok(clientOIDC).build();
} catch (ClientRegistrationException cre) {
logger.clientRegistrationException(cre.getMessage());
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index 8aa4961..de7eef7 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -425,4 +425,9 @@ public interface ServicesLogger extends BasicLogger {
@LogMessage(level = ERROR)
@Message(id=95, value="Client is not allowed to initiate browser login with given response_type. %s flow is disabled for the client.")
void flowNotAllowed(String flowName);
+
+ @LogMessage(level = WARN)
+ @Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s")
+ void supportedJwkNotFound(String usage);
+
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory b/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory
index c9a536f..1ad9238 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory
@@ -16,5 +16,5 @@
#
org.keycloak.exportimport.KeycloakClientDescriptionConverter
-org.keycloak.protocol.oidc.OIDCClientDescriptionConverter
+org.keycloak.protocol.oidc.OIDCClientDescriptionConverterFactory
org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
index 2f8824d..83ee504 100755
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java
@@ -18,10 +18,12 @@
package org.keycloak.testsuite.forms;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
@@ -119,4 +121,9 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator
public String getId() {
return PROVIDER_ID;
}
+
+ @Override
+ public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
+ return Collections.emptySet();
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index a964e07..586351d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -40,7 +40,7 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
-import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
+import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 6ccb0d1..5d7d2c1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -17,32 +17,61 @@
package org.keycloak.testsuite.client;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert;
-
+import org.keycloak.testsuite.util.OAuthClient;
+import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
import javax.ws.rs.core.Response;
import static org.junit.Assert.*;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
+ private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
+ private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ super.addTestRealms(testRealms);
+ testRealms.get(0).setPrivateKey(PRIVATE_KEY);
+ testRealms.get(0).setPublicKey(PUBLIC_KEY);
+ }
+
@Before
public void before() throws Exception {
super.before();
@@ -122,6 +151,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
assertEquals("http://redirect", response.getRedirectUris().get(0));
assertEquals(Arrays.asList("code", "none"), response.getResponseTypes());
assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
+ assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
}
@Test
@@ -136,6 +166,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
assertNotNull(response.getClientSecret());
assertEquals(0, response.getClientSecretExpiresAt().intValue());
+ assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
}
@Test
@@ -174,4 +205,89 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
reg.oidc().delete(response);
}
+ @Test
+ public void createClientWithJWKS() throws Exception {
+ OIDCClientRepresentation clientRep = createRep();
+
+ clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+ clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+ // Corresponds to PRIVATE_KEY
+ JSONWebKeySet keySet = loadJson(getClass().getResourceAsStream("/clientreg-test/jwks.json"), JSONWebKeySet.class);
+ clientRep.setJwks(keySet);
+
+ OIDCClientRepresentation response = reg.oidc().create(clientRep);
+ Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
+ Assert.assertNull(response.getClientSecret());
+ Assert.assertNull(response.getClientSecretExpiresAt());
+
+ // Tries to authenticate client with privateKey JWT
+ String signedJwt = getClientSignedJWT(response.getClientId());
+ OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
+ Assert.assertEquals(200, accessTokenResponse.getStatusCode());
+ AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
+ Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
+ }
+
+ @Test
+ public void createClientWithJWKSURI() throws Exception {
+ OIDCClientRepresentation clientRep = createRep();
+
+ clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+ clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+ // Use the realmKey for client authentication too
+ clientRep.setJwksUri(oauth.getCertsUrl(REALM_NAME));
+
+ OIDCClientRepresentation response = reg.oidc().create(clientRep);
+ Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
+ Assert.assertNull(response.getClientSecret());
+ Assert.assertNull(response.getClientSecretExpiresAt());
+
+ // Tries to authenticate client with privateKey JWT
+ String signedJwt = getClientSignedJWT(response.getClientId());
+ OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
+ Assert.assertEquals(200, accessTokenResponse.getStatusCode());
+ AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
+ Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
+ }
+
+
+ // Client auth with signedJWT - helper methods
+
+ private String getClientSignedJWT(String clientId) {
+ String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
+
+ PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(PRIVATE_KEY);
+
+ JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
+ jwtProvider.setPrivateKey(privateKey);
+ jwtProvider.setTokenTimeout(10);
+ return jwtProvider.createSignedRequestToken(clientId, realmInfoUrl);
+
+ }
+
+
+ private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
+ List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+ parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+ parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+ HttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
+ return new OAuthClient.AccessTokenResponse(response);
+ }
+
+ private HttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
+ CloseableHttpClient client = new DefaultHttpClient();
+ try {
+ HttpPost post = new HttpPost(requestUrl);
+ UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ post.setEntity(formEntity);
+ return client.execute(post);
+ } finally {
+ oauth.closeClient(client);
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/jwks.json b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/jwks.json
new file mode 100644
index 0000000..25ba0dd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/jwks.json
@@ -0,0 +1,32 @@
+{"keys": [
+ {
+ "use": "enc",
+ "n": "tx3Hjdbc19lkTiohbJrNj4jf2_90MEE122CRrwtFu6saDywKcG7Bi7w2FMAK2oTkuWfqhWRb5BEGmnSXdiCEPO5d-ytqP3nwlZXHaCDYscpP8bB4YLhvCn7R8Efw6gwQle24QPRP3lYoFeuUbDUq7GKA5SfaZUvWoeWjqyLIaBspKQsC26_Umx1E4IXLrMSL6nkRnrYcVZBAXrYCeTP1XtsV38_lZVJfHSaJaUy4PKaj3yvgm93EV2CXybPti7CCMXZ34VqqWiF64pQjZsPu3ZTr7ha_TTQq499-zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q",
+ "e": "AQAB",
+ "kty": "RSA",
+ "kid": "a0"
+ },
+ {
+ "use":"sig",
+ "n":"q1awrk7QK24Gmcy9Yb4dMbS-ZnO6NDaj1Z2F5C74HMIgtwYyxsNbRhBqCWlw7kmkZZaG5udyQYY8d91Db_uc_1DBuJMrQVsYXjVSpy-hoKpTWmzGhXzyzwhfJAICp7Iu_TTKPp-ip0mPGHlJnnP6dr4ztjY7EgFXFhEDFYSd9S8",
+ "e":"AQAB",
+ "kty":"RSA",
+ "kid":"FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"
+ },
+ {
+ "use": "sig",
+ "crv": "P-256",
+ "kty": "EC",
+ "y": "HtxLgYFXpJSomE8cN7qCEHXvKuLGZMWbK1FiJLCRCW8",
+ "x": "PMtxvxd-owwLzE_cUlA4_nT_bWcdcfnlhFF3wh8Gl5o",
+ "kid": "a2"
+ },
+ {
+ "use": "enc",
+ "crv": "P-256",
+ "kty": "EC",
+ "y": "xJd7r3N8WSjTW7ebZySfYzJtWYHeWjF34u3-BxoPfs4",
+ "x": "KIWYBJU45adk20B99K_93qvVGaqumQKGauW_RTQPazY",
+ "kid": "a3"
+ }
+]}
\ No newline at end of file