Merge pull request #3143 from mposolda/oidc-dynamic2
KEYCLOAK-3424 Support for save JWKS in OIDC ClientRegistration endpoint
This commit is contained in:
commit
006a0f856e
33 changed files with 711 additions and 127 deletions
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.representations;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
|
@ -37,8 +37,20 @@ public class JWK {
|
||||||
|
|
||||||
public static final String PUBLIC_KEY_USE = "use";
|
public static final String PUBLIC_KEY_USE = "use";
|
||||||
|
|
||||||
public static final String SIG_USE = "sig";
|
public enum Use {
|
||||||
public static final String ENCRYPTION_USE = "enc";
|
SIG("sig"),
|
||||||
|
ENCRYPTION("enc");
|
||||||
|
|
||||||
|
private String str;
|
||||||
|
|
||||||
|
Use(String str) {
|
||||||
|
this.str = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String asString() {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonProperty(KEY_ID)
|
@JsonProperty(KEY_ID)
|
||||||
private String keyId;
|
private String keyId;
|
||||||
|
|
|
@ -67,7 +67,7 @@ public class JWKParser {
|
||||||
|
|
||||||
public PublicKey toPublicKey() {
|
public PublicKey toPublicKey() {
|
||||||
String algorithm = jwk.getKeyType();
|
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 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()));
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ package org.keycloak.representations.idm;
|
||||||
public class CertificateRepresentation {
|
public class CertificateRepresentation {
|
||||||
|
|
||||||
protected String privateKey;
|
protected String privateKey;
|
||||||
|
protected String publicKey;
|
||||||
protected String certificate;
|
protected String certificate;
|
||||||
|
|
||||||
public String getPrivateKey() {
|
public String getPrivateKey() {
|
||||||
|
@ -35,6 +36,14 @@ public class CertificateRepresentation {
|
||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKey(String publicKey) {
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCertificate() {
|
public String getCertificate() {
|
||||||
return certificate;
|
return certificate;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.representations.oidc;
|
package org.keycloak.representations.oidc;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ public class OIDCClientRepresentation {
|
||||||
|
|
||||||
private String jwks_uri;
|
private String jwks_uri;
|
||||||
|
|
||||||
private String jwks;
|
private JSONWebKeySet jwks;
|
||||||
|
|
||||||
private String sector_identifier_uri;
|
private String sector_identifier_uri;
|
||||||
|
|
||||||
|
@ -240,11 +241,11 @@ public class OIDCClientRepresentation {
|
||||||
this.jwks_uri = jwks_uri;
|
this.jwks_uri = jwks_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getJwks() {
|
public JSONWebKeySet getJwks() {
|
||||||
return jwks;
|
return jwks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setJwks(String jwks) {
|
public void setJwks(JSONWebKeySet jwks) {
|
||||||
this.jwks = jwks;
|
this.jwks = jwks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,14 @@ public class JsonParserTest {
|
||||||
Assert.assertEquals(3600, clientRep.getDefaultMaxAge().intValue());
|
Assert.assertEquals(3600, clientRep.getDefaultMaxAge().intValue());
|
||||||
Assert.assertEquals(1, clientRep.getRedirectUris().size());
|
Assert.assertEquals(1, clientRep.getRedirectUris().size());
|
||||||
Assert.assertEquals("https://op.certification.openid.net:60720/authz_cb", clientRep.getRedirectUris().get(0));
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.authentication;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
@ -60,4 +61,12 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
|
||||||
*/
|
*/
|
||||||
Map<String, Object> getAdapterConfiguration(ClientModel client);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,12 @@ package org.keycloak.authentication.authenticators.client;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
@ -33,6 +36,7 @@ import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
@ -179,4 +183,16 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,11 @@ import java.security.cert.X509Certificate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -39,10 +41,13 @@ import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client authentication based on JWT signed by client private key .
|
* Client authentication based on JWT signed by client private key .
|
||||||
|
@ -58,6 +63,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||||
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "client-jwt";
|
public static final String PROVIDER_ID = "client-jwt";
|
||||||
|
public static final String ATTR_PREFIX = "jwt.credential";
|
||||||
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
|
public static final String CERTIFICATE_ATTR = "jwt.credential.certificate";
|
||||||
|
|
||||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
|
@ -116,15 +122,12 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get client key and validate signature
|
// Get client key and validate signature
|
||||||
String encodedCertificate = client.getAttribute(CERTIFICATE_ATTR);
|
PublicKey clientPublicKey = getSignatureValidationKey(client, context);
|
||||||
if (encodedCertificate == null) {
|
if (clientPublicKey == null) {
|
||||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + clientId + "' doesn't have certificate configured");
|
// Error response already set to context
|
||||||
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
|
|
||||||
PublicKey clientPublicKey = clientCert.getPublicKey();
|
|
||||||
boolean signatureValid;
|
boolean signatureValid;
|
||||||
try {
|
try {
|
||||||
signatureValid = RSAProvider.verify(jws, clientPublicKey);
|
signatureValid = RSAProvider.verify(jws, clientPublicKey);
|
||||||
|
@ -159,6 +162,33 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
|
||||||
|
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX);
|
||||||
|
|
||||||
|
String encodedCertificate = certInfo.getCertificate();
|
||||||
|
String encodedPublicKey = certInfo.getPublicKey();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Override
|
||||||
public String getDisplayType() {
|
public String getDisplayType() {
|
||||||
return "Signed Jwt";
|
return "Signed Jwt";
|
||||||
|
@ -209,4 +239,15 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.broker.oidc;
|
package org.keycloak.broker.oidc;
|
||||||
|
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
|
||||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKParser;
|
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
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.representations.OIDCConfigurationRepresentation;
|
||||||
|
import org.keycloak.protocol.oidc.utils.JWKSUtils;
|
||||||
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -36,6 +36,8 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory<OIDCIdentityProvider> {
|
public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory<OIDCIdentityProvider> {
|
||||||
|
|
||||||
|
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "oidc";
|
public static final String PROVIDER_ID = "oidc";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -59,7 +61,7 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
|
protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
|
||||||
OIDCConfigurationRepresentation rep = null;
|
OIDCConfigurationRepresentation rep;
|
||||||
try {
|
try {
|
||||||
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
|
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -72,31 +74,24 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||||
config.setTokenUrl(rep.getTokenEndpoint());
|
config.setTokenUrl(rep.getTokenEndpoint());
|
||||||
config.setUserInfoUrl(rep.getUserinfoEndpoint());
|
config.setUserInfoUrl(rep.getUserinfoEndpoint());
|
||||||
if (rep.getJwksUri() != null) {
|
if (rep.getJwksUri() != null) {
|
||||||
String uri = rep.getJwksUri();
|
sendJwksRequest(rep, config);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return config.getConfig();
|
return config.getConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static boolean keyTypeSupported(String type) {
|
protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||||
return type != null && type.equals("RSA");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc;
|
package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
|
||||||
import org.keycloak.exportimport.ClientDescriptionConverter;
|
import org.keycloak.exportimport.ClientDescriptionConverter;
|
||||||
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.clientregistration.oidc.DescriptionConverter;
|
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>
|
* @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 OIDCClientDescriptionConverter(KeycloakSession session) {
|
||||||
public boolean isSupported(String description) {
|
this.session = session;
|
||||||
description = description.trim();
|
|
||||||
return (description.startsWith("{") && description.endsWith("}") && description.contains("\"redirect_uris\""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientRepresentation convertToInternal(String description) {
|
public ClientRepresentation convertToInternal(String description) {
|
||||||
try {
|
try {
|
||||||
OIDCClientRepresentation clientOIDC = JsonSerialization.readValue(description, OIDCClientRepresentation.class);
|
OIDCClientRepresentation clientOIDC = JsonSerialization.readValue(description, OIDCClientRepresentation.class);
|
||||||
return DescriptionConverter.toInternal(clientOIDC);
|
return DescriptionConverter.toInternal(session, clientOIDC);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getId() {
|
|
||||||
return ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
public static final String PROMPT_VALUE_CONSENT = "consent";
|
public static final String PROMPT_VALUE_CONSENT = "consent";
|
||||||
public static final String PROMPT_VALUE_SELECT_ACCOUNT = "select_account";
|
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;
|
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
|
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKBuilder;
|
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.LogoutEndpoint;
|
||||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||||
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
|
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.ServicesLogger;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,15 @@
|
||||||
package org.keycloak.protocol.oidc;
|
package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationService;
|
import org.keycloak.services.clientregistration.ClientRegistrationService;
|
||||||
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
|
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.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
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");
|
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());
|
public static final List<String> DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||||
|
|
||||||
// The exact list depends on protocolMappers
|
// The exact list depends on protocolMappers
|
||||||
|
@ -93,7 +95,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
||||||
config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_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.setTokenEndpointAuthSigningAlgValuesSupported(DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED);
|
||||||
|
|
||||||
config.setClaimsSupported(DEFAULT_CLAIMS_SUPPORTED);
|
config.setClaimsSupported(DEFAULT_CLAIMS_SUPPORTED);
|
||||||
|
@ -120,4 +122,16 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
return s;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,20 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.utils;
|
package org.keycloak.protocol.oidc.utils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
@ -70,6 +75,18 @@ public class AuthorizeClientUtil {
|
||||||
return processor;
|
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 {
|
public static class ClientAuthResult {
|
||||||
|
|
||||||
private final ClientModel client;
|
private final ClientModel client;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.saml;
|
package org.keycloak.protocol.saml;
|
||||||
|
|
||||||
import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -35,7 +35,7 @@ public interface SamlConfigAttributes {
|
||||||
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
|
||||||
String SAML_ENCRYPT = "saml.encrypt";
|
String SAML_ENCRYPT = "saml.encrypt";
|
||||||
String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
|
String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
|
||||||
String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + CertificateInfoHelper.X509CERTIFICATE;
|
||||||
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
|
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.X509CERTIFICATE;
|
||||||
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.PRIVATE_KEY;
|
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY;
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,4 +425,9 @@ public interface ServicesLogger extends BasicLogger {
|
||||||
@LogMessage(level = ERROR)
|
@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.")
|
@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);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,38 @@
|
||||||
package org.keycloak.services.clientregistration.oidc;
|
package org.keycloak.services.clientregistration.oidc;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
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.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.protocol.oidc.utils.OIDCResponseType;
|
||||||
|
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationException;
|
import org.keycloak.services.clientregistration.ClientRegistrationException;
|
||||||
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class DescriptionConverter {
|
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();
|
ClientRepresentation client = new ClientRepresentation();
|
||||||
client.setClientId(clientOIDC.getClientId());
|
client.setClientId(clientOIDC.getClientId());
|
||||||
client.setName(clientOIDC.getClientName());
|
client.setName(clientOIDC.getClientName());
|
||||||
|
@ -60,17 +74,79 @@ public class DescriptionConverter {
|
||||||
throw new ClientRegistrationException(iae.getMessage(), iae);
|
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);
|
||||||
|
|
||||||
|
CertificateRepresentation rep = new CertificateRepresentation();
|
||||||
|
rep.setPublicKey(publicKeyPem);
|
||||||
|
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
return client;
|
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();
|
OIDCClientRepresentation response = new OIDCClientRepresentation();
|
||||||
response.setClientId(client.getClientId());
|
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.setClientName(client.getName());
|
||||||
response.setClientUri(client.getBaseUrl());
|
response.setClientUri(client.getBaseUrl());
|
||||||
response.setClientSecret(client.getSecret());
|
|
||||||
response.setRedirectUris(client.getRedirectUris());
|
response.setRedirectUris(client.getRedirectUris());
|
||||||
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
|
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
|
||||||
response.setRegistrationClientUri(uri.toString());
|
response.setRegistrationClientUri(uri.toString());
|
||||||
|
|
|
@ -53,10 +53,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
|
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
||||||
client = create(client);
|
client = create(client);
|
||||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
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());
|
clientOIDC.setClientIdIssuedAt(Time.currentTime());
|
||||||
return Response.created(uri).entity(clientOIDC).build();
|
return Response.created(uri).entity(clientOIDC).build();
|
||||||
} catch (ClientRegistrationException cre) {
|
} catch (ClientRegistrationException cre) {
|
||||||
|
@ -70,7 +70,7 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response getOIDC(@PathParam("clientId") String clientId) {
|
public Response getOIDC(@PathParam("clientId") String clientId) {
|
||||||
ClientRepresentation client = get(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();
|
return Response.ok(clientOIDC).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +79,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
|
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
|
||||||
try {
|
try {
|
||||||
ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
|
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
||||||
client = update(clientId, client);
|
client = update(clientId, client);
|
||||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
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();
|
return Response.ok(clientOIDC).build();
|
||||||
} catch (ClientRegistrationException cre) {
|
} catch (ClientRegistrationException cre) {
|
||||||
logger.clientRegistrationException(cre.getMessage());
|
logger.clientRegistrationException(cre.getMessage());
|
||||||
|
|
|
@ -25,14 +25,19 @@ import org.jboss.resteasy.spi.NotFoundException;
|
||||||
import org.keycloak.common.util.StreamUtil;
|
import org.keycloak.common.util.StreamUtil;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.oidc.utils.JWKSUtils;
|
||||||
import org.keycloak.representations.KeyStoreConfig;
|
import org.keycloak.representations.KeyStoreConfig;
|
||||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
@ -49,6 +54,7 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -60,17 +66,12 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public class ClientAttributeCertificateResource {
|
public class ClientAttributeCertificateResource {
|
||||||
|
|
||||||
public static final String PRIVATE_KEY = "private.key";
|
|
||||||
public static final String X509CERTIFICATE = "certificate";
|
|
||||||
|
|
||||||
protected RealmModel realm;
|
protected RealmModel realm;
|
||||||
private RealmAuth auth;
|
private RealmAuth auth;
|
||||||
protected ClientModel client;
|
protected ClientModel client;
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
protected AdminEventBuilder adminEvent;
|
protected AdminEventBuilder adminEvent;
|
||||||
protected String attributePrefix;
|
protected String attributePrefix;
|
||||||
protected String privateAttribute;
|
|
||||||
protected String certificateAttribute;
|
|
||||||
|
|
||||||
public ClientAttributeCertificateResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, String attributePrefix, AdminEventBuilder adminEvent) {
|
public ClientAttributeCertificateResource(RealmModel realm, RealmAuth auth, ClientModel client, KeycloakSession session, String attributePrefix, AdminEventBuilder adminEvent) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -78,8 +79,6 @@ public class ClientAttributeCertificateResource {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.attributePrefix = attributePrefix;
|
this.attributePrefix = attributePrefix;
|
||||||
this.privateAttribute = attributePrefix + "." + PRIVATE_KEY;
|
|
||||||
this.certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
|
||||||
this.adminEvent = adminEvent.resource(ResourceType.CLIENT);
|
this.adminEvent = adminEvent.resource(ResourceType.CLIENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +97,7 @@ public class ClientAttributeCertificateResource {
|
||||||
throw new NotFoundException("Could not find client");
|
throw new NotFoundException("Could not find client");
|
||||||
}
|
}
|
||||||
|
|
||||||
CertificateRepresentation info = new CertificateRepresentation();
|
CertificateRepresentation info = CertificateInfoHelper.getCertificateFromClient(client, attributePrefix);
|
||||||
info.setCertificate(client.getAttribute(certificateAttribute));
|
|
||||||
info.setPrivateKey(client.getAttribute(privateAttribute));
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,8 +119,7 @@ public class ClientAttributeCertificateResource {
|
||||||
|
|
||||||
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
|
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
|
||||||
|
|
||||||
client.setAttribute(privateAttribute, info.getPrivateKey());
|
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
|
||||||
client.setAttribute(certificateAttribute, info.getCertificate());
|
|
||||||
|
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
||||||
|
|
||||||
|
@ -151,18 +147,12 @@ public class ClientAttributeCertificateResource {
|
||||||
|
|
||||||
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
|
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
|
||||||
|
|
||||||
if (info.getPrivateKey() != null) {
|
try {
|
||||||
client.setAttribute(privateAttribute, info.getPrivateKey());
|
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
|
||||||
} else if (info.getCertificate() != null) {
|
} catch (IllegalStateException ise) {
|
||||||
client.removeAttribute(privateAttribute);
|
|
||||||
} else {
|
|
||||||
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.getCertificate() != null) {
|
|
||||||
client.setAttribute(certificateAttribute, info.getCertificate());
|
|
||||||
}
|
|
||||||
|
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
@ -187,12 +177,12 @@ public class ClientAttributeCertificateResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
|
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
|
||||||
|
info.setPrivateKey(null);
|
||||||
|
|
||||||
if (info.getCertificate() != null) {
|
try {
|
||||||
client.setAttribute(certificateAttribute, info.getCertificate());
|
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
|
||||||
client.removeAttribute(privateAttribute);
|
} catch (IllegalStateException ise) {
|
||||||
} else {
|
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
||||||
throw new ErrorResponseException("certificate-not-found", "Certificate with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
||||||
|
@ -210,10 +200,16 @@ public class ClientAttributeCertificateResource {
|
||||||
info.setCertificate(pem);
|
info.setCertificate(pem);
|
||||||
return info;
|
return info;
|
||||||
|
|
||||||
|
} else if (keystoreFormat.equals("JSON Web Key Set (JWK)")) {
|
||||||
|
InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
|
||||||
|
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
|
||||||
|
PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||||
|
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
|
||||||
|
info.setPublicKey(publicKeyPem);
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
String keyAlias = uploadForm.get("keyAlias").get(0).getBodyAsString();
|
String keyAlias = uploadForm.get("keyAlias").get(0).getBodyAsString();
|
||||||
List<InputPart> keyPasswordPart = uploadForm.get("keyPassword");
|
List<InputPart> keyPasswordPart = uploadForm.get("keyPassword");
|
||||||
char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.get(0).getBodyAsString().toCharArray() : null;
|
char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.get(0).getBodyAsString().toCharArray() : null;
|
||||||
|
@ -272,8 +268,10 @@ public class ClientAttributeCertificateResource {
|
||||||
throw new NotAcceptableException("Only support jks or pkcs12 format.");
|
throw new NotAcceptableException("Only support jks or pkcs12 format.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String privatePem = client.getAttribute(privateAttribute);
|
CertificateRepresentation info = CertificateInfoHelper.getCertificateFromClient(client, attributePrefix);
|
||||||
String certPem = client.getAttribute(certificateAttribute);
|
String privatePem = info.getPrivateKey();
|
||||||
|
String certPem = info.getCertificate();
|
||||||
|
|
||||||
if (privatePem == null && certPem == null) {
|
if (privatePem == null && certPem == null) {
|
||||||
throw new NotFoundException("keypair not generated for client");
|
throw new NotFoundException("keypair not generated for client");
|
||||||
}
|
}
|
||||||
|
@ -322,7 +320,10 @@ public class ClientAttributeCertificateResource {
|
||||||
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
|
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
|
||||||
byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate());
|
byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate());
|
||||||
|
|
||||||
client.setAttribute(certificateAttribute, info.getCertificate());
|
info.setPrivateKey(null);
|
||||||
|
|
||||||
|
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
|
||||||
|
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
|
||||||
return rtn;
|
return rtn;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* 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.services.util;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CertificateInfoHelper {
|
||||||
|
|
||||||
|
|
||||||
|
public static final String PRIVATE_KEY = "private.key";
|
||||||
|
public static final String X509CERTIFICATE = "certificate";
|
||||||
|
public static final String PUBLIC_KEY = "public.key";
|
||||||
|
|
||||||
|
|
||||||
|
public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) {
|
||||||
|
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||||
|
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||||
|
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
|
||||||
|
|
||||||
|
CertificateRepresentation rep = new CertificateRepresentation();
|
||||||
|
rep.setCertificate(client.getAttribute(certificateAttribute));
|
||||||
|
rep.setPublicKey(client.getAttribute(publicKeyAttribute));
|
||||||
|
rep.setPrivateKey(client.getAttribute(privateKeyAttribute));
|
||||||
|
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void updateClientModelCertificateInfo(ClientModel client, CertificateRepresentation rep, String attributePrefix) {
|
||||||
|
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||||
|
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||||
|
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
|
||||||
|
|
||||||
|
if (rep.getPublicKey() == null && rep.getCertificate() == null) {
|
||||||
|
throw new IllegalStateException("Both certificate and publicKey are null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rep.getPublicKey() != null && rep.getCertificate() != null) {
|
||||||
|
throw new IllegalStateException("Both certificate and publicKey are not null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
|
||||||
|
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
|
||||||
|
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) {
|
||||||
|
if (attrValue != null) {
|
||||||
|
client.setAttribute(attrName, attrValue);
|
||||||
|
} else {
|
||||||
|
client.removeAttribute(attrName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
|
||||||
|
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||||
|
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||||
|
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
|
||||||
|
|
||||||
|
if (rep.getPublicKey() == null && rep.getCertificate() == null) {
|
||||||
|
throw new IllegalStateException("Both certificate and publicKey are null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rep.getPublicKey() != null && rep.getCertificate() != null) {
|
||||||
|
throw new IllegalStateException("Both certificate and publicKey are not null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
|
||||||
|
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
|
||||||
|
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) {
|
||||||
|
if (attrValue != null) {
|
||||||
|
if (client.getAttributes() == null) {
|
||||||
|
client.setAttributes(new HashMap<>());
|
||||||
|
}
|
||||||
|
client.getAttributes().put(attrName, attrValue);
|
||||||
|
} else {
|
||||||
|
if (client.getAttributes() != null) {
|
||||||
|
client.getAttributes().remove(attrName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,5 +16,5 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
org.keycloak.exportimport.KeycloakClientDescriptionConverter
|
org.keycloak.exportimport.KeycloakClientDescriptionConverter
|
||||||
org.keycloak.protocol.oidc.OIDCClientDescriptionConverter
|
org.keycloak.protocol.oidc.OIDCClientDescriptionConverterFactory
|
||||||
org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter
|
org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter
|
|
@ -18,10 +18,12 @@
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||||
|
@ -119,4 +121,9 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
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.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
|
@ -17,32 +17,61 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.client;
|
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.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
|
||||||
import org.keycloak.client.registration.Auth;
|
import org.keycloak.client.registration.Auth;
|
||||||
import org.keycloak.client.registration.ClientRegistrationException;
|
import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
import org.keycloak.client.registration.HttpErrorException;
|
import org.keycloak.client.registration.HttpErrorException;
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
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.protocol.oidc.utils.OIDCResponseType;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
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
|
@Before
|
||||||
public void before() throws Exception {
|
public void before() throws Exception {
|
||||||
super.before();
|
super.before();
|
||||||
|
@ -122,6 +151,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
assertEquals("http://redirect", response.getRedirectUris().get(0));
|
assertEquals("http://redirect", response.getRedirectUris().get(0));
|
||||||
assertEquals(Arrays.asList("code", "none"), response.getResponseTypes());
|
assertEquals(Arrays.asList("code", "none"), response.getResponseTypes());
|
||||||
assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
|
assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
|
||||||
|
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -136,6 +166,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
|
assertTrue(CollectionUtil.collectionEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
|
||||||
assertNotNull(response.getClientSecret());
|
assertNotNull(response.getClientSecret());
|
||||||
assertEquals(0, response.getClientSecretExpiresAt().intValue());
|
assertEquals(0, response.getClientSecretExpiresAt().intValue());
|
||||||
|
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -174,4 +205,89 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
reg.oidc().delete(response);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]}
|
|
@ -275,6 +275,7 @@ service-account-roles.tooltip=Allows you to authenticate role mappings for the s
|
||||||
client-authenticator=Client Authenticator
|
client-authenticator=Client Authenticator
|
||||||
client-authenticator.tooltip=Client Authenticator used for authentication this client against Keycloak server
|
client-authenticator.tooltip=Client Authenticator used for authentication this client against Keycloak server
|
||||||
certificate.tooltip=Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.
|
certificate.tooltip=Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.
|
||||||
|
publicKey.tooltip=Public Key for validate JWT issued by client and signed by Client private key.
|
||||||
no-client-certificate-configured=No client certificate configured
|
no-client-certificate-configured=No client certificate configured
|
||||||
gen-new-keys-and-cert=Generate new keys and certificate
|
gen-new-keys-and-cert=Generate new keys and certificate
|
||||||
import-certificate=Import Certificate
|
import-certificate=Import Certificate
|
||||||
|
|
|
@ -364,8 +364,12 @@ module.controller('ClientCertificateImportCtrl', function($scope, $location, $ht
|
||||||
"Certificate PEM"
|
"Certificate PEM"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (callingContext == 'jwt-credentials') {
|
||||||
|
$scope.keyFormats.push('JSON Web Key Set (JWK)');
|
||||||
|
}
|
||||||
|
|
||||||
$scope.hideKeystoreSettings = function() {
|
$scope.hideKeystoreSettings = function() {
|
||||||
return $scope.uploadKeyFormat == 'Certificate PEM';
|
return $scope.uploadKeyFormat == 'Certificate PEM' || $scope.uploadKeyFormat == 'JSON Web Key Set (JWK)';
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.uploadKeyFormat = $scope.keyFormats[0];
|
$scope.uploadKeyFormat = $scope.keyFormats[0];
|
||||||
|
|
|
@ -26,14 +26,14 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'archive-format.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'archive-format.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" data-ng-hide="hideKeystoreSettings()">
|
||||||
<label class="col-md-2 control-label" for="uploadKeyAlias">{{:: 'key-alias' | translate}}</label>
|
<label class="col-md-2 control-label" for="uploadKeyAlias">{{:: 'key-alias' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input class="form-control" type="text" id="uploadKeyAlias" name="uploadKeyAlias" data-ng-model="uploadKeyAlias" autofocus required>
|
<input class="form-control" type="text" id="uploadKeyAlias" name="uploadKeyAlias" data-ng-model="uploadKeyAlias" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'jwt-import.key-alias.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'jwt-import.key-alias.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" data-ng-hide="hideKeystoreSettings()">
|
||||||
<label class="col-md-2 control-label" for="uploadStorePassword">{{:: 'store-password' | translate}}</label>
|
<label class="col-md-2 control-label" for="uploadStorePassword">{{:: 'store-password' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<input class="form-control" type="password" id="uploadStorePassword" name="uploadStorePassword" data-ng-model="uploadStorePassword" autofocus required>
|
<input class="form-control" type="password" id="uploadStorePassword" name="uploadStorePassword" data-ng-model="uploadStorePassword" autofocus required>
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
<div>
|
<div>
|
||||||
<form class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
|
<form class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
|
|
||||||
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
|
|
||||||
|
|
||||||
<div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
|
<div data-ng-show="signingKeyInfo.certificate">
|
||||||
<textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
|
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
|
||||||
|
|
||||||
|
<div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
|
||||||
|
<textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate">
|
|
||||||
|
<div data-ng-show="signingKeyInfo.publicKey">
|
||||||
|
<label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'publicKey.tooltip' | translate}}</kc-tooltip>
|
||||||
|
|
||||||
|
<div class="col-sm-10" data-ng-show="signingKeyInfo.publicKey">
|
||||||
|
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.publicKey}}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
|
||||||
{{:: 'no-client-certificate-configured' | translate}}
|
{{:: 'no-client-certificate-configured' | translate}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue