Merge pull request #3274 from mposolda/pk-rotation

KEYCLOAK-3493 KEYCLOAK-3532 Added KeyStorageProvider. Support key rot…
This commit is contained in:
Marek Posolda 2016-09-30 22:54:26 +02:00 committed by GitHub
commit b0da7dc72f
88 changed files with 1960 additions and 429 deletions

View file

@ -20,9 +20,14 @@ package org.keycloak.adapters;
import java.security.PublicKey; import java.security.PublicKey;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.UserSessionManagement; import org.keycloak.adapters.spi.UserSessionManagement;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.VersionRepresentation; import org.keycloak.representations.VersionRepresentation;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
@ -85,6 +90,10 @@ public class PreAuthActionsHandler {
if (!resolveDeployment()) return true; if (!resolveDeployment()) return true;
handleTestAvailable(); handleTestAvailable();
return true; return true;
} else if (requestUri.endsWith(AdapterConstants.K_JWKS)) {
if (!resolveDeployment()) return true;
handleJwksRequest();
return true;
} }
return false; return false;
} }
@ -244,4 +253,26 @@ public class PreAuthActionsHandler {
} }
} }
protected void handleJwksRequest() {
try {
JSONWebKeySet jwks = new JSONWebKeySet();
ClientCredentialsProvider clientCredentialsProvider = deployment.getClientAuthenticator();
// For now, just get signature key from JWT provider. We can add more if we support encryption etc.
if (clientCredentialsProvider instanceof JWTClientCredentialsProvider) {
PublicKey publicKey = ((JWTClientCredentialsProvider) clientCredentialsProvider).getPublicKey();
JWK jwk = JWKBuilder.create().rs256(publicKey);
jwks.setKeys(new JWK[] { jwk });
} else {
jwks.setKeys(new JWK[] {});
}
facade.getResponse().setStatus(200);
facade.getResponse().setHeader("Content-Type", "application/json");
JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), jwks);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} }

View file

@ -17,12 +17,15 @@
package org.keycloak.adapters.authentication; package org.keycloak.adapters.authentication;
import java.security.PrivateKey; import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Map; import java.util.Map;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.KeystoreUtil;
@ -38,7 +41,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
public static final String PROVIDER_ID = "jwt"; public static final String PROVIDER_ID = "jwt";
private PrivateKey privateKey; private KeyPair keyPair;
private JWK publicKeyJwk;
private int tokenTimeout; private int tokenTimeout;
@Override @Override
@ -46,8 +51,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
return PROVIDER_ID; return PROVIDER_ID;
} }
public void setPrivateKey(PrivateKey privateKey) { public void setupKeyPair(KeyPair keyPair) {
this.privateKey = privateKey; this.keyPair = keyPair;
this.publicKeyJwk = JWKBuilder.create().rs256(keyPair.getPublic());
} }
public void setTokenTimeout(int tokenTimeout) { public void setTokenTimeout(int tokenTimeout) {
@ -58,6 +64,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
return tokenTimeout; return tokenTimeout;
} }
public PublicKey getPublicKey() {
return keyPair.getPublic();
}
@Override @Override
public void init(KeycloakDeployment deployment, Object config) { public void init(KeycloakDeployment deployment, Object config) {
if (config == null || !(config instanceof Map)) { if (config == null || !(config instanceof Map)) {
@ -88,7 +98,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
if (clientKeyAlias == null) { if (clientKeyAlias == null) {
clientKeyAlias = deployment.getResourceName(); clientKeyAlias = deployment.getResourceName();
} }
this.privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
KeyPair keyPair = KeystoreUtil.loadKeyPairFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
setupKeyPair(keyPair);
this.tokenTimeout = asInt(cfg, "token-timeout", 10); this.tokenTimeout = asInt(cfg, "token-timeout", 10);
} }
@ -120,8 +132,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
public String createSignedRequestToken(String clientId, String realmInfoUrl) { public String createSignedRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl); JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
return new JWSBuilder() return new JWSBuilder()
.kid(publicKeyJwk.getKeyId())
.jsonContent(jwt) .jsonContent(jwt)
.rsa256(privateKey); .rsa256(keyPair.getPrivate());
} }
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {

View file

@ -71,8 +71,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
sendRequest(deployment); sendRequest(deployment);
lastRequestTime = currentTime; lastRequestTime = currentTime;
} else { } else {
// TODO: debug log.debugf("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
log.infof("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
} }
} }
} }
@ -83,9 +82,9 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
private void sendRequest(KeycloakDeployment deployment) { private void sendRequest(KeycloakDeployment deployment) {
// Send the request if (log.isTraceEnabled()) {
// TODO: trace or remove? log.tracef("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
log.infof("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName()); }
HttpGet getMethod = new HttpGet(deployment.getJwksUrl()); HttpGet getMethod = new HttpGet(deployment.getJwksUrl());
try { try {
@ -93,8 +92,9 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG); Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
// TODO: Debug with condition if (log.isDebugEnabled()) {
log.infof("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString()); log.debugf("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
}
// Update current keys // Update current keys
currentKeys.clear(); currentKeys.clear();

View file

@ -20,8 +20,10 @@ package org.keycloak.common.util;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey;
import org.keycloak.common.constants.GenericConstants; import org.keycloak.common.constants.GenericConstants;
@ -49,7 +51,7 @@ public class KeystoreUtil {
return trustStore; return trustStore;
} }
public static PrivateKey loadPrivateKeyFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) { public static KeyPair loadKeyPairFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) {
InputStream stream = FindFile.findFile(keystoreFile); InputStream stream = FindFile.findFile(keystoreFile);
try { try {
@ -61,11 +63,12 @@ public class KeystoreUtil {
} }
keyStore.load(stream, storePassword.toCharArray()); keyStore.load(stream, storePassword.toCharArray());
PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray()); PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
if (key == null) { if (privateKey == null) {
throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore"); throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore");
} }
return key; PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
return new KeyPair(publicKey, privateKey);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to load private key: " + e.getMessage(), e); throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
} }

View file

@ -81,6 +81,8 @@ public interface OAuth2Constants {
String MAX_AGE = "max_age"; String MAX_AGE = "max_age";
String JWT = "JWT";
} }

View file

@ -29,6 +29,7 @@ public interface AdapterConstants {
public static final String K_PUSH_NOT_BEFORE = "k_push_not_before"; public static final String K_PUSH_NOT_BEFORE = "k_push_not_before";
public static final String K_TEST_AVAILABLE = "k_test_available"; public static final String K_TEST_AVAILABLE = "k_test_available";
public static final String K_QUERY_BEARER_TOKEN = "k_query_bearer_token"; public static final String K_QUERY_BEARER_TOKEN = "k_query_bearer_token";
public static final String K_JWKS = "k_jwks";
// This param name is defined again in Keycloak Subsystem class // This param name is defined again in Keycloak Subsystem class
// org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor. We have this value in // org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor. We have this value in

View file

@ -34,6 +34,7 @@ public class JWKBuilder {
public static final String DEFAULT_PUBLIC_KEY_USE = "sig"; public static final String DEFAULT_PUBLIC_KEY_USE = "sig";
public static final String DEFAULT_MESSAGE_DIGEST = "SHA-256"; public static final String DEFAULT_MESSAGE_DIGEST = "SHA-256";
private String kid;
private JWKBuilder() { private JWKBuilder() {
} }
@ -42,11 +43,18 @@ public class JWKBuilder {
return new JWKBuilder(); return new JWKBuilder();
} }
public JWKBuilder kid(String kid) {
this.kid = kid;
return this;
}
public JWK rs256(PublicKey key) { public JWK rs256(PublicKey key) {
RSAPublicKey rsaKey = (RSAPublicKey) key; RSAPublicKey rsaKey = (RSAPublicKey) key;
RSAPublicJWK k = new RSAPublicJWK(); RSAPublicJWK k = new RSAPublicJWK();
k.setKeyId(createKeyId(key));
String kid = this.kid != null ? this.kid : createKeyId(key);
k.setKeyId(kid);
k.setKeyType(RSAPublicJWK.RSA); k.setKeyType(RSAPublicJWK.RSA);
k.setAlgorithm(RSAPublicJWK.RS256); k.setAlgorithm(RSAPublicJWK.RS256);
k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE); k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE);
@ -56,7 +64,7 @@ public class JWKBuilder {
return k; return k;
} }
private String createKeyId(Key key) { public static String createKeyId(Key key) {
try { try {
return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded())); return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded()));
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {

View file

@ -27,6 +27,7 @@ public class CertificateRepresentation {
protected String privateKey; protected String privateKey;
protected String publicKey; protected String publicKey;
protected String certificate; protected String certificate;
protected String kid;
public String getPrivateKey() { public String getPrivateKey() {
return privateKey; return privateKey;
@ -52,5 +53,11 @@ public class CertificateRepresentation {
this.certificate = certificate; this.certificate = certificate;
} }
public String getKid() {
return kid;
}
public void setKid(String kid) {
this.kid = kid;
}
} }

View file

@ -42,4 +42,15 @@ public class JWKSUtils {
return result; return result;
} }
public static JWK getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
for (JWK jwk : keySet.getKeys()) {
JWKParser parser = JWKParser.create(jwk);
if (parser.getJwk().getPublicKeyUse().equals(requestedUse.asString()) && parser.isKeyTypeSupported(jwk.getKeyType())) {
return jwk;
}
}
return null;
}
} }

View file

@ -89,6 +89,10 @@
<local-cache name="loginFailures"/> <local-cache name="loginFailures"/>
<local-cache name="authorization"/> <local-cache name="authorization"/>
<local-cache name="work"/> <local-cache name="work"/>
<local-cache name="keys">
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
</cache-container> </cache-container>
<xsl:apply-templates select="node()|@*"/> <xsl:apply-templates select="node()|@*"/>
</xsl:copy> </xsl:copy>

View file

@ -10,5 +10,8 @@ embed-server --server-config=standalone-ha.xml
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC") /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC")
/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
run-batch --file=default-keycloak-subsys-config.cli run-batch --file=default-keycloak-subsys-config.cli

View file

@ -10,5 +10,8 @@ embed-server --server-config=standalone.xml
/subsystem=infinispan/cache-container=keycloak/local-cache=work:add() /subsystem=infinispan/cache-container=keycloak/local-cache=work:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add() /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
run-batch --file=default-keycloak-subsys-config.cli run-batch --file=default-keycloak-subsys-config.cli

View file

@ -17,6 +17,8 @@
package org.keycloak.connections.infinispan; package org.keycloak.connections.infinispan;
import java.util.concurrent.TimeUnit;
import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration; import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.ConfigurationBuilder;
@ -98,7 +100,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup); cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
containerManaged = true; containerManaged = true;
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(true, InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX)); cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries(); long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
@ -106,9 +108,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX; maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
} }
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(true, maxEntries)); cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup); logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to retrieve cache container", e); throw new RuntimeException("Failed to retrieve cache container", e);
@ -116,6 +120,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} }
protected void initEmbedded() { protected void initEmbedded() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false); boolean clustered = config.getBoolean("clustered", false);
@ -176,7 +183,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup()); counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC); counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(false, InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX)); cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries(); long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
@ -184,19 +191,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX; maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
} }
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(false, maxEntries)); cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
} }
private Configuration getRevisionCacheConfig(boolean managed, long maxEntries) { private Configuration getRevisionCacheConfig(long maxEntries) {
ConfigurationBuilder cb = new ConfigurationBuilder(); ConfigurationBuilder cb = new ConfigurationBuilder();
cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL); cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL);
// Workaround: Use Dummy manager even in managed ( wildfly/eap ) environment. Without this workaround, there is an issue in EAP7 overlay. // Use Dummy manager even in managed ( wildfly/eap ) environment. We don't want infinispan to participate in global transaction
// After start+end revisions batch is left the JTA transaction in committed state. This is incorrect and causes other issues afterwards. cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
// TODO: Investigate
// if (!managed)
cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
cb.transaction().lockingMode(LockingMode.PESSIMISTIC); cb.transaction().lockingMode(LockingMode.PESSIMISTIC);
@ -204,4 +211,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build(); return cb.build();
} }
protected Configuration getKeysCacheConfig() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
cb.expiration().maxIdle(InfinispanConnectionProvider.KEYS_CACHE_MAX_IDLE_SECONDS, TimeUnit.SECONDS);
return cb.build();
}
} }

View file

@ -39,6 +39,10 @@ public interface InfinispanConnectionProvider extends Provider {
String WORK_CACHE_NAME = "work"; String WORK_CACHE_NAME = "work";
String AUTHORIZATION_CACHE_NAME = "authorization"; String AUTHORIZATION_CACHE_NAME = "authorization";
String KEYS_CACHE_NAME = "keys";
int KEYS_CACHE_DEFAULT_MAX = 500;
int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
<K, V> Cache<K, V> getCache(String name); <K, V> Cache<K, V> getCache(String name);

View file

@ -0,0 +1,160 @@
/*
* 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.keys.infinispan;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.keys.KeyLoader;
import org.keycloak.keys.KeyStorageProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanKeyStorageProvider implements KeyStorageProvider {
private static final Logger log = Logger.getLogger(InfinispanKeyStorageProvider.class);
private final Cache<String, PublicKeysEntry> keys;
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress;
private final int minTimeBetweenRequests ;
public InfinispanKeyStorageProvider(Cache<String, PublicKeysEntry> keys, Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
this.keys = keys;
this.tasksInProgress = tasksInProgress;
this.minTimeBetweenRequests = minTimeBetweenRequests;
}
@Override
public PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader) {
// Check if key is in cache
PublicKeysEntry entry = keys.get(modelKey);
if (entry != null) {
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
}
int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
int currentTime = Time.currentTime();
// Check if we are allowed to send request
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
WrapperCallable wrapperCallable = new WrapperCallable(modelKey, loader);
FutureTask<PublicKeysEntry> task = new FutureTask<>(wrapperCallable);
FutureTask<PublicKeysEntry> existing = tasksInProgress.putIfAbsent(modelKey, task);
if (existing == null) {
task.run();
} else {
task = existing;
}
try {
entry = task.get();
// Computation finished. Let's see if key is available
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
} catch (ExecutionException ee) {
throw new RuntimeException("Error when loading public keys", ee);
} catch (InterruptedException ie) {
throw new RuntimeException("Error. Interrupted when loading public keys", ie);
} finally {
// Our thread inserted the task. Let's clean
if (existing == null) {
tasksInProgress.remove(modelKey);
}
}
} else {
log.warnf("Won't load the keys for model '%s' . Last request time was %d", modelKey, lastRequestTime);
}
Set<String> availableKids = entry==null ? Collections.emptySet() : entry.getCurrentKeys().keySet();
log.warnf("PublicKey wasn't found in the storage. Requested kid: '%s' . Available kids: '%s'", kid, availableKids);
return null;
}
private PublicKey getPublicKey(Map<String, PublicKey> publicKeys, String kid) {
// Backwards compatibility
if (kid == null && !publicKeys.isEmpty()) {
return publicKeys.values().iterator().next();
} else {
return publicKeys.get(kid);
}
}
@Override
public void close() {
}
private class WrapperCallable implements Callable<PublicKeysEntry> {
private final String modelKey;
private final KeyLoader delegate;
public WrapperCallable(String modelKey, KeyLoader delegate) {
this.modelKey = modelKey;
this.delegate = delegate;
}
@Override
public PublicKeysEntry call() throws Exception {
PublicKeysEntry entry = keys.get(modelKey);
int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
int currentTime = Time.currentTime();
// Check again if we are allowed to send request. There is a chance other task was already finished and removed from tasksInProgress in the meantime.
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
Map<String, PublicKey> publicKeys = delegate.loadKeys();
if (log.isDebugEnabled()) {
log.debugf("Public keys retrieved successfully for model %s. New kids: %s", modelKey, publicKeys.keySet().toString());
}
entry = new PublicKeysEntry(currentTime, publicKeys);
keys.put(modelKey, entry);
}
return entry;
}
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.keys.infinispan;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.keys.KeyStorageProvider;
import org.keycloak.keys.KeyStorageProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanKeyStorageProviderFactory implements KeyStorageProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanKeyStorageProviderFactory.class);
public static final String PROVIDER_ID = "infinispan";
private Cache<String, PublicKeysEntry> keysCache;
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
private int minTimeBetweenRequests;
@Override
public KeyStorageProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanKeyStorageProvider(keysCache, tasksInProgress, minTimeBetweenRequests);
}
private void lazyInit(KeycloakSession session) {
if (keysCache == null) {
synchronized (this) {
if (keysCache == null) {
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
}
}
}
}
@Override
public void init(Config.Scope config) {
minTimeBetweenRequests = config.getInt("minTimeBetweenRequests", 10);
log.debugf("minTimeBetweenRequests is %d", minTimeBetweenRequests);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,45 @@
/*
* 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.keys.infinispan;
import java.io.Serializable;
import java.security.PublicKey;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PublicKeysEntry implements Serializable {
private final int lastRequestTime;
private final Map<String, PublicKey> currentKeys;
public PublicKeysEntry(int lastRequestTime, Map<String, PublicKey> currentKeys) {
this.lastRequestTime = lastRequestTime;
this.currentKeys = currentKeys;
}
public int getLastRequestTime() {
return lastRequestTime;
}
public Map<String, PublicKey> getCurrentKeys() {
return currentKeys;
}
}

View file

@ -0,0 +1,18 @@
#
# 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.
#
org.keycloak.keys.infinispan.InfinispanKeyStorageProviderFactory

View file

@ -0,0 +1,172 @@
/*
* 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.models.keys.infinispan;
import java.security.PublicKey;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.keys.KeyLoader;
import org.keycloak.keys.infinispan.InfinispanKeyStorageProvider;
import org.keycloak.keys.infinispan.PublicKeysEntry;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanKeyStorageProviderTest {
private Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
Cache<String, PublicKeysEntry> keys = getKeysCache();
Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
int minTimeBetweenRequests = 10;
@Before
public void before() {
Time.setOffset(0);
}
@After
public void after() {
Time.setOffset(0);
}
@Test
public void testConcurrency() throws Exception {
// Just one thread will execute the task
List<Thread> threads = new LinkedList<>();
for (int i=0 ; i<10 ; i++) {
Thread t = new Thread(new SampleWorker("model1"));
threads.add(t);
}
startAndJoinAll(threads);
Assert.assertEquals(counters.get("model1").get(), 1);
threads.clear();
// model1 won't be executed due to lastRequestTime. model2 will be executed just with one thread
for (int i=0 ; i<10 ; i++) {
Thread t = new Thread(new SampleWorker("model1"));
threads.add(t);
}
for (int i=0 ; i<10 ; i++) {
Thread t = new Thread(new SampleWorker("model2"));
threads.add(t);
}
startAndJoinAll(threads);
Assert.assertEquals(counters.get("model1").get(), 1);
Assert.assertEquals(counters.get("model2").get(), 1);
threads.clear();
// Increase time offset
Time.setOffset(20);
// Time updated. So another thread should successfully run loader for both model1 and model2
for (int i=0 ; i<10 ; i++) {
Thread t = new Thread(new SampleWorker("model1"));
threads.add(t);
}
for (int i=0 ; i<10 ; i++) {
Thread t = new Thread(new SampleWorker("model2"));
threads.add(t);
}
startAndJoinAll(threads);
Assert.assertEquals(counters.get("model1").get(), 2);
Assert.assertEquals(counters.get("model2").get(), 2);
threads.clear();
}
private void startAndJoinAll(List<Thread> threads) throws Exception {
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
private class SampleWorker implements Runnable {
private final String modelKey;
private SampleWorker(String modelKey) {
this.modelKey = modelKey;
}
@Override
public void run() {
InfinispanKeyStorageProvider provider = new InfinispanKeyStorageProvider(keys, tasksInProgress, minTimeBetweenRequests);
provider.getPublicKey(modelKey, "kid1", new SampleLoader(modelKey));
}
}
private class SampleLoader implements KeyLoader {
private final String modelKey;
private SampleLoader(String modelKey) {
this.modelKey = modelKey;
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
counters.putIfAbsent(modelKey, new AtomicInteger(0));
AtomicInteger currentCounter = counters.get(modelKey);
currentCounter.incrementAndGet();
return Collections.emptyMap();
}
}
protected Cache<String, PublicKeysEntry> getKeysCache() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
gcb.globalJmxStatistics().allowDuplicateDomains(true);
final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
Configuration cfg = cb.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, cfg);
return cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
}
}

View file

@ -32,9 +32,11 @@ import javax.ws.rs.core.UriInfo;
*/ */
public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> { public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
protected final KeycloakSession session;
private final C config; private final C config;
public AbstractIdentityProvider(C config) { public AbstractIdentityProvider(KeycloakSession session, C config) {
this.session = session;
this.config = config; this.config = config;
} }

View file

@ -39,10 +39,11 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
* <p>Creates an {@link IdentityProvider} based on the configuration contained in * <p>Creates an {@link IdentityProvider} based on the configuration contained in
* <code>model</code>.</p> * <code>model</code>.</p>
* *
* @param session
* @param model The configuration to be used to create the identity provider. * @param model The configuration to be used to create the identity provider.
* @return * @return
*/ */
T create(IdentityProviderModel model); T create(KeycloakSession session, IdentityProviderModel model);
/** /**
* <p>Creates an {@link IdentityProvider} based on the configuration from * <p>Creates an {@link IdentityProvider} based on the configuration from

View file

@ -0,0 +1,30 @@
/*
* 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.keys;
import java.security.PublicKey;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface KeyLoader {
Map<String, PublicKey> loadKeys() throws Exception;
}

View file

@ -0,0 +1,40 @@
/*
* 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.keys;
import java.security.PublicKey;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface KeyStorageProvider extends Provider {
/**
* Get public key to verify messages signed by particular client. Used for example during JWT client authentication
*
* @param modelKey
* @param kid
* @param loader
* @return
*/
PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader);
}

View file

@ -0,0 +1,26 @@
/*
* 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.keys;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface KeyStorageProviderFactory extends ProviderFactory<KeyStorageProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* 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.keys;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeyStorageSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "keyStorage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return KeyStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return KeyStorageProviderFactory.class;
}
}

View file

@ -65,3 +65,4 @@ org.keycloak.policy.PasswordPolicyManagerSpi
org.keycloak.transaction.TransactionManagerLookupSpi org.keycloak.transaction.TransactionManagerLookupSpi
org.keycloak.credential.hash.PasswordHashSpi org.keycloak.credential.hash.PasswordHashSpi
org.keycloak.credential.CredentialSpi org.keycloak.credential.CredentialSpi
org.keycloak.keys.KeyStorageSpi

View file

@ -38,6 +38,7 @@ import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.keys.loader.KeyStorageManager;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -125,7 +126,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
} }
// Get client key and validate signature // Get client key and validate signature
PublicKey clientPublicKey = getSignatureValidationKey(client, context); PublicKey clientPublicKey = getSignatureValidationKey(client, context, jws);
if (clientPublicKey == null) { if (clientPublicKey == null) {
// Error response already set to context // Error response already set to context
return; return;
@ -166,13 +167,14 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
} }
} }
protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) { protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context, JWSInput jws) {
try { PublicKey publicKey = KeyStorageManager.getClientPublicKey(context.getSession(), client, jws);
return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX); if (publicKey == null) {
} catch (ModelException me) { Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unable to load public key");
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage());
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
return null; return null;
} else {
return publicKey;
} }
} }

View file

@ -73,8 +73,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type"; public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type";
public AbstractOAuth2IdentityProvider(C config) { public AbstractOAuth2IdentityProvider(KeycloakSession session, C config) {
super(config); super(session, config);
if (config.getDefaultScope() == null || config.getDefaultScope().isEmpty()) { if (config.getDefaultScope() == null || config.getDefaultScope().isEmpty()) {
config.setDefaultScope(getDefaultScopes()); config.setDefaultScope(getDefaultScopes());

View file

@ -23,6 +23,7 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
@ -46,8 +47,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN"; public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) { public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(config); super(session, config);
} }
@Override @Override
@ -56,8 +57,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
} }
@Override @Override
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) { protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
JsonWebToken access = validateToken(idpKey, response.getToken()); JsonWebToken access = validateToken(response.getToken());
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access); context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
} }
@ -76,13 +77,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
logger.warn("Failed to verify logout request"); logger.warn("Failed to verify logout request");
return Response.status(400).build(); return Response.status(400).build();
} }
PublicKey key = getExternalIdpKey();
if (key != null) { if (!verify(token)) {
if (!verify(token, key)) { logger.warn("Failed to verify logout request");
logger.warn("Failed to verify logout request"); return Response.status(400).build();
return Response.status(400).build();
}
} }
LogoutAction action = null; LogoutAction action = null;
try { try {
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class); action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);

View file

@ -36,8 +36,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
} }
@Override @Override
public KeycloakOIDCIdentityProvider create(IdentityProviderModel model) { public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new KeycloakOIDCIdentityProvider(new OIDCIdentityProviderConfig(model)); return new KeycloakOIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
} }
@Override @Override

View file

@ -31,6 +31,7 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.keys.loader.KeyStorageManager;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -71,8 +72,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE"; public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN"; public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
public OIDCIdentityProvider(OIDCIdentityProviderConfig config) { public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(config); super(session, config);
String defaultScope = config.getDefaultScope(); String defaultScope = config.getDefaultScope();
@ -86,21 +87,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return new OIDCEndpoint(callback, realm, event); return new OIDCEndpoint(callback, realm, event);
} }
protected PublicKey getExternalIdpKey() {
String signingCert = getConfig().getCertificateSignatureVerifier();
try {
if (signingCert != null && !signingCert.trim().equals("")) {
return PemUtils.decodeCertificate(signingCert).getPublicKey();
} else if (getConfig().getPublicKeySignatureVerifier() != null && !getConfig().getPublicKeySignatureVerifier().trim().equals("")) {
return PemUtils.decodePublicKey(getConfig().getPublicKeySignatureVerifier());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
protected class OIDCEndpoint extends Endpoint { protected class OIDCEndpoint extends Endpoint {
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) { public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event); super(callback, realm, event);
@ -233,7 +219,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return authorizationUrl; return authorizationUrl;
} }
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) { protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
} }
@ -245,14 +231,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} catch (IOException e) { } catch (IOException e) {
throw new IdentityBrokerException("Could not decode access token response.", e); throw new IdentityBrokerException("Could not decode access token response.", e);
} }
PublicKey key = getExternalIdpKey(); String accessToken = verifyAccessToken(tokenResponse);
String accessToken = verifyAccessToken(key, tokenResponse);
String encodedIdToken = tokenResponse.getIdToken(); String encodedIdToken = tokenResponse.getIdToken();
JsonWebToken idToken = validateToken(encodedIdToken);
JsonWebToken idToken = validateToken(key, encodedIdToken);
try { try {
String id = idToken.getSubject(); String id = idToken.getSubject();
@ -274,7 +257,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse); identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken); identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
processAccessTokenResponse(identity, key, tokenResponse); processAccessTokenResponse(identity, tokenResponse);
identity.setId(id); identity.setId(id);
identity.setName(name); identity.setName(name);
@ -305,7 +288,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} }
} }
private String verifyAccessToken(PublicKey key, AccessTokenResponse tokenResponse) { private String verifyAccessToken(AccessTokenResponse tokenResponse) {
String accessToken = tokenResponse.getToken(); String accessToken = tokenResponse.getToken();
if (accessToken == null) { if (accessToken == null) {
@ -314,14 +297,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return accessToken; return accessToken;
} }
protected boolean verify(JWSInput jws, PublicKey key) { protected boolean verify(JWSInput jws) {
if (key == null) return true;
if (!getConfig().isValidateSignature()) return true; if (!getConfig().isValidateSignature()) return true;
return RSAProvider.verify(jws, key);
PublicKey publicKey = KeyStorageManager.getIdentityProviderPublicKey(session, session.getContext().getRealm(), getConfig(), jws);
return publicKey != null && RSAProvider.verify(jws, publicKey);
} }
protected JsonWebToken validateToken(PublicKey key, String encodedToken) { protected JsonWebToken validateToken(String encodedToken) {
if (encodedToken == null) { if (encodedToken == null) {
throw new IdentityBrokerException("No token from server."); throw new IdentityBrokerException("No token from server.");
} }
@ -329,7 +313,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
JsonWebToken token; JsonWebToken token;
try { try {
JWSInput jws = new JWSInput(encodedToken); JWSInput jws = new JWSInput(encodedToken);
if (!verify(jws, key)) { if (!verify(jws)) {
throw new IdentityBrokerException("token signature validation failed"); throw new IdentityBrokerException("token signature validation failed");
} }
token = jws.readJsonContent(JsonWebToken.class); token = jws.readJsonContent(JsonWebToken.class);

View file

@ -17,12 +17,18 @@
package org.keycloak.broker.oidc; package org.keycloak.broker.oidc;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Pedro Igor * @author Pedro Igor
*/ */
public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig { public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
private static final String JWKS_URL = "jwksUrl";
private static final String USE_JWKS_URL = "useJwksUrl";
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) { public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
super(identityProviderModel); super(identityProviderModel);
} }
@ -46,13 +52,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
public void setLogoutUrl(String url) { public void setLogoutUrl(String url) {
getConfig().put("logoutUrl", url); getConfig().put("logoutUrl", url);
} }
public String getCertificateSignatureVerifier() {
return getConfig().get("certificateSignatureVerifier");
}
public void setCertificateSignatureVerifier(String signingCertificate) {
getConfig().put("certificateSignatureVerifier", signingCertificate);
}
public String getPublicKeySignatureVerifier() { public String getPublicKeySignatureVerifier() {
return getConfig().get("publicKeySignatureVerifier"); return getConfig().get("publicKeySignatureVerifier");
} }
@ -69,6 +69,22 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
getConfig().put("validateSignature", String.valueOf(validateSignature)); getConfig().put("validateSignature", String.valueOf(validateSignature));
} }
public boolean isUseJwksUrl() {
return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
}
public void setUseJwksUrl(boolean useJwksUrl) {
getConfig().put(USE_JWKS_URL, String.valueOf(useJwksUrl));
}
public String getJwksUrl() {
return getConfig().get(JWKS_URL);
}
public void setJwksUrl(String jwksUrl) {
getConfig().put(JWKS_URL, jwksUrl);
}
public boolean isBackchannelSupported() { public boolean isBackchannelSupported() {
return Boolean.valueOf(getConfig().get("backchannelSupported")); return Boolean.valueOf(getConfig().get("backchannelSupported"));
} }

View file

@ -18,13 +18,15 @@ package org.keycloak.broker.oidc;
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.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.jose.jwk.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.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import java.io.IOException; import java.io.IOException;
@ -47,8 +49,8 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
} }
@Override @Override
public OIDCIdentityProvider create(IdentityProviderModel model) { public OIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new OIDCIdentityProvider(new OIDCIdentityProviderConfig(model)); return new OIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
} }
@Override @Override
@ -75,24 +77,11 @@ 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) {
sendJwksRequest(session, rep, config); config.setValidateSignature(true);
config.setUseJwksUrl(true);
config.setJwksUrl(rep.getJwksUri());
} }
return config.getConfig(); return config.getConfig();
} }
protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
try {
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, 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);
}
}
} }

View file

@ -56,8 +56,8 @@ import java.security.PublicKey;
*/ */
public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> { public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class); protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class);
public SAMLIdentityProvider(SAMLIdentityProviderConfig config) { public SAMLIdentityProvider(KeycloakSession session, SAMLIdentityProviderConfig config) {
super(config); super(session, config);
} }
@Override @Override

View file

@ -50,8 +50,8 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
} }
@Override @Override
public SAMLIdentityProvider create(IdentityProviderModel model) { public SAMLIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new SAMLIdentityProvider(new SAMLIdentityProviderConfig(model)); return new SAMLIdentityProvider(session, new SAMLIdentityProviderConfig(model));
} }
@Override @Override

View file

@ -0,0 +1,101 @@
/*
* 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.keys.loader;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Map;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyLoader;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.util.JWKSUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientPublicKeyLoader implements KeyLoader {
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private final KeycloakSession session;
private final ClientModel client;
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
this.session = session;
this.client = client;
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
if (config.isUseJwksUrl()) {
String jwksUrl = config.getJwksUrl();
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
} else {
try {
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
PublicKey publicKey = getSignatureValidationKey(certInfo);
// Check if we have kid in DB, generate otherwise
String kid = certInfo.getKid() != null ? certInfo.getKid() : JWKBuilder.createKeyId(publicKey);
return Collections.singletonMap(kid, publicKey);
} catch (ModelException me) {
logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
return Collections.emptyMap();
}
}
}
private static PublicKey getSignatureValidationKey(CertificateRepresentation certInfo) throws ModelException {
String encodedCertificate = certInfo.getCertificate();
String encodedPublicKey = certInfo.getPublicKey();
if (encodedCertificate == null && encodedPublicKey == null) {
throw new ModelException("Client doesn't have certificate or publicKey configured");
}
if (encodedCertificate != null && encodedPublicKey != null) {
throw new ModelException("Client has both publicKey and certificate configured");
}
if (encodedCertificate != null) {
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
return clientCert.getPublicKey();
} else {
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
}
}
}

View file

@ -0,0 +1,62 @@
/*
* 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.keys.loader;
import java.security.PublicKey;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.KeyStorageProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeyStorageManager {
public static PublicKey getClientPublicKey(KeycloakSession session, ClientModel client, JWSInput input) {
String kid = input.getHeader().getKeyId();
KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
String modelKey = getModelKey(client);
ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client);
return keyStorage.getPublicKey(modelKey, kid, loader);
}
private static String getModelKey(ClientModel client) {
return client.getRealm().getId() + "::client::" + client.getId();
}
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
String kid = input.getHeader().getKeyId();
KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
String modelKey = getModelKey(realm, idpConfig);
OIDCIdentityProviderLoader loader = new OIDCIdentityProviderLoader(session, idpConfig);
return keyStorage.getPublicKey(modelKey, kid, loader);
}
private static String getModelKey(RealmModel realm, OIDCIdentityProviderConfig idpConfig) {
return realm.getId() + "::idp::" + idpConfig.getInternalId();
}
}

View file

@ -0,0 +1,80 @@
/*
* 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.keys.loader;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Map;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.util.PemUtils;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyLoader;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.services.ServicesLogger;
import org.keycloak.util.JWKSUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCIdentityProviderLoader implements KeyLoader {
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private final KeycloakSession session;
private final OIDCIdentityProviderConfig config;
public OIDCIdentityProviderLoader(KeycloakSession session, OIDCIdentityProviderConfig config) {
this.session = session;
this.config = config;
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
if (config.isUseJwksUrl()) {
String jwksUrl = config.getJwksUrl();
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
} else {
try {
PublicKey publicKey = getSavedPublicKey();
if (publicKey == null) {
return Collections.emptyMap();
}
String kid = JWKBuilder.createKeyId(publicKey);
return Collections.singletonMap(kid, publicKey);
} catch (Exception e) {
logger.warnf(e, "Unable to retrieve publicKey for verify signature of identityProvider '%s' . Error details: %s", config.getAlias(), e.getMessage());
return Collections.emptyMap();
}
}
}
protected PublicKey getSavedPublicKey() throws Exception {
if (config.getPublicKeySignatureVerifier() != null && !config.getPublicKeySignatureVerifier().trim().equals("")) {
return PemUtils.decodePublicKey(config.getPublicKeySignatureVerifier());
} else {
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
return null;
}
}
}

View file

@ -32,6 +32,10 @@ public class OIDCAdvancedConfigWrapper {
private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg"; private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
private static final String JWKS_URL = "jwks.url";
private static final String USE_JWKS_URL = "use.jwks.url";
private final ClientModel clientModel; private final ClientModel clientModel;
private final ClientRepresentation clientRep; private final ClientRepresentation clientRep;
@ -74,6 +78,23 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr); setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
} }
public boolean isUseJwksUrl() {
String useJwksUrl = getAttribute(USE_JWKS_URL);
return Boolean.parseBoolean(useJwksUrl);
}
public void setUseJwksUrl(boolean useJwksUrl) {
String val = String.valueOf(useJwksUrl);
setAttribute(USE_JWKS_URL, val);
}
public String getJwksUrl() {
return getAttribute(JWKS_URL);
}
public void setJwksUrl(String jwksUrl) {
setAttribute(JWKS_URL, jwksUrl);
}
private String getAttribute(String attrKey) { private String getAttribute(String attrKey) {
if (clientModel != null) { if (clientModel != null) {

View file

@ -81,7 +81,6 @@ import java.util.Set;
*/ */
public class TokenManager { public class TokenManager {
protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private static final String JWT = "JWT";
// Harcoded for now // Harcoded for now
Algorithm jwsAlgorithm = Algorithm.RS256; Algorithm jwsAlgorithm = Algorithm.RS256;
@ -621,7 +620,7 @@ public class TokenManager {
public String encodeToken(RealmModel realm, Object token) { public String encodeToken(RealmModel realm, Object token) {
String encodedToken = new JWSBuilder() String encodedToken = new JWSBuilder()
.type(JWT) .type(OAuth2Constants.JWT)
.kid(realm.getKeyId()) .kid(realm.getKeyId())
.jsonContent(token) .jsonContent(token)
.sign(jwsAlgorithm, realm.getPrivateKey()); .sign(jwsAlgorithm, realm.getPrivateKey());
@ -747,7 +746,7 @@ public class TokenManager {
AccessTokenResponse res = new AccessTokenResponse(); AccessTokenResponse res = new AccessTokenResponse();
if (accessToken != null) { if (accessToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(accessToken).sign(jwsAlgorithm, realm.getPrivateKey()); String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(accessToken).sign(jwsAlgorithm, realm.getPrivateKey());
res.setToken(encodedToken); res.setToken(encodedToken);
res.setTokenType("bearer"); res.setTokenType("bearer");
res.setSessionState(accessToken.getSessionState()); res.setSessionState(accessToken.getSessionState());
@ -765,11 +764,11 @@ public class TokenManager {
} }
if (idToken != null) { if (idToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(idToken).sign(jwsAlgorithm, realm.getPrivateKey()); String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(idToken).sign(jwsAlgorithm, realm.getPrivateKey());
res.setIdToken(encodedToken); res.setIdToken(encodedToken);
} }
if (refreshToken != null) { if (refreshToken != null) {
String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(refreshToken).sign(jwsAlgorithm, realm.getPrivateKey()); String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(refreshToken).sign(jwsAlgorithm, realm.getPrivateKey());
res.setRefreshToken(encodedToken); res.setRefreshToken(encodedToken);
if (refreshToken.getExpiration() != 0) { if (refreshToken.getExpiration() != 0) {
res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime()); res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());

View file

@ -54,12 +54,12 @@ public class AuthorizationEndpointRequestParserProcessor {
} }
if (requestParam != null) { if (requestParam != null) {
new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request); new AuthzEndpointRequestObjectParser(session, requestParam, client).parseRequest(request);
} else if (requestUriParam != null) { } else if (requestUriParam != null) {
InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam); InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam);
String retrievedRequest = StreamUtil.readString(is); String retrievedRequest = StreamUtil.readString(is);
new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request); new AuthzEndpointRequestObjectParser(session, retrievedRequest, client).parseRequest(request);
} }
return request; return request;

View file

@ -27,7 +27,10 @@ import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.keys.KeyStorageProvider;
import org.keycloak.keys.loader.KeyStorageManager;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
@ -41,7 +44,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
private final Map<String, Object> requestParams; private final Map<String, Object> requestParams;
public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception { public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
JWSInput input = new JWSInput(requestObject); JWSInput input = new JWSInput(requestObject);
JWSHeader header = input.getHeader(); JWSHeader header = input.getHeader();
@ -54,7 +57,11 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
if (header.getAlgorithm() == Algorithm.none) { if (header.getAlgorithm() == Algorithm.none) {
this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
} else if (header.getAlgorithm() == Algorithm.RS256) { } else if (header.getAlgorithm() == Algorithm.RS256) {
PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX); PublicKey clientPublicKey = KeyStorageManager.getClientPublicKey(session, client, input);
if (clientPublicKey == null) {
throw new RuntimeException("Client public key not found");
}
boolean verified = RSAProvider.verify(input, clientPublicKey); boolean verified = RSAProvider.verify(input, clientPublicKey);
if (!verified) { if (!verified) {
throw new RuntimeException("Failed to verify signature on 'request' object"); throw new RuntimeException("Failed to verify signature on 'request' object");

View file

@ -30,27 +30,14 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
/** /**
* TODO: Merge with JWKSUtils from keycloak-core?
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class JWKSUtils { public class JWKSHttpUtils {
public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException { public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException {
InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI); InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI);
String keySetString = StreamUtil.readString(is); String keySetString = StreamUtil.readString(is);
return JsonSerialization.readValue(keySetString, JSONWebKeySet.class); 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.isKeyTypeSupported(jwk.getKeyType())) {
return parser.toPublicKey();
}
}
return null;
}
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
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.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -31,7 +32,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper; import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.oidc.utils.JWKSUtils; import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils; import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
import org.keycloak.protocol.oidc.utils.SubjectType; import org.keycloak.protocol.oidc.utils.SubjectType;
@ -41,6 +42,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
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 org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.util.JWKSUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -94,19 +96,11 @@ public class DescriptionConverter {
} }
client.setClientAuthenticatorType(clientAuthFactory.getId()); client.setClientAuthenticatorType(clientAuthFactory.getId());
PublicKey publicKey = retrievePublicKey(session, clientOIDC); boolean publicKeySet = setPublicKey(clientOIDC, client);
if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) { if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && !publicKeySet) {
throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString()); throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
} }
if (publicKey != null) {
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
CertificateRepresentation rep = new CertificateRepresentation();
rep.setPublicKey(publicKeyPem);
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
}
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client); OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
if (clientOIDC.getUserinfoSignedResponseAlg() != null) { if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg()); Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg());
@ -122,27 +116,39 @@ public class DescriptionConverter {
} }
private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) { private static boolean setPublicKey(OIDCClientRepresentation clientOIDC, ClientRepresentation clientRep) {
if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) { if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
return null; return false;
} }
if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) { if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks"); throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks");
} }
JSONWebKeySet keySet; OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
if (clientOIDC.getJwks() != null) {
keySet = clientOIDC.getJwks();
} else {
try {
keySet = JWKSUtils.sendJwksRequest(session, 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); if (clientOIDC.getJwks() != null) {
JSONWebKeySet keySet = clientOIDC.getJwks();
JWK publicKeyJWk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
if (publicKeyJWk == null) {
return false;
} else {
PublicKey publicKey = JWKParser.create(publicKeyJWk).toPublicKey();
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
CertificateRepresentation rep = new CertificateRepresentation();
rep.setPublicKey(publicKeyPem);
rep.setKid(publicKeyJWk.getKeyId());
CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, rep, JWTClientAuthenticator.ATTR_PREFIX);
configWrapper.setUseJwksUrl(false);
return true;
}
} else {
configWrapper.setUseJwksUrl(true);
configWrapper.setJwksUrl(clientOIDC.getJwksUri());
return true;
}
} }
@ -176,6 +182,9 @@ public class DescriptionConverter {
if (config.getRequestObjectSignatureAlg() != null) { if (config.getRequestObjectSignatureAlg() != null) {
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString()); response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
} }
if (config.isUseJwksUrl()) {
response.setJwksUri(config.getJwksUrl());
}
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client); List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE; SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;

View file

@ -23,11 +23,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper; import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper; import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper; import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
import org.keycloak.protocol.oidc.utils.SubjectType; import org.keycloak.protocol.oidc.utils.SubjectType;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;

View file

@ -801,7 +801,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
throw new IdentityBrokerException("Could not find factory for identity provider [" + alias + "]."); throw new IdentityBrokerException("Could not find factory for identity provider [" + alias + "].");
} }
return providerFactory.create(identityProviderModel); return providerFactory.create(session, identityProviderModel);
} }
throw new IdentityBrokerException("Identity Provider [" + alias + "] not found."); throw new IdentityBrokerException("Identity Provider [" + alias + "] not found.");

View file

@ -27,16 +27,18 @@ 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.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
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.protocol.oidc.utils.JWKSHttpUtils;
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.services.util.CertificateInfoHelper;
import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@ -149,16 +151,15 @@ public class ClientAttributeCertificateResource {
throw new NotFoundException("Could not find client"); throw new NotFoundException("Could not find client");
} }
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
try { try {
CertificateRepresentation info = getCertFromRequest(input);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
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);
} }
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} }
/** /**
@ -180,20 +181,19 @@ public class ClientAttributeCertificateResource {
throw new NotFoundException("Could not find client"); throw new NotFoundException("Could not find client");
} }
CertificateRepresentation info = getCertFromRequest(uriInfo, input);
info.setPrivateKey(null);
try { try {
CertificateRepresentation info = getCertFromRequest(input);
info.setPrivateKey(null);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
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);
} }
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} }
private CertificateRepresentation getCertFromRequest(UriInfo uriInfo, MultipartFormDataInput input) throws IOException { private CertificateRepresentation getCertFromRequest(MultipartFormDataInput input) throws IOException {
auth.requireManage(); auth.requireManage();
CertificateRepresentation info = new CertificateRepresentation(); CertificateRepresentation info = new CertificateRepresentation();
Map<String, List<InputPart>> uploadForm = input.getFormDataMap(); Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
@ -218,10 +218,16 @@ public class ClientAttributeCertificateResource {
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
InputStream stream = inputParts.get(0).getBody(InputStream.class, null); InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class); JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); if (publicKeyJwk == null) {
info.setPublicKey(publicKeyPem); throw new IllegalStateException("Certificate not found for use sig");
return info; } else {
PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
info.setPublicKey(publicKeyPem);
info.setKid(publicKeyJwk.getKeyId());
return info;
}
} }

View file

@ -229,7 +229,7 @@ public class IdentityProviderResource {
try { try {
IdentityProviderFactory factory = getIdentityProviderFactory(); IdentityProviderFactory factory = getIdentityProviderFactory();
return factory.create(identityProviderModel).export(uriInfo, realm, format); return factory.create(session, identityProviderModel).export(uriInfo, realm, format);
} catch (Exception e) { } catch (Exception e) {
return ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND); return ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND);
} }

View file

@ -42,6 +42,8 @@ public class CertificateInfoHelper {
public static final String X509CERTIFICATE = "certificate"; public static final String X509CERTIFICATE = "certificate";
public static final String PUBLIC_KEY = "public.key"; public static final String PUBLIC_KEY = "public.key";
public static final String KID = "kid";
// CLIENT MODEL METHODS // CLIENT MODEL METHODS
@ -49,11 +51,13 @@ public class CertificateInfoHelper {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
CertificateRepresentation rep = new CertificateRepresentation(); CertificateRepresentation rep = new CertificateRepresentation();
rep.setCertificate(client.getAttribute(certificateAttribute)); rep.setCertificate(client.getAttribute(certificateAttribute));
rep.setPublicKey(client.getAttribute(publicKeyAttribute)); rep.setPublicKey(client.getAttribute(publicKeyAttribute));
rep.setPrivateKey(client.getAttribute(privateKeyAttribute)); rep.setPrivateKey(client.getAttribute(privateKeyAttribute));
rep.setKid(client.getAttribute(kidAttribute));
return rep; return rep;
} }
@ -63,6 +67,7 @@ public class CertificateInfoHelper {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
if (rep.getPublicKey() == null && rep.getCertificate() == null) { if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!"); throw new IllegalStateException("Both certificate and publicKey are null!");
@ -75,6 +80,7 @@ public class CertificateInfoHelper {
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
setOrRemoveAttr(client, kidAttribute, rep.getKid());
} }
private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) { private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) {
@ -86,36 +92,13 @@ public class CertificateInfoHelper {
} }
public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException {
CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix);
String encodedCertificate = certInfo.getCertificate();
String encodedPublicKey = certInfo.getPublicKey();
if (encodedCertificate == null && encodedPublicKey == null) {
throw new ModelException("Client doesn't have certificate or publicKey configured");
}
if (encodedCertificate != null && encodedPublicKey != null) {
throw new ModelException("Client has both publicKey and certificate configured");
}
// 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);
}
}
// CLIENT REPRESENTATION METHODS // CLIENT REPRESENTATION METHODS
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) { public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
if (rep.getPublicKey() == null && rep.getCertificate() == null) { if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!"); throw new IllegalStateException("Both certificate and publicKey are null!");
@ -128,6 +111,7 @@ public class CertificateInfoHelper {
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
setOrRemoveAttr(client, kidAttribute, rep.getKid());
} }
private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) { private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) {

View file

@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -37,8 +38,8 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
public static final String PROFILE_URL = "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name"; public static final String PROFILE_URL = "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name";
public static final String DEFAULT_SCOPE = "email"; public static final String DEFAULT_SCOPE = "email";
public FacebookIdentityProvider(OAuth2IdentityProviderConfig config) { public FacebookIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Pedro Igor * @author Pedro Igor
@ -34,8 +35,8 @@ public class FacebookIdentityProviderFactory extends AbstractIdentityProviderFac
} }
@Override @Override
public FacebookIdentityProvider create(IdentityProviderModel model) { public FacebookIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new FacebookIdentityProvider(new OAuth2IdentityProviderConfig(model)); return new FacebookIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
} }
@Override @Override

View file

@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -37,8 +38,8 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
public static final String PROFILE_URL = "https://api.github.com/user"; public static final String PROFILE_URL = "https://api.github.com/user";
public static final String DEFAULT_SCOPE = "user:email"; public static final String DEFAULT_SCOPE = "user:email";
public GitHubIdentityProvider(OAuth2IdentityProviderConfig config) { public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Pedro Igor * @author Pedro Igor
@ -34,8 +35,8 @@ public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFacto
} }
@Override @Override
public GitHubIdentityProvider create(IdentityProviderModel model) { public GitHubIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new GitHubIdentityProvider(new OAuth2IdentityProviderConfig(model)); return new GitHubIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
} }
@Override @Override

View file

@ -19,6 +19,7 @@ package org.keycloak.social.google;
import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,8 +31,8 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
public static final String PROFILE_URL = "https://www.googleapis.com/plus/v1/people/me/openIdConnect"; public static final String PROFILE_URL = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
public static final String DEFAULT_SCOPE = "openid profile email"; public static final String DEFAULT_SCOPE = "openid profile email";
public GoogleIdentityProvider(OIDCIdentityProviderConfig config) { public GoogleIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Pedro Igor * @author Pedro Igor
@ -34,8 +35,8 @@ public class GoogleIdentityProviderFactory extends AbstractIdentityProviderFacto
} }
@Override @Override
public GoogleIdentityProvider create(IdentityProviderModel model) { public GoogleIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new GoogleIdentityProvider(new OIDCIdentityProviderConfig(model)); return new GoogleIdentityProvider(session, new OIDCIdentityProviderConfig(model));
} }
@Override @Override

View file

@ -30,6 +30,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/** /**
* LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2 * LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2
@ -45,8 +46,8 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
public static final String PROFILE_URL = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,public-profile-url)?format=json"; public static final String PROFILE_URL = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,public-profile-url)?format=json";
public static final String DEFAULT_SCOPE = "r_basicprofile r_emailaddress"; public static final String DEFAULT_SCOPE = "r_basicprofile r_emailaddress";
public LinkedInIdentityProvider(OAuth2IdentityProviderConfig config) { public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Vlastimil Elias (velias at redhat dot com) * @author Vlastimil Elias (velias at redhat dot com)
@ -35,8 +36,8 @@ public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFac
} }
@Override @Override
public LinkedInIdentityProvider create(IdentityProviderModel model) { public LinkedInIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new LinkedInIdentityProvider(new OAuth2IdentityProviderConfig(model)); return new LinkedInIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
} }
@Override @Override

View file

@ -30,6 +30,7 @@ import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.models.KeycloakSession;
/** /**
* *
@ -46,8 +47,8 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
public static final String PROFILE_URL = "https://apis.live.net/v5.0/me"; public static final String PROFILE_URL = "https://apis.live.net/v5.0/me";
public static final String DEFAULT_SCOPE = "wl.basic,wl.emails"; public static final String DEFAULT_SCOPE = "wl.basic,wl.emails";
public MicrosoftIdentityProvider(OAuth2IdentityProviderConfig config) { public MicrosoftIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Vlastimil Elias (velias at redhat dot com) * @author Vlastimil Elias (velias at redhat dot com)
@ -34,8 +35,8 @@ public class MicrosoftIdentityProviderFactory extends AbstractIdentityProviderFa
} }
@Override @Override
public MicrosoftIdentityProvider create(IdentityProviderModel model) { public MicrosoftIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new MicrosoftIdentityProvider(new OAuth2IdentityProviderConfig(model)); return new MicrosoftIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
} }
@Override @Override

View file

@ -31,6 +31,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.models.KeycloakSession;
/** /**
* Stackoverflow social provider. See https://api.stackexchange.com/docs/authentication * Stackoverflow social provider. See https://api.stackexchange.com/docs/authentication
@ -46,8 +47,8 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
public static final String PROFILE_URL = "https://api.stackexchange.com/2.2/me?order=desc&sort=name&site=stackoverflow"; public static final String PROFILE_URL = "https://api.stackexchange.com/2.2/me?order=desc&sort=name&site=stackoverflow";
public static final String DEFAULT_SCOPE = ""; public static final String DEFAULT_SCOPE = "";
public StackoverflowIdentityProvider(StackOverflowIdentityProviderConfig config) { public StackoverflowIdentityProvider(KeycloakSession session, StackOverflowIdentityProviderConfig config) {
super(config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(PROFILE_URL);

View file

@ -19,6 +19,7 @@ package org.keycloak.social.stackoverflow;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Vlastimil Elias (velias at redhat dot com) * @author Vlastimil Elias (velias at redhat dot com)
@ -35,8 +36,8 @@ public class StackoverflowIdentityProviderFactory extends
} }
@Override @Override
public StackoverflowIdentityProvider create(IdentityProviderModel model) { public StackoverflowIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new StackoverflowIdentityProvider(new StackOverflowIdentityProviderConfig(model)); return new StackoverflowIdentityProvider(session, new StackOverflowIdentityProviderConfig(model));
} }
@Override @Override

View file

@ -57,8 +57,8 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
SocialIdentityProvider<OAuth2IdentityProviderConfig> { SocialIdentityProvider<OAuth2IdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class); protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
public TwitterIdentityProvider(OAuth2IdentityProviderConfig config) { public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(config); super(session, config);
} }
@Override @Override

View file

@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author Pedro Igor * @author Pedro Igor
@ -34,8 +35,8 @@ public class TwitterIdentityProviderFactory extends AbstractIdentityProviderFact
} }
@Override @Override
public TwitterIdentityProvider create(IdentityProviderModel model) { public TwitterIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new TwitterIdentityProvider(new OAuth2IdentityProviderConfig(model)); return new TwitterIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
} }
@Override @Override

View file

@ -24,6 +24,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -129,7 +130,7 @@ public class AbstractOAuth2IdentityProviderTest {
private static class TestProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> { private static class TestProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> {
public TestProvider(OAuth2IdentityProviderConfig config) { public TestProvider(OAuth2IdentityProviderConfig config) {
super(config); super(null, config);
} }
@Override @Override

View file

@ -120,7 +120,8 @@ public class TestingOIDCEndpointsApplicationResource {
} }
PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate(); PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate();
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey)); String kid = JWKBuilder.createKeyId(clientData.getSigningKeyPair().getPublic());
clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).rsa256(privateKey));
} else { } else {
throw new BadRequestException("Unknown argument: " + jwaAlgorithm); throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
} }

View file

@ -31,6 +31,7 @@ import org.keycloak.common.Version;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
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.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -73,6 +74,7 @@ import java.util.regex.Pattern;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
import static org.keycloak.testsuite.util.WaitUtils.pause; import static org.keycloak.testsuite.util.WaitUtils.pause;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
@ -264,6 +266,43 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
setAdapterAndServerTimeOffset(0, adapterActionsUrl); setAdapterAndServerTimeOffset(0, adapterActionsUrl);
} }
@Test
public void testClientWithJwksUri() throws Exception {
// Set client to bad JWKS URI
ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "secure-portal");
ClientRepresentation client = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
wrapper.setUseJwksUrl(true);
wrapper.setJwksUrl(securePortal + "/bad-jwks-url");
clientResource.update(client);
// Login should fail at the code-to-token
securePortal.navigateTo();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
String pageSource = driver.getPageSource();
assertCurrentUrlStartsWith(securePortal);
assertFalse(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
// Set client to correct JWKS URI
client = clientResource.toRepresentation();
wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
wrapper.setUseJwksUrl(true);
wrapper.setJwksUrl(securePortal + "/" + AdapterConstants.K_JWKS);
clientResource.update(client);
// Login to secure-portal should be fine now. Client keys downloaded from JWKS URI
securePortal.navigateTo();
assertCurrentUrlEquals(securePortal);
pageSource = driver.getPageSource();
assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
// Logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
.queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
driver.navigate().to(logoutUri);
}
@Test @Test
public void testLoginSSOAndLogout() { public void testLoginSSOAndLogout() {
// test login to customer-portal which does a bearer request to customer-db // test login to customer-portal which does a bearer request to customer-db

View file

@ -17,32 +17,18 @@
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.jose.jws.Algorithm; import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
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;
@ -50,15 +36,10 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; 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.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.OAuthClient;
import java.security.PrivateKey;
import java.util.*; import java.util.*;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -228,59 +209,6 @@ 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);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
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(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
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);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
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(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
Assert.assertEquals(200, accessTokenResponse.getStatusCode());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
}
@Test @Test
public void testSignaturesRequired() throws Exception { public void testSignaturesRequired() throws Exception {
OIDCClientRepresentation clientRep = createRep(); OIDCClientRepresentation clientRep = createRep();
@ -299,55 +227,6 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256); Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
} }
// Client auth with signedJWT - helper methods
private String getClientSignedJWT(String clientId, String privateKeyPem) {
String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem);
// Use token-endpoint as audience as OIDC conformance testsuite is using it too.
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() {
@Override
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString();
jwt.audience(tokenEndpointUrl);
return jwt;
}
};
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);
}
}
private void createTrustedHost(String name, int count) { private void createTrustedHost(String name, int count) {
Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create(name, count, count)); Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create(name, count, count));
Assert.assertEquals(201, response.getStatus()); Assert.assertEquals(201, response.getStatus());

View file

@ -0,0 +1,308 @@
/*
* 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.testsuite.client;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.client.registration.Auth;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.util.OAuthClient;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTest {
private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms);
testRealms.get(0).setPrivateKey(PRIVATE_KEY);
testRealms.get(0).setPublicKey(PUBLIC_KEY);
}
@Before
public void before() throws Exception {
super.before();
ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
reg.auth(Auth.token(token));
}
private OIDCClientRepresentation createRep() {
OIDCClientRepresentation client = new OIDCClientRepresentation();
client.setClientName("RegistrationAccessTokenTest");
client.setClientUri(OAuthClient.APP_ROOT);
client.setRedirectUris(Collections.singletonList(oauth.getRedirectUri()));
return client;
}
@Test
public void createClientWithJWKS_generatedKid() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
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
assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
}
// The "kid" is null in the signed JWT. This is backwards compatibility test as in versions prior to 2.3.0, the "kid" wasn't set by JWTClientCredentialsProvider
@Test
public void createClientWithJWKS_nullKid() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
clientRep.setJwks(keySet);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
// Tries to authenticate client with privateKey JWT
assertAuthenticateClientSuccess(generatedKeys, response, null);
}
// The "kid" is set manually to some custom value
@Test
public void createClientWithJWKS_customKid() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
// Override kid with custom value
keySet.getKeys()[0].setKeyId("a1");
clientRep.setJwks(keySet);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
// Tries to authenticate client with privateKey JWT
assertAuthenticateClientSuccess(generatedKeys, response, "a1");
}
@Test
public void createClientWithJWKSURI() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
Assert.assertNull(response.getClientSecret());
Assert.assertNull(response.getClientSecretExpiresAt());
Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri());
// Tries to authenticate client with privateKey JWT
assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
}
@Test
public void createClientWithJWKSURI_rotateClientKeys() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
Assert.assertNull(response.getClientSecret());
Assert.assertNull(response.getClientSecretExpiresAt());
Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri());
// Tries to authenticate client with privateKey JWT
assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
// Add new key to the jwks
Map<String, String> generatedKeys2 = oidcClientEndpointsResource.generateKeys();
// Error should happen. KeyStorageProvider won't yet download new keys because of timeout
assertAuthenticateClientError(generatedKeys2, response, KEEP_GENERATED_KID);
setTimeOffset(20);
// Now new keys should be successfully downloaded
assertAuthenticateClientSuccess(generatedKeys2, response, KEEP_GENERATED_KID);
}
// Client auth with signedJWT - helper methods
private void assertAuthenticateClientSuccess(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception {
KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys);
String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid);
OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
Assert.assertEquals(200, accessTokenResponse.getStatusCode());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
}
private void assertAuthenticateClientError(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception {
KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys);
String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid);
OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
Assert.assertEquals(400, accessTokenResponse.getStatusCode());
Assert.assertNull(accessTokenResponse.getAccessToken());
Assert.assertNotNull(accessTokenResponse.getError());
}
private KeyPair getKeyPairFromGeneratedPems(Map<String, String> generatedKeys) {
String privateKeyPem = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
String publicKeyPem = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem);
PublicKey publicKey = KeycloakModelUtils.getPublicKey(publicKeyPem);
return new KeyPair(publicKey, privateKey);
}
private static final String KEEP_GENERATED_KID = "KEEP_GENERATED_KID";
private String getClientSignedJWT(String clientId, KeyPair keyPair, final String kid) {
String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
// Use token-endpoint as audience as OIDC conformance testsuite is using it too.
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() {
@Override
public String createSignedRequestToken(String clientId, String realmInfoUrl) {
if (KEEP_GENERATED_KID.equals(kid)) {
return super.createSignedRequestToken(clientId, realmInfoUrl);
} else {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
return new JWSBuilder()
.kid(kid)
.jsonContent(jwt)
.rsa256(keyPair.getPrivate());
}
}
@Override
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString();
jwt.audience(tokenEndpointUrl);
return jwt;
}
};
jwtProvider.setupKeyPair(keyPair);
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);
}
}
}

View file

@ -24,7 +24,6 @@ import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
@ -32,7 +31,6 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
@ -40,6 +38,7 @@ import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider; import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.admin.client.resource.ClientAttributeCertificateResource; import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.*; import org.keycloak.common.util.*;
@ -70,6 +69,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import java.io.*; import java.io.*;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
@ -308,18 +308,21 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
keyStoreIs.close(); keyStoreIs.close();
client = getClient(testRealm.getRealm(), client.getId()).toRepresentation(); client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
X509Certificate x509Cert = (X509Certificate) keyStore.getCertificate(keyAlias);
assertCertificate(client, certOld, assertCertificate(client, certOld,
KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias))); KeycloakModelUtils.getPemFromCertificate(x509Cert));
// Try to login with the new keys // Try to login with the new keys
oauth.clientId(client.getClientId()); oauth.clientId(client.getClientId());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray()); PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
KeyPair keyPair = new KeyPair(x509Cert.getPublicKey(), privateKey);
OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(), OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(),
user.getCredentials().get(0).getValue(), user.getCredentials().get(0).getValue(),
getClientSignedJWT(privateKey, client.getClientId())); getClientSignedJWT(keyPair, client.getClientId()));
assertEquals(200, response.getStatusCode()); assertEquals(200, response.getStatusCode());
@ -469,7 +472,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
@Test @Test
public void testAssertionMissingIssuer() throws Exception { public void testAssertionMissingIssuer() throws Exception {
String invalidJwt = getClientSignedJWT(getClient1PrivateKey(), null); String invalidJwt = getClientSignedJWT(getClient1KeyPair(), null);
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -484,7 +487,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
@Test @Test
public void testAssertionUnknownClient() throws Exception { public void testAssertionUnknownClient() throws Exception {
String invalidJwt = getClientSignedJWT(getClient1PrivateKey(), "unknown-client"); String invalidJwt = getClientSignedJWT(getClient1KeyPair(), "unknown-client");
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -549,7 +552,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
@Test @Test
public void testAssertionInvalidSignature() throws Exception { public void testAssertionInvalidSignature() throws Exception {
// JWT for client1, but signed by privateKey of client2 // JWT for client1, but signed by privateKey of client2
String invalidJwt = getClientSignedJWT(getClient2PrivateKey(), "client1"); String invalidJwt = getClientSignedJWT(getClient2KeyPair(), "client1");
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@ -559,7 +562,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters); HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp); OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS); assertError(response, "client1", "unauthorized_client", AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED.toString().toLowerCase());
} }
@ -659,7 +662,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
private OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception { private OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception {
CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider(); CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider();
jwtProvider.setPrivateKey(getClient1PrivateKey()); jwtProvider.setupKeyPair(getClient1KeyPair());
jwtProvider.setTokenTimeout(10); jwtProvider.setTokenTimeout(10);
for (String claim : claims) { for (String claim : claims) {
@ -778,26 +781,26 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
} }
private String getClient1SignedJWT() { private String getClient1SignedJWT() {
return getClientSignedJWT(getClient1PrivateKey(), "client1"); return getClientSignedJWT(getClient1KeyPair(), "client1");
} }
private String getClient2SignedJWT() { private String getClient2SignedJWT() {
return getClientSignedJWT(getClient2PrivateKey(), "client2"); return getClientSignedJWT(getClient2KeyPair(), "client2");
} }
private PrivateKey getClient1PrivateKey() { private KeyPair getClient1KeyPair() {
return KeystoreUtil.loadPrivateKeyFromKeystore("classpath:client-auth-test/keystore-client1.jks", return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client1.jks",
"storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS); "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
} }
private PrivateKey getClient2PrivateKey() { private KeyPair getClient2KeyPair() {
return KeystoreUtil.loadPrivateKeyFromKeystore("classpath:client-auth-test/keystore-client2.jks", return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks",
"storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS); "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
} }
private String getClientSignedJWT(PrivateKey privateKey, String clientId) { private String getClientSignedJWT(KeyPair keyPair, String clientId) {
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider(); JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
jwtProvider.setPrivateKey(privateKey); jwtProvider.setupKeyPair(keyPair);
jwtProvider.setTokenTimeout(10); jwtProvider.setTokenTimeout(10);
return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl()); return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
} }

View file

@ -406,6 +406,9 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest {
CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX); CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX);
clientResource.update(clientRep); clientResource.update(clientRep);
// set time offset, so that new keys are downloaded
setTimeOffset(20);
// Check signed request_uri will pass // Check signed request_uri will pass
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode()); Assert.assertNotNull(response.getCode());

View file

@ -62,4 +62,5 @@ log4j.logger.org.jboss.resteasy=warn
log4j.logger.org.apache.directory.api=warn log4j.logger.org.apache.directory.api=warn
log4j.logger.org.apache.directory.server.core=warn log4j.logger.org.apache.directory.server.core=warn
log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace # log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
# log4j.logger.org.keycloak.keys.infinispan=trace

View file

@ -64,7 +64,7 @@ public class IdentityProviderRegistrationTest extends AbstractIdentityProviderMo
identityProviderModel.setAlias("custom-provider"); identityProviderModel.setAlias("custom-provider");
CustomSocialProvider customSocialProvider = providerFactory.create(identityProviderModel); CustomSocialProvider customSocialProvider = providerFactory.create(this.session, identityProviderModel);
assertNotNull(customSocialProvider); assertNotNull(customSocialProvider);
IdentityProviderModel config = customSocialProvider.getConfig(); IdentityProviderModel config = customSocialProvider.getConfig();
@ -87,7 +87,7 @@ public class IdentityProviderRegistrationTest extends AbstractIdentityProviderMo
identityProviderModel.setAlias("custom-provider"); identityProviderModel.setAlias("custom-provider");
CustomIdentityProvider provider = providerFactory.create(identityProviderModel); CustomIdentityProvider provider = providerFactory.create(this.session, identityProviderModel);
assertNotNull(provider); assertNotNull(provider);
IdentityProviderModel config = provider.getConfig(); IdentityProviderModel config = provider.getConfig();

View file

@ -158,7 +158,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertGoogleIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertGoogleIdentityProviderConfig(IdentityProviderModel identityProvider) {
GoogleIdentityProvider googleIdentityProvider = new GoogleIdentityProviderFactory().create(identityProvider); GoogleIdentityProvider googleIdentityProvider = new GoogleIdentityProviderFactory().create(session, identityProvider);
OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig(); OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig();
assertEquals("model-google", config.getAlias()); assertEquals("model-google", config.getAlias());
@ -176,7 +176,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertSamlIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertSamlIdentityProviderConfig(IdentityProviderModel identityProvider) {
SAMLIdentityProvider samlIdentityProvider = new SAMLIdentityProviderFactory().create(identityProvider); SAMLIdentityProvider samlIdentityProvider = new SAMLIdentityProviderFactory().create(session, identityProvider);
SAMLIdentityProviderConfig config = samlIdentityProvider.getConfig(); SAMLIdentityProviderConfig config = samlIdentityProvider.getConfig();
assertEquals("model-saml-signed-idp", config.getAlias()); assertEquals("model-saml-signed-idp", config.getAlias());
@ -196,7 +196,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertOidcIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertOidcIdentityProviderConfig(IdentityProviderModel identityProvider) {
OIDCIdentityProvider googleIdentityProvider = new OIDCIdentityProviderFactory().create(identityProvider); OIDCIdentityProvider googleIdentityProvider = new OIDCIdentityProviderFactory().create(session, identityProvider);
OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig(); OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig();
assertEquals("model-oidc-idp", config.getAlias()); assertEquals("model-oidc-idp", config.getAlias());
@ -210,7 +210,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) { private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider); FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(session, identityProvider);
OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig(); OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
assertEquals("model-facebook", config.getAlias()); assertEquals("model-facebook", config.getAlias());
@ -229,7 +229,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) { private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider); GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(session, identityProvider);
OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig(); OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
assertEquals("model-github", config.getAlias()); assertEquals("model-github", config.getAlias());
@ -248,7 +248,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) {
LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(identityProvider); LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(session, identityProvider);
OAuth2IdentityProviderConfig config = liIdentityProvider.getConfig(); OAuth2IdentityProviderConfig config = liIdentityProvider.getConfig();
assertEquals("model-linkedin", config.getAlias()); assertEquals("model-linkedin", config.getAlias());
@ -265,7 +265,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertStackoverflowIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertStackoverflowIdentityProviderConfig(IdentityProviderModel identityProvider) {
StackoverflowIdentityProvider soIdentityProvider = new StackoverflowIdentityProviderFactory().create(identityProvider); StackoverflowIdentityProvider soIdentityProvider = new StackoverflowIdentityProviderFactory().create(session, identityProvider);
StackOverflowIdentityProviderConfig config = soIdentityProvider.getConfig(); StackOverflowIdentityProviderConfig config = soIdentityProvider.getConfig();
assertEquals("model-stackoverflow", config.getAlias()); assertEquals("model-stackoverflow", config.getAlias());
@ -283,7 +283,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
} }
private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) { private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) {
TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(identityProvider); TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(session, identityProvider);
OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig(); OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig();
assertEquals("model-twitter", config.getAlias()); assertEquals("model-twitter", config.getAlias());

View file

@ -180,4 +180,5 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
public void testAccountManagementLinkedIdentityAlreadyExists() { public void testAccountManagementLinkedIdentityAlreadyExists() {
super.testAccountManagementLinkedIdentityAlreadyExists(); super.testAccountManagementLinkedIdentityAlreadyExists();
} }
} }

View file

@ -0,0 +1,174 @@
/*
* 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.testsuite.broker;
import javax.ws.rs.core.UriBuilder;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.util.Time;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest {
private static final int PORT = 8082;
private static Keycloak keycloak1;
private static Keycloak keycloak2;
@ClassRule
public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
@Override
protected void configureServer(KeycloakServer server) {
server.getConfig().setPort(PORT);
}
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
}
@Override
protected String[] getTestRealms() {
return new String[] { "realm-with-oidc-identity-provider" };
}
};
@BeforeClass
public static void beforeClazz() {
keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
}
@Override
public void onBefore() {
super.onBefore();
// Enable validate signatures
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setValidateSignature(true);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
}
@Override
protected String getProviderId() {
return "kc-oidc-idp";
}
@Test
public void testSignatureVerificationJwksUrl() throws Exception {
// Configure OIDC identity provider with JWKS URL
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(true);
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT));
String jwksUrl = b.build("realm-with-oidc-identity-provider").toString();
cfg.setJwksUrl(jwksUrl);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Set time offset. New keys can be downloaded. Check that user is able to login.
Time.setOffset(20);
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
Time.setOffset(0);
}
@Test
public void testSignatureVerificationHardcodedPublicKey() throws Exception {
// Configure OIDC identity provider with publicKeySignatureVerifier
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(false);
RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
cfg.setPublicKeySignatureVerifier(realm.getPublicKey());
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
Time.setOffset(20);
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
Time.setOffset(0);
}
}

View file

@ -29,8 +29,8 @@ import javax.ws.rs.core.Response;
*/ */
public class CustomIdentityProvider extends AbstractIdentityProvider<IdentityProviderModel> { public class CustomIdentityProvider extends AbstractIdentityProvider<IdentityProviderModel> {
public CustomIdentityProvider(IdentityProviderModel config) { public CustomIdentityProvider(KeycloakSession session, IdentityProviderModel config) {
super(config); super(session, config);
} }
@Override @Override

View file

@ -18,6 +18,7 @@ package org.keycloak.testsuite.broker.provider;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
/** /**
* @author pedroigor * @author pedroigor
@ -32,8 +33,8 @@ public class CustomIdentityProviderFactory extends AbstractIdentityProviderFacto
} }
@Override @Override
public CustomIdentityProvider create(IdentityProviderModel model) { public CustomIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new CustomIdentityProvider(model); return new CustomIdentityProvider(session, model);
} }
@Override @Override

View file

@ -30,8 +30,8 @@ import javax.ws.rs.core.Response;
*/ */
public class CustomSocialProvider extends AbstractIdentityProvider<IdentityProviderModel> implements SocialIdentityProvider<IdentityProviderModel> { public class CustomSocialProvider extends AbstractIdentityProvider<IdentityProviderModel> implements SocialIdentityProvider<IdentityProviderModel> {
public CustomSocialProvider(IdentityProviderModel config) { public CustomSocialProvider(KeycloakSession session, IdentityProviderModel config) {
super(config); super(session, config);
} }
@Override @Override

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.broker.provider.social;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
/** /**
* @author pedroigor * @author pedroigor
@ -33,8 +34,8 @@ public class CustomSocialProviderFactory extends AbstractIdentityProviderFactory
} }
@Override @Override
public CustomSocialProvider create(IdentityProviderModel model) { public CustomSocialProvider create(KeycloakSession session, IdentityProviderModel model) {
return new CustomSocialProvider(model); return new CustomSocialProvider(session, model);
} }
@Override @Override

View file

@ -293,6 +293,12 @@ gen-new-keys-and-cert=Generate new keys and certificate
import-certificate=Import Certificate import-certificate=Import Certificate
gen-client-private-key=Generate Client Private Key gen-client-private-key=Generate Client Private Key
generate-private-key=Generate Private Key generate-private-key=Generate Private Key
kid=Kid
kid.tooltip=KID (Key ID) of the client public key from imported JWKS.
use-jwks-url=Use JWKS URL
use-jwks-url.tooltip=If the switch is on, then client public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when client generates new keypair. If the switch is off, then public key (or certificate) from the Keycloak DB is used, so when client keypair changes, you always need to import new key (or certificate) to the Keycloak DB as well.
jwks-url=JWKS URL
jwks-url.tooltip=URL where client keys in JWK format are stored. See JWK specification for more details. If you use keycloak client adapter with "jwt" credential, then you can use URL of your app with '/k_jwks' suffix. For example 'http://www.myhost.com/myapp/k_jwks' .
archive-format=Archive Format archive-format=Archive Format
archive-format.tooltip=Java keystore or PKCS12 archive format. archive-format.tooltip=Java keystore or PKCS12 archive format.
key-alias=Key Alias key-alias=Key Alias
@ -468,6 +474,8 @@ select-account.option=select_account
prompt.tooltip=Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. prompt.tooltip=Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
validate-signatures=Validate Signatures validate-signatures=Validate Signatures
identity-provider.validate-signatures.tooltip=Enable/disable signature validation of external IDP signatures. identity-provider.validate-signatures.tooltip=Enable/disable signature validation of external IDP signatures.
identity-provider.use-jwks-url.tooltip=If the switch is on, then identity provider public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when identity provider generates new keypair. If the switch is off, then public key (or certificate) from the Keycloak DB is used, so when identity provider keypair changes, you always need to import new key to the Keycloak DB as well.
identity-provider.jwks-url.tooltip=URL where identity provider keys in JWK format are stored. See JWK specification for more details. If you use external keycloak identity provider, then you can use URL like 'http://broker-keycloak:8180/auth/realms/test/protocol/openid-connect/certs' assuming your brokered keycloak is running on 'http://broker-keycloak:8180' and it's realm is 'test' .
validating-public-key=Validating Public Key validating-public-key=Validating Public Key
identity-provider.validating-public-key.tooltip=The public key in PEM format that must be used to verify external IDP signatures. identity-provider.validating-public-key.tooltip=The public key in PEM format that must be used to verify external IDP signatures.
import-external-idp-config=Import External IDP Config import-external-idp-config=Import External IDP Config

View file

@ -124,13 +124,54 @@ module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret,
}; };
}); });
module.controller('ClientSignedJWTCtrl', function($scope, $location, ClientCertificate) { module.controller('ClientSignedJWTCtrl', function($scope, $location, Client, ClientCertificate, Notifications, $route) {
var signingKeyInfo = ClientCertificate.get({ realm : $scope.realm.realm, client : $scope.client.id, attribute: 'jwt.credential' }, var signingKeyInfo = ClientCertificate.get({ realm : $scope.realm.realm, client : $scope.client.id, attribute: 'jwt.credential' },
function() { function() {
$scope.signingKeyInfo = signingKeyInfo; $scope.signingKeyInfo = signingKeyInfo;
} }
); );
console.log('ClientSignedJWTCtrl invoked');
$scope.clientCopy = angular.copy($scope.client);
$scope.changed = false;
$scope.$watch('client', function() {
if (!angular.equals($scope.client, $scope.clientCopy)) {
$scope.changed = true;
}
}, true);
if ($scope.client.attributes["use.jwks.url"]) {
if ($scope.client.attributes["use.jwks.url"] == "true") {
$scope.useJwksUrl = true;
} else {
$scope.useJwksUrl = false;
}
}
$scope.switchChange = function() {
$scope.changed = true;
}
$scope.save = function() {
if ($scope.useJwksUrl == true) {
$scope.client.attributes["use.jwks.url"] = "true";
} else {
$scope.client.attributes["use.jwks.url"] = "false";
}
Client.update({
realm : $scope.realm.realm,
client : $scope.client.id
}, $scope.client, function() {
$scope.changed = false;
$scope.clientCopy = angular.copy($scope.client);
Notifications.success("Client authentication configuration has been saved to the client.");
});
};
$scope.importCertificate = function() { $scope.importCertificate = function() {
$location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/import/jwt.credential"); $location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/import/jwt.credential");
}; };
@ -139,8 +180,8 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, ClientCerti
$location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/export/jwt.credential"); $location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/export/jwt.credential");
}; };
$scope.cancel = function() { $scope.reset = function() {
$location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials"); $route.reload();
}; };
}); });

View file

@ -784,6 +784,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.identityProvider.enabled = true; $scope.identityProvider.enabled = true;
$scope.identityProvider.authenticateByDefault = false; $scope.identityProvider.authenticateByDefault = false;
$scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login'; $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login';
$scope.identityProvider.config.useJwksUrl = 'true';
$scope.newIdentityProvider = true; $scope.newIdentityProvider = true;
} }

View file

@ -1,34 +1,74 @@
<div> <div 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 data-ng-show="signingKeyInfo.certificate"> <div class="form-group">
<label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label> <label class="col-md-2 control-label" for="useJwksUrl">{{:: 'use-jwks-url' | translate}}</label>
<kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip> <div class="col-sm-6">
<input ng-model="useJwksUrl" name="useJwksUrl" id="useJwksUrl" ng-click="switchChange()" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'use-jwks-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="col-sm-10" data-ng-show="signingKeyInfo.certificate"> <div class="form-group" data-ng-show="useJwksUrl">
<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="jwksUrl">{{:: 'jwks-url' | translate}}</label>
</div> <div class="col-sm-6">
</div> <input class="form-control" type="text" name="jwksUrl" id="jwksUrl" data-ng-model="client.attributes['jwks.url']">
</div>
<kc-tooltip>{{:: 'jwks-url.tooltip' | translate}}</kc-tooltip>
</div>
<div data-ng-show="signingKeyInfo.publicKey"> <div data-ng-show="!useJwksUrl">
<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"> <div class="form-group" data-ng-show="signingKeyInfo.certificate">
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.publicKey}}</textarea> <label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
</div> <kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
</div>
<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 class="form-group" 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="form-group" data-ng-show="signingKeyInfo.kid">
<label class="col-md-2 control-label" for="kid">{{:: 'kid' | translate}}</label>
<kc-tooltip>{{:: 'kid.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-6">
<div class="row">
<div class="col-sm-6">
<input readonly kc-select-action="click" class="form-control" type="text" id="kid" name="kid" data-ng-model="signingKeyInfo.kid">
</div>
</div>
</div>
</div>
<div class="form-group" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
<label class="col-md-2 control-label"></label>
<div class="col-sm-6">
<div class="row">
<div class="col-sm-6">
{{:: 'no-client-certificate-configured' | translate}}
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
<button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">{{:: 'gen-new-keys-and-cert' | translate}}</button>
<button data-ng-disabled="useJwksUrl" class="btn btn-default" type="submit" data-ng-click="importCertificate()">{{:: 'import-certificate' | translate}}</button>
<button kc-save data-ng-disabled="!changed" data-ng-click="save()">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed" data-ng-click="reset()">{{:: 'cancel' | translate}}</button>
</div>
</div>
<div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
{{:: 'no-client-certificate-configured' | translate}}
</div>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
<button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">{{:: 'gen-new-keys-and-cert' | translate}}</button>
<button class="btn btn-default" type="submit" data-ng-click="importCertificate()">{{:: 'import-certificate' | translate}}</button>
</div>
</div>
</form>
</div> </div>

View file

@ -184,13 +184,35 @@
</div> </div>
<kc-tooltip>{{:: 'identity-provider.validate-signatures.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'identity-provider.validate-signatures.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group clearfix" data-ng-show="identityProvider.config.validateSignature == 'true'">
<label class="col-md-2 control-label" for="publicKeySignatureVerifier">{{:: 'validating-public-key' | translate}}</label> <div data-ng-show="identityProvider.config.validateSignature == 'true'">
<div class="col-md-6">
<textarea class="form-control" id="publicKeySignatureVerifier" ng-model="identityProvider.config.publicKeySignatureVerifier"/> <div class="form-group">
<label class="col-md-2 control-label" for="useJwksUrl">{{:: 'use-jwks-url' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.useJwksUrl" id="useJwksUrl" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'identity-provider.use-jwks-url.tooltip' | translate}}</kc-tooltip>
</div> </div>
<kc-tooltip>{{:: 'identity-provider.validating-public-key.tooltip' | translate}}</kc-tooltip>
<div class="form-group clearfix" data-ng-show="identityProvider.config.useJwksUrl == 'true'">
<label class="col-md-2 control-label" for="jwksUrl">{{:: 'jwks-url' | translate}} </label>
<div class="col-md-6">
<input class="form-control" id="jwksUrl" type="text" ng-model="identityProvider.config.jwksUrl">
</div>
<kc-tooltip>{{:: 'identity-provider.jwks-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-hide="identityProvider.config.useJwksUrl == 'true'">
<label class="col-md-2 control-label" for="publicKeySignatureVerifier">{{:: 'validating-public-key' | translate}}</label>
<div class="col-md-6">
<textarea class="form-control" id="publicKeySignatureVerifier" ng-model="identityProvider.config.publicKeySignatureVerifier"/>
</div>
<kc-tooltip>{{:: 'identity-provider.validating-public-key.tooltip' | translate}}</kc-tooltip>
</div>
</div> </div>
</fieldset> </fieldset>
<fieldset data-ng-show="newIdentityProvider"> <fieldset data-ng-show="newIdentityProvider">
<legend uncollapsed><span class="text">{{:: 'import-external-idp-config' | translate}}</span> <kc-tooltip>{{:: 'import-external-idp-config.tooltip' | translate}}</kc-tooltip></legend> <legend uncollapsed><span class="text">{{:: 'import-external-idp-config' | translate}}</span> <kc-tooltip>{{:: 'import-external-idp-config.tooltip' | translate}}</kc-tooltip></legend>

View file

@ -73,4 +73,11 @@ keycloak.server.subsys.default.config=\
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>\ <default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>\
<provider name="jboss" enabled="true"/>\ <provider name="jboss" enabled="true"/>\
</spi>\ </spi>\
<spi name="keyStorage">\
<provider name="infinispan" enabled="true">\
<properties>\
<property name="minTimeBetweenRequests" value="10"/>\
</properties>\
</provider>\
</spi>\
</subsystem>\ </subsystem>\

View file

@ -100,7 +100,8 @@ public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcesso
st.addDependency(cacheContainerService.append("offlineSessions")); st.addDependency(cacheContainerService.append("offlineSessions"));
st.addDependency(cacheContainerService.append("loginFailures")); st.addDependency(cacheContainerService.append("loginFailures"));
st.addDependency(cacheContainerService.append("work")); st.addDependency(cacheContainerService.append("work"));
st.addDependency(cacheContainerService.append("authorization"));; st.addDependency(cacheContainerService.append("authorization"));
st.addDependency(cacheContainerService.append("keys"));
} }
} }

View file

@ -19,3 +19,5 @@
/subsystem=keycloak-server/spi=connectionsInfinispan/provider=default/:add(properties={cacheContainer => "java:comp/env/infinispan/Keycloak"},enabled=true) /subsystem=keycloak-server/spi=connectionsInfinispan/provider=default/:add(properties={cacheContainer => "java:comp/env/infinispan/Keycloak"},enabled=true)
/subsystem=keycloak-server/spi=jta-lookup/:add(default-provider=${keycloak.jta.lookup.provider:jboss}) /subsystem=keycloak-server/spi=jta-lookup/:add(default-provider=${keycloak.jta.lookup.provider:jboss})
/subsystem=keycloak-server/spi=jta-lookup/provider=jboss/:add(enabled=true) /subsystem=keycloak-server/spi=jta-lookup/provider=jboss/:add(enabled=true)
/subsystem=keycloak-server/spi=keyStorage/:add
/subsystem=keycloak-server/spi=keyStorage/provider=infinispan/:add(properties={minTimeBetweenRequests => "10"},enabled=true)

View file

@ -36,6 +36,10 @@
<local-cache name="authorization"> <local-cache name="authorization">
<eviction max-entries="100" strategy="LRU"/> <eviction max-entries="100" strategy="LRU"/>
</local-cache> </local-cache>
<local-cache name="keys">
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
</cache-container> </cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server"> <cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default"> <local-cache name="default">
@ -97,6 +101,10 @@
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/> <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
<distributed-cache name="authorization" mode="SYNC" owners="1"/> <distributed-cache name="authorization" mode="SYNC" owners="1"/>
<replicated-cache name="work" mode="SYNC" /> <replicated-cache name="work" mode="SYNC" />
<local-cache name="keys">
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
</cache-container> </cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server"> <cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/> <transport lock-timeout="60000"/>