merge conflicts

This commit is contained in:
Bill Burke 2016-09-30 19:19:12 -04:00
commit d4c3fae546
170 changed files with 3439 additions and 690 deletions

3
.gitignore vendored
View file

@ -45,3 +45,6 @@ catalog.xml
######### #########
target target
# Maven shade
#############
*dependency-reduced-pom.xml

View file

@ -17,11 +17,20 @@
package org.keycloak.adapters; package org.keycloak.adapters;
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.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
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.representations.VersionRepresentation;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
@ -85,6 +94,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 +257,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,9 +17,17 @@
package org.keycloak.adapters.authentication; package org.keycloak.adapters.authentication;
import java.security.KeyPair;
import java.security.PublicKey;
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.representations.JsonWebToken;
import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
@ -38,7 +46,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 +56,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 +69,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 +103,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 +137,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

@ -22,8 +22,10 @@ import org.keycloak.common.constants.GenericConstants;
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;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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

@ -25,6 +25,7 @@ import java.util.Map;
public class IdentityProviderRepresentation { public class IdentityProviderRepresentation {
protected String alias; protected String alias;
protected String displayName;
protected String internalId; protected String internalId;
protected String providerId; protected String providerId;
protected boolean enabled = true; protected boolean enabled = true;
@ -176,4 +177,12 @@ public class IdentityProviderRepresentation {
this.trustEmail = trustEmail; this.trustEmail = trustEmail;
} }
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
} }

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

@ -1270,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
for (IdentityProviderEntity entity: entities) { for (IdentityProviderEntity entity: entities) {
IdentityProviderModel identityProviderModel = new IdentityProviderModel(); IdentityProviderModel identityProviderModel = new IdentityProviderModel();
identityProviderModel.setProviderId(entity.getProviderId()); identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias()); identityProviderModel.setAlias(entity.getAlias());
identityProviderModel.setDisplayName(entity.getDisplayName());
identityProviderModel.setInternalId(entity.getInternalId()); identityProviderModel.setInternalId(entity.getInternalId());
Map<String, String> config = entity.getConfig(); Map<String, String> config = entity.getConfig();
Map<String, String> copy = new HashMap<>(); Map<String, String> copy = new HashMap<>();
@ -1309,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entity.setInternalId(KeycloakModelUtils.generateId()); entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias()); entity.setAlias(identityProvider.getAlias());
entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId()); entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled()); entity.setEnabled(identityProvider.isEnabled());
entity.setStoreToken(identityProvider.isStoreToken()); entity.setStoreToken(identityProvider.isStoreToken());
@ -1342,6 +1344,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) { for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) { if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias()); entity.setAlias(identityProvider.getAlias());
entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled()); entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());

View file

@ -58,6 +58,9 @@ public class IdentityProviderEntity {
@Column(name="PROVIDER_ALIAS") @Column(name="PROVIDER_ALIAS")
private String alias; private String alias;
@Column(name="PROVIDER_DISPLAY_NAME")
private String displayName;
@Column(name="ENABLED") @Column(name="ENABLED")
private boolean enabled; private boolean enabled;
@ -181,6 +184,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail; this.trustEmail = trustEmail;
} }
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -33,8 +33,9 @@
<dropColumn tableName="USER_ENTITY" columnName="TOTP" /> <dropColumn tableName="USER_ENTITY" columnName="TOTP" />
<addColumn tableName="IDENTITY_PROVIDER">
<column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
</addColumn>
</changeSet> </changeSet>

View file

@ -912,6 +912,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
identityProviderModel.setProviderId(entity.getProviderId()); identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias()); identityProviderModel.setAlias(entity.getAlias());
identityProviderModel.setDisplayName(entity.getDisplayName());
identityProviderModel.setInternalId(entity.getInternalId()); identityProviderModel.setInternalId(entity.getInternalId());
Map<String, String> config = entity.getConfig(); Map<String, String> config = entity.getConfig();
Map<String, String> copy = new HashMap<>(); Map<String, String> copy = new HashMap<>();
@ -948,6 +949,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
entity.setInternalId(KeycloakModelUtils.generateId()); entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias()); entity.setAlias(identityProvider.getAlias());
entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId()); entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled()); entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setTrustEmail(identityProvider.isTrustEmail());
@ -978,6 +980,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) { for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) { if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias()); entity.setAlias(identityProvider.getAlias());
entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled()); entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail()); entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault()); entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());

View file

@ -26,6 +26,7 @@ public class IdentityProviderEntity {
private String internalId; private String internalId;
private String alias; private String alias;
private String displayName;
private String providerId; private String providerId;
private String name; private String name;
private boolean enabled; private boolean enabled;
@ -134,6 +135,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail; this.trustEmail = trustEmail;
} }
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

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

@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
private String postBrokerLoginFlowId; private String postBrokerLoginFlowId;
private String displayName;
/** /**
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items * <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
* in the map are understood by the identity provider implementation.</p> * in the map are understood by the identity provider implementation.</p>
@ -70,6 +72,7 @@ public class IdentityProviderModel implements Serializable {
this.internalId = model.getInternalId(); this.internalId = model.getInternalId();
this.providerId = model.getProviderId(); this.providerId = model.getProviderId();
this.alias = model.getAlias(); this.alias = model.getAlias();
this.displayName = model.getDisplayName();
this.config = new HashMap<String, String>(model.getConfig()); this.config = new HashMap<String, String>(model.getConfig());
this.enabled = model.isEnabled(); this.enabled = model.isEnabled();
this.trustEmail = model.isTrustEmail(); this.trustEmail = model.isTrustEmail();
@ -169,5 +172,13 @@ public class IdentityProviderModel implements Serializable {
public void setTrustEmail(boolean trustEmail) { public void setTrustEmail(boolean trustEmail) {
this.trustEmail = trustEmail; this.trustEmail = trustEmail;
} }
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
} }

View file

@ -1,12 +1,33 @@
/*
* 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; package org.keycloak.models;
/** /**
* Denotes an executable Script with metadata. * A representation of a Script with some additional meta-data.
* *
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/ */
public interface ScriptModel { public interface ScriptModel {
/**
* MIME-Type for JavaScript
*/
String TEXT_JAVASCRIPT = "text/javascript";
/** /**
* Returns the unique id of the script. {@literal null} for ad-hoc created scripts. * Returns the unique id of the script. {@literal null} for ad-hoc created scripts.
*/ */

View file

@ -18,6 +18,8 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils; import org.keycloak.common.util.PemUtils;
@ -685,4 +687,21 @@ public final class KeycloakModelUtils {
} }
} }
public static String getIdentityProviderDisplayName(KeycloakSession session, IdentityProviderModel provider) {
String displayName = provider.getDisplayName();
if (displayName != null && !displayName.isEmpty()) {
return displayName;
}
SocialIdentityProviderFactory providerFactory = (SocialIdentityProviderFactory) session.getKeycloakSessionFactory()
.getProviderFactory(SocialIdentityProvider.class, provider.getProviderId());
if (providerFactory != null) {
return providerFactory.getName();
} else {
return provider.getAlias();
}
}
} }

View file

@ -626,6 +626,7 @@ public class ModelToRepresentation {
providerRep.setInternalId(identityProviderModel.getInternalId()); providerRep.setInternalId(identityProviderModel.getInternalId());
providerRep.setProviderId(identityProviderModel.getProviderId()); providerRep.setProviderId(identityProviderModel.getProviderId());
providerRep.setAlias(identityProviderModel.getAlias()); providerRep.setAlias(identityProviderModel.getAlias());
providerRep.setDisplayName(identityProviderModel.getDisplayName());
providerRep.setEnabled(identityProviderModel.isEnabled()); providerRep.setEnabled(identityProviderModel.isEnabled());
providerRep.setStoreToken(identityProviderModel.isStoreToken()); providerRep.setStoreToken(identityProviderModel.isStoreToken());
providerRep.setTrustEmail(identityProviderModel.isTrustEmail()); providerRep.setTrustEmail(identityProviderModel.isTrustEmail());

View file

@ -1523,6 +1523,7 @@ public class RepresentationToModel {
identityProviderModel.setInternalId(representation.getInternalId()); identityProviderModel.setInternalId(representation.getInternalId());
identityProviderModel.setAlias(representation.getAlias()); identityProviderModel.setAlias(representation.getAlias());
identityProviderModel.setDisplayName(representation.getDisplayName());
identityProviderModel.setProviderId(representation.getProviderId()); identityProviderModel.setProviderId(representation.getProviderId());
identityProviderModel.setEnabled(representation.isEnabled()); identityProviderModel.setEnabled(representation.isEnabled());
identityProviderModel.setTrustEmail(representation.isTrustEmail()); identityProviderModel.setTrustEmail(representation.isTrustEmail());

View file

@ -1,64 +0,0 @@
package org.keycloak.scripting;
import org.keycloak.models.ScriptModel;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
/**
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class InvocableScript implements Invocable {
/**
* Holds the script metadata as well as the actual script.
*/
private final ScriptModel script;
/**
* Holds the {@link ScriptEngine} instance initialized with the script code.
*/
private final ScriptEngine scriptEngine;
public InvocableScript(ScriptModel script, ScriptEngine scriptEngine) {
this.script = script;
this.scriptEngine = scriptEngine;
}
@Override
public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
return getInvocableEngine().invokeMethod(thiz, name, args);
}
@Override
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
return getInvocableEngine().invokeFunction(name, args);
}
@Override
public <T> T getInterface(Class<T> clazz) {
return getInvocableEngine().getInterface(clazz);
}
@Override
public <T> T getInterface(Object thiz, Class<T> clazz) {
return getInvocableEngine().getInterface(thiz, clazz);
}
private Invocable getInvocableEngine() {
return (Invocable) scriptEngine;
}
/**
* Returns {@literal true} iif the {@link ScriptEngine} has a function with the given {@code functionName}.
* @param functionName
* @return
*/
public boolean hasFunction(String functionName){
Object candidate = scriptEngine.getContext().getAttribute(functionName);
return candidate != null;
}
}

View file

@ -0,0 +1,118 @@
/*
* 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.scripting;
import org.keycloak.models.ScriptModel;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
/**
* Wraps a {@link ScriptModel} and makes it {@link Invocable}.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class InvocableScriptAdapter implements Invocable {
/**
* Holds the {@ScriptModel}
*/
private final ScriptModel scriptModel;
/**
* Holds the {@link ScriptEngine} instance initialized with the script code.
*/
private final ScriptEngine scriptEngine;
/**
* Creates a new {@link InvocableScriptAdapter} instance.
*
* @param scriptModel must not be {@literal null}
* @param scriptEngine must not be {@literal null}
*/
public InvocableScriptAdapter(ScriptModel scriptModel, ScriptEngine scriptEngine) {
if (scriptModel == null) {
throw new IllegalArgumentException("scriptModel must not be null");
}
if (scriptEngine == null) {
throw new IllegalArgumentException("scriptEngine must not be null");
}
this.scriptModel = scriptModel;
this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
}
@Override
public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptExecutionException {
try {
return getInvocableEngine().invokeMethod(thiz, name, args);
} catch (ScriptException | NoSuchMethodException e) {
throw new ScriptExecutionException(scriptModel, e);
}
}
@Override
public Object invokeFunction(String name, Object... args) throws ScriptExecutionException {
try {
return getInvocableEngine().invokeFunction(name, args);
} catch (ScriptException | NoSuchMethodException e) {
throw new ScriptExecutionException(scriptModel, e);
}
}
@Override
public <T> T getInterface(Class<T> clazz) {
return getInvocableEngine().getInterface(clazz);
}
@Override
public <T> T getInterface(Object thiz, Class<T> clazz) {
return getInvocableEngine().getInterface(thiz, clazz);
}
/**
* Returns {@literal true} if the {@link ScriptEngine} has a definition with the given {@code name}.
*
* @param name
* @return
*/
public boolean isDefined(String name) {
Object candidate = scriptEngine.getContext().getAttribute(name);
return candidate != null;
}
private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
try {
engine.eval(script.getCode());
} catch (ScriptException se) {
throw new ScriptExecutionException(script, se);
}
return engine;
}
private Invocable getInvocableEngine() {
return (Invocable) scriptEngine;
}
}

View file

@ -1,8 +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.scripting; package org.keycloak.scripting;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
/** /**
* A {@link ScriptModel} which holds some meta-data.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/ */
public class Script implements ScriptModel { public class Script implements ScriptModel {

View file

@ -1,17 +1,33 @@
/*
* 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.scripting; package org.keycloak.scripting;
import javax.script.Bindings; import javax.script.Bindings;
/** /**
* Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}. * Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}.
* * <p>Used by {@link ScriptingProvider}</p>
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/ */
@FunctionalInterface @FunctionalInterface
public interface ScriptBindingsConfigurer { public interface ScriptBindingsConfigurer {
/** /**
* A default {@link ScriptBindingsConfigurer} leaves the Bindings empty. * A default {@link ScriptBindingsConfigurer} that provides no Bindings.
*/ */
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() { ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {

View file

@ -1,3 +1,19 @@
/*
* 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.scripting; package org.keycloak.scripting;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
@ -11,7 +27,7 @@ import javax.script.ScriptException;
*/ */
public class ScriptExecutionException extends RuntimeException { public class ScriptExecutionException extends RuntimeException {
public ScriptExecutionException(ScriptModel script, ScriptException se) { public ScriptExecutionException(ScriptModel script, Exception ex) {
super("Error executing script '" + script.getName() + "'", se); super("Could not execute script '" + script.getName() + "' problem was: " + ex.getMessage(), ex);
} }
} }

View file

@ -1,3 +1,19 @@
/*
* 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.scripting; package org.keycloak.scripting;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
@ -13,20 +29,23 @@ import javax.script.ScriptEngine;
public interface ScriptingProvider extends Provider { public interface ScriptingProvider extends Provider {
/** /**
* Returns an {@link InvocableScript} based on the given {@link ScriptModel}. * Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
* <p>The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p> * <p>The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
* *
* @param script the script to wrap * @param scriptModel the scriptModel to wrap
* @param bindingsConfigurer populates the {@link javax.script.Bindings} * @param bindingsConfigurer populates the {@link javax.script.Bindings}
* @return * @return
*/ */
InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer); InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
/** /**
* Returns an {@link InvocableScript} based on the given {@link ScriptModel} with an {@link ScriptBindingsConfigurer#EMPTY} {@code ScriptBindingsConfigurer}. * Creates a new {@link ScriptModel} instance.
* @see #prepareScript(ScriptModel, ScriptBindingsConfigurer) *
* @param script * @param realmId
* @param scriptName
* @param scriptCode
* @param scriptDescription
* @return * @return
*/ */
InvocableScript prepareScript(ScriptModel script); ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription);
} }

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

@ -1,23 +1,82 @@
/*
* 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.authentication.authenticators.browser; package org.keycloak.authentication.authenticators.browser;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.ScriptModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.scripting.InvocableScript; import org.keycloak.scripting.InvocableScriptAdapter;
import org.keycloak.scripting.Script; import org.keycloak.scripting.ScriptExecutionException;
import org.keycloak.scripting.ScriptBindingsConfigurer;
import org.keycloak.scripting.ScriptingProvider; import org.keycloak.scripting.ScriptingProvider;
import javax.script.Bindings;
import javax.script.ScriptException;
import java.util.Map; import java.util.Map;
/** /**
* An {@link Authenticator} that can execute a configured script during authentication flow. * An {@link Authenticator} that can execute a configured script during authentication flow.
* <p>scripts must provide </p> * <p>
* Scripts must at least provide one of the following functions:
* <ol>
* <li>{@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}</li>
* <li>{@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}</li>
* </ol>
* </p>
* <p>
* Custom {@link Authenticator Authenticator's} should at least provide the {@code authenticate(..)} function.
* The following script {@link javax.script.Bindings} are available for convenient use within script code.
* <ol>
* <li>{@code script} the {@link ScriptModel} to access script metadata</li>
* <li>{@code realm} the {@link RealmModel}</li>
* <li>{@code user} the current {@link UserModel}</li>
* <li>{@code session} the active {@link KeycloakSession}</li>
* <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
* <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
* </ol>
* </p>
* <p>
* Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
* or {@code action(context)} function.
* <p>
* An example {@link ScriptBasedAuthenticator} definition could look as follows:
* <pre>
* {@code
*
* AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
*
* function authenticate(context) {
*
* LOG.info(script.name + " --> trace auth for: " + user.username);
*
* if ( user.username === "tester"
* && user.getAttribute("someAttribute")
* && user.getAttribute("someAttribute").contains("someValue")) {
*
* context.failure(AuthenticationFlowError.INVALID_USER);
* return;
* }
*
* context.success();
* }
* }
* </pre>
* *
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a> * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/ */
@ -29,58 +88,51 @@ public class ScriptBasedAuthenticator implements Authenticator {
static final String SCRIPT_NAME = "scriptName"; static final String SCRIPT_NAME = "scriptName";
static final String SCRIPT_DESCRIPTION = "scriptDescription"; static final String SCRIPT_DESCRIPTION = "scriptDescription";
static final String ACTION = "action"; static final String ACTION_FUNCTION_NAME = "action";
static final String AUTHENTICATE = "authenticate"; static final String AUTHENTICATE_FUNCTION_NAME = "authenticate";
static final String TEXT_JAVASCRIPT = "text/javascript";
@Override @Override
public void authenticate(AuthenticationFlowContext context) { public void authenticate(AuthenticationFlowContext context) {
tryInvoke(AUTHENTICATE, context); tryInvoke(AUTHENTICATE_FUNCTION_NAME, context);
} }
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
tryInvoke(ACTION, context); tryInvoke(ACTION_FUNCTION_NAME, context);
} }
private void tryInvoke(String functionName, AuthenticationFlowContext context) { private void tryInvoke(String functionName, AuthenticationFlowContext context) {
InvocableScript script = getInvocableScript(context); if (!hasAuthenticatorConfig(context)) {
// this is an empty not yet configured script authenticator
// we mark this execution as success to not lock out users due to incompletely configured authenticators.
context.success();
return;
}
if (!script.hasFunction(functionName)) { InvocableScriptAdapter invocableScriptAdapter = getInvocableScriptAdapter(context);
if (!invocableScriptAdapter.isDefined(functionName)) {
return; return;
} }
try { try {
//should context be wrapped in a readonly wrapper? //should context be wrapped in a read-only wrapper?
script.invokeFunction(functionName, context); invocableScriptAdapter.invokeFunction(functionName, context);
} catch (ScriptException | NoSuchMethodException e) { } catch (ScriptExecutionException e) {
LOGGER.error(e); LOGGER.error(e);
context.failure(AuthenticationFlowError.INTERNAL_ERROR);
} }
} }
private InvocableScript getInvocableScript(final AuthenticationFlowContext context) { private boolean hasAuthenticatorConfig(AuthenticationFlowContext context) {
return context != null
final Script script = createAdhocScriptFromContext(context); && context.getAuthenticatorConfig() != null
&& context.getAuthenticatorConfig().getConfig() != null
ScriptBindingsConfigurer bindingsConfigurer = new ScriptBindingsConfigurer() { && !context.getAuthenticatorConfig().getConfig().isEmpty();
@Override
public void configureBindings(Bindings bindings) {
bindings.put("script", script);
bindings.put("LOG", LOGGER);
}
};
ScriptingProvider scripting = context.getSession().scripting();
//how to deal with long running scripts -> timeout?
return scripting.prepareScript(script, bindingsConfigurer);
} }
private Script createAdhocScriptFromContext(AuthenticationFlowContext context) { private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig(); Map<String, String> config = context.getAuthenticatorConfig().getConfig();
@ -90,21 +142,35 @@ public class ScriptBasedAuthenticator implements Authenticator {
RealmModel realm = context.getRealm(); RealmModel realm = context.getRealm();
return new Script(null /* scriptId */, realm.getId(), scriptName, TEXT_JAVASCRIPT, scriptCode, scriptDescription); ScriptingProvider scripting = context.getSession().scripting();
//TODO lookup script by scriptId instead of creating it every time
ScriptModel script = scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
//how to deal with long running scripts -> timeout?
return scripting.prepareInvocableScript(script, bindings -> {
bindings.put("script", script);
bindings.put("realm", context.getRealm());
bindings.put("user", context.getUser());
bindings.put("session", context.getSession());
bindings.put("httpRequest", context.getHttpRequest());
bindings.put("LOG", LOGGER);
});
} }
@Override @Override
public boolean requiresUser() { public boolean requiresUser() {
return false; return true;
} }
@Override @Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false; return true;
} }
@Override @Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
//TODO make RequiredActions configurable in the script
//NOOP //NOOP
} }

View file

@ -1,5 +1,23 @@
/*
* 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.authentication.authenticators.browser; package org.keycloak.authentication.authenticators.browser;
import org.apache.commons.io.IOUtils;
import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.AuthenticatorFactory;
@ -8,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import java.io.IOException;
import java.util.List; import java.util.List;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -24,7 +43,9 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
*/ */
public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory { public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
static final String PROVIDER_ID = "auth-script-based"; private static final Logger LOGGER = Logger.getLogger(ScriptBasedAuthenticatorFactory.class);
public static final String PROVIDER_ID = "auth-script-based";
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.REQUIRED,
@ -77,7 +98,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
@Override @Override
public boolean isUserSetupAllowed() { public boolean isUserSetupAllowed() {
return false; return true;
} }
@Override @Override
@ -87,12 +108,12 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
@Override @Override
public String getDisplayType() { public String getDisplayType() {
return "Script-based Authentication"; return "Script";
} }
@Override @Override
public String getHelpText() { public String getHelpText() {
return "Script based authentication."; return "Script based authentication. Allows to define custom authentication logic via JavaScript.";
} }
@Override @Override
@ -114,9 +135,16 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
script.setType(SCRIPT_TYPE); script.setType(SCRIPT_TYPE);
script.setName(SCRIPT_CODE); script.setName(SCRIPT_CODE);
script.setLabel("Script Source"); script.setLabel("Script Source");
script.setDefaultValue("//enter your script here");
script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate' that accepts a context (AuthenticationFlowContext) parameter." + String scriptTemplate = "//enter your script code here";
"This authenticator exposes the following additional variables: 'script', 'LOG'"); try {
scriptTemplate = IOUtils.toString(getClass().getResource("/scripts/authenticator-template.js"));
} catch (IOException ioe) {
LOGGER.warn(ioe);
}
script.setDefaultValue(scriptTemplate);
script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate(context)' that accepts a context (AuthenticationFlowContext) parameter.\n" +
"This authenticator exposes the following additional variables: 'script', 'realm', 'user', 'session', 'httpRequest', 'LOG'");
return asList(name, description, script); return asList(name, description, script);
} }

View file

@ -23,6 +23,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;
@ -120,7 +121,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;
@ -161,13 +162,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

@ -32,6 +32,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;
@ -70,8 +71,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();
@ -85,21 +86,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);
@ -232,7 +218,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) {
} }
@ -244,14 +230,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();
@ -273,7 +256,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);
@ -304,7 +287,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) {
@ -313,14 +296,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.");
} }
@ -328,7 +312,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

@ -19,12 +19,14 @@ package org.keycloak.broker.oidc;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
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.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.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

@ -22,8 +22,9 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.Urls; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.AccountService;
import org.keycloak.services.Urls;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.net.URI; import java.net.URI;
@ -69,7 +70,8 @@ public class AccountFederatedIdentityBean {
.queryParam("stateChecker", stateChecker) .queryParam("stateChecker", stateChecker)
.build().toString(); .build().toString();
FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, provider.getAlias(), provider.getAlias(), actionUrl, String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, displayName, provider.getAlias(), provider.getAlias(), actionUrl,
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null); provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
orderedSet.add(entry); orderedSet.add(entry);
} }
@ -105,10 +107,12 @@ public class AccountFederatedIdentityBean {
private final String providerName; private final String providerName;
private final String actionUrl; private final String actionUrl;
private final String guiOrder; private final String guiOrder;
private final String displayName;
public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String providerId, String providerName, String actionUrl, String guiOrder public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String displayName, String providerId,
) { String providerName, String actionUrl, String guiOrder) {
this.federatedIdentityModel = federatedIdentityModel; this.federatedIdentityModel = federatedIdentityModel;
this.displayName = displayName;
this.providerId = providerId; this.providerId = providerId;
this.providerName = providerName; this.providerName = providerName;
this.actionUrl = actionUrl; this.actionUrl = actionUrl;
@ -142,6 +146,11 @@ public class AccountFederatedIdentityBean {
public String getGuiOrder() { public String getGuiOrder() {
return guiOrder; return guiOrder;
} }
public String getDisplayName() {
return displayName;
}
} }
public static class IdentityProviderComparator implements Comparator<FederatedIdentityEntry> { public static class IdentityProviderComparator implements Comparator<FederatedIdentityEntry> {

View file

@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders(); List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData); identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo)); attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
@ -398,7 +398,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders(); List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData); identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo)); attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
@ -425,7 +425,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
} }
} }
@Override @Override
public Response createLogin() { public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN); return createResponse(LoginFormsPages.LOGIN);

View file

@ -17,7 +17,9 @@
package org.keycloak.forms.login.freemarker.model; package org.keycloak.forms.login.freemarker.model;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
@ -35,12 +37,13 @@ import java.util.TreeSet;
public class IdentityProviderBean { public class IdentityProviderBean {
private boolean displaySocial; private boolean displaySocial;
private List<IdentityProvider> providers; private List<IdentityProvider> providers;
private RealmModel realm; private RealmModel realm;
private final KeycloakSession session;
public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) { public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
this.realm = realm; this.realm = realm;
this.session = session;
if (!identityProviders.isEmpty()) { if (!identityProviders.isEmpty()) {
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE); Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
@ -59,7 +62,10 @@ public class IdentityProviderBean {
private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) { private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString(); String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
orderedSet.add(new IdentityProvider(identityProvider.getAlias(), identityProvider.getProviderId(), loginUrl, String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
displayName, identityProvider.getProviderId(), loginUrl,
identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null)); identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null));
} }
@ -77,9 +83,11 @@ public class IdentityProviderBean {
private final String providerId; // This refer to providerType (facebook, google, etc.) private final String providerId; // This refer to providerType (facebook, google, etc.)
private final String loginUrl; private final String loginUrl;
private final String guiOrder; private final String guiOrder;
private final String displayName;
public IdentityProvider(String alias, String providerId, String loginUrl, String guiOrder) { public IdentityProvider(String alias, String displayName, String providerId, String loginUrl, String guiOrder) {
this.alias = alias; this.alias = alias;
this.displayName = displayName;
this.providerId = providerId; this.providerId = providerId;
this.loginUrl = loginUrl; this.loginUrl = loginUrl;
this.guiOrder = guiOrder; this.guiOrder = guiOrder;
@ -100,6 +108,10 @@ public class IdentityProviderBean {
public String getGuiOrder() { public String getGuiOrder() {
return guiOrder; return guiOrder;
} }
public String getDisplayName() {
return displayName;
}
} }
public static class IdentityProviderComparator implements Comparator<IdentityProvider> { public static class IdentityProviderComparator implements Comparator<IdentityProvider> {
@ -112,7 +124,7 @@ public class IdentityProviderBean {
@Override @Override
public int compare(IdentityProvider o1, IdentityProvider o2) { public int compare(IdentityProvider o1, IdentityProvider o2) {
int o1order = parseOrder(o1); int o1order = parseOrder(o1);
int o2order = parseOrder(o2); int o2order = parseOrder(o2);
@ -120,7 +132,7 @@ public class IdentityProviderBean {
return 1; return 1;
else if (o1order < o2order) else if (o1order < o2order)
return -1; return -1;
return 1; return 1;
} }
@ -134,6 +146,5 @@ public class IdentityProviderBean {
} }
return 10000; return 10000;
} }
} }
} }

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

@ -52,12 +52,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

@ -22,7 +22,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 java.io.InputStream;
import java.security.PublicKey; import java.security.PublicKey;
/** /**
* 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

@ -1,3 +1,19 @@
/*
* 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.scripting; package org.keycloak.scripting;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
@ -6,7 +22,6 @@ import javax.script.Bindings;
import javax.script.ScriptContext; import javax.script.ScriptContext;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager; import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
/** /**
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}. * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@ -18,40 +33,70 @@ public class DefaultScriptingProvider implements ScriptingProvider {
private final ScriptEngineManager scriptEngineManager; private final ScriptEngineManager scriptEngineManager;
public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) { public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
if (scriptEngineManager == null) {
throw new IllegalStateException("scriptEngineManager must not be null!");
}
this.scriptEngineManager = scriptEngineManager; this.scriptEngineManager = scriptEngineManager;
} }
/**
* Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
*
* @param scriptModel must not be {@literal null}
* @param bindingsConfigurer must not be {@literal null}
* @return
*/
@Override @Override
public InvocableScript prepareScript(ScriptModel script) { public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
return prepareScript(script, ScriptBindingsConfigurer.EMPTY);
}
@Override if (scriptModel == null) {
public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) { throw new IllegalArgumentException("script must not be null");
if (script == null) {
throw new NullPointerException("script must not be null");
} }
if (script.getCode() == null || script.getCode().trim().isEmpty()) { if (scriptModel.getCode() == null || scriptModel.getCode().trim().isEmpty()) {
throw new IllegalArgumentException("script must not be null or empty"); throw new IllegalArgumentException("script must not be null or empty");
} }
if (bindingsConfigurer == null) { if (bindingsConfigurer == null) {
throw new NullPointerException("bindingsConfigurer must not be null"); throw new IllegalArgumentException("bindingsConfigurer must not be null");
} }
ScriptEngine engine = lookupScriptEngineFor(script); ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
if (engine == null) { return new InvocableScriptAdapter(scriptModel, engine);
}
//TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
//TODO allow script lookup by (scriptId)
//TODO allow script lookup by (name, realmName)
@Override
public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
return script;
}
/**
* Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
*
* @param script
* @param bindingsConfigurer
* @return
*/
private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
ScriptEngine scriptEngine = lookupScriptEngineFor(script);
if (scriptEngine == null) {
throw new IllegalStateException("Could not find ScriptEngine for script: " + script); throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
} }
configureBindings(bindingsConfigurer, engine); configureBindings(bindingsConfigurer, scriptEngine);
loadScriptIntoEngine(script, engine); return scriptEngine;
return new InvocableScript(script, engine);
} }
private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) { private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
@ -61,15 +106,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE); engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
} }
private void loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) { /**
* Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
try { */
engine.eval(script.getCode());
} catch (ScriptException se) {
throw new ScriptExecutionException(script, se);
}
}
private ScriptEngine lookupScriptEngineFor(ScriptModel script) { private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
return scriptEngineManager.getEngineByMimeType(script.getMimeType()); return scriptEngineManager.getEngineByMimeType(script.getMimeType());
} }

View file

@ -1,3 +1,19 @@
/*
* 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.scripting; package org.keycloak.scripting;
import org.keycloak.Config; import org.keycloak.Config;
@ -13,11 +29,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
static final String ID = "script-based-auth"; static final String ID = "script-based-auth";
private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
@Override @Override
public ScriptingProvider create(KeycloakSession session) { public ScriptingProvider create(KeycloakSession session) {
return new DefaultScriptingProvider(scriptEngineManager); return new DefaultScriptingProvider(ScriptEngineManagerHolder.SCRIPT_ENGINE_MANAGER);
} }
@Override @Override
@ -39,4 +53,12 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
public String getId() { public String getId() {
return ID; return ID;
} }
/**
* Holder class for lazy initialization of {@link ScriptEngineManager}.
*/
private static class ScriptEngineManagerHolder {
private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager();
}
} }

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

@ -805,7 +805,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

@ -28,15 +28,17 @@ 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.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;
@ -148,16 +150,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;
} }
/** /**
@ -179,20 +180,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();
@ -217,10 +217,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

@ -228,7 +228,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

@ -37,6 +37,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
@ -44,11 +46,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;
} }
@ -58,6 +62,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!");
@ -70,6 +75,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) {
@ -81,36 +87,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!");
@ -123,6 +106,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.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 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

@ -18,8 +18,9 @@ package org.keycloak.social.github;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; 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.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
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

@ -18,8 +18,9 @@ package org.keycloak.social.google;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
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

@ -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;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@ -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

@ -18,8 +18,9 @@ package org.keycloak.social.linkedin;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; 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.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
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

@ -28,6 +28,9 @@ 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 com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.models.KeycloakSession;
import java.net.URLEncoder; import java.net.URLEncoder;
/** /**
@ -45,8 +48,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

@ -25,6 +25,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;
import java.io.StringWriter; import java.io.StringWriter;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@ -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

@ -17,8 +17,9 @@
package org.keycloak.social.stackoverflow; package org.keycloak.social.stackoverflow;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
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

@ -18,8 +18,9 @@ package org.keycloak.social.twitter;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; 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.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
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

@ -0,0 +1,37 @@
/*
* Template for JavaScript based authenticator's.
* See org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory
*/
// import enum for error lookup
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
/**
* An example authenticate function.
*
* The following variables are available for convenience:
* user - current user {@see org.keycloak.models.UserModel}
* realm - current realm {@see org.keycloak.models.RealmModel}
* session - current KeycloakSession {@see org.keycloak.models.KeycloakSession}
* httpRequest - current HttpRequest {@see org.jboss.resteasy.spi.HttpRequest}
* script - current script {@see org.keycloak.models.ScriptModel}
* LOG - current logger {@see org.jboss.logging.Logger}
*
* You one can extract current http request headers via:
* httpRequest.getHttpHeaders().getHeaderString("Forwarded")
*
* @param context {@see org.keycloak.authentication.AuthenticationFlowContext}
*/
function authenticate(context) {
LOG.info(script.name + " trace auth for: " + user.username);
var authShouldFail = false;
if (authShouldFail) {
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
context.success();
}

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

@ -32,32 +32,32 @@ public class IdentityProviderBeanTest {
@Test @Test
public void testIdentityProviderComparator() { public void testIdentityProviderComparator() {
IdentityProvider o1 = new IdentityProvider("alias1", "id1", "ur1", null); IdentityProvider o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", null);
IdentityProvider o2 = new IdentityProvider("alias2", "id2", "ur2", null); IdentityProvider o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", null);
// guiOrder not defined at any object - first is always lower // guiOrder not defined at any object - first is always lower
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is not a number so it is same as not defined - first is always lower // guiOrder is not a number so it is same as not defined - first is always lower
o1 = new IdentityProvider("alias1", "id1", "ur1", "not a number"); o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "not a number");
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is defined for one only to it is always first // guiOrder is defined for one only to it is always first
o1 = new IdentityProvider("alias1", "id1", "ur1", "0"); o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is defined for both but is same - first is always lower // guiOrder is defined for both but is same - first is always lower
o1 = new IdentityProvider("alias1", "id1", "ur1", "0"); o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
o2 = new IdentityProvider("alias2", "id2", "ur2", "0"); o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "0");
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is reflected // guiOrder is reflected
o1 = new IdentityProvider("alias1", "id1", "ur1", "0"); o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
o2 = new IdentityProvider("alias2", "id2", "ur2", "1"); o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "1");
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));

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

@ -19,6 +19,7 @@
], ],
"realmRoles": [ "user" ], "realmRoles": [ "user" ],
"clientRoles": { "clientRoles": {
"realm-management" : [ "view-realm" ],
"account": ["view-profile", "manage-account"] "account": ["view-profile", "manage-account"]
} }
},{ },{

View file

@ -29,6 +29,7 @@
<button onclick="keycloak.register()">Register</button> <button onclick="keycloak.register()">Register</button>
<button onclick="refreshToken(9999)">Refresh Token</button> <button onclick="refreshToken(9999)">Refresh Token</button>
<button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button> <button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
<button onclick="refreshToken(5)">Refresh Token (if <5s validity)</button>
<button onclick="showError()">Show Error Response</button> <button onclick="showError()">Show Error Response</button>
<button onclick="loadProfile()">Get Profile</button> <button onclick="loadProfile()">Get Profile</button>
<button onclick="loadUserInfo()">Get User Info</button> <button onclick="loadUserInfo()">Get User Info</button>
@ -41,6 +42,13 @@
<button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button> <button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
<button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button> <button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
<button onclick="createBearerRequest()">Create Bearer Request</button> <button onclick="createBearerRequest()">Create Bearer Request</button>
<button onclick="output(showTime())">Show current time</button>
<input id="timeSkewInput"/>
<button onclick="addToTimeSkew()">timeSkew offset</button>
<button onclick="refreshTimeSkew()">refresh timeSkew</button>
<button onclick="sendBearerToKeycloak()">Bearer to keycloak</button>
<select id="flowSelect"> <select id="flowSelect">
<option value="standard">standard</option> <option value="standard">standard</option>
<option value="implicit">implicit</option> <option value="implicit">implicit</option>
@ -62,6 +70,9 @@
<h2>Events</h2> <h2>Events</h2>
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre> <pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
<h2>Info</h2>
TimeSkew: <div id="timeSkew"></div>
<script> <script>
function loadProfile() { function loadProfile() {
@ -135,6 +146,16 @@
document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e; document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
} }
function addToTimeSkew() {
var offset = document.getElementById("timeSkewInput").value;
keycloak.timeSkew += parseInt(offset);
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
}
function refreshTimeSkew() {
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
}
function createBearerRequest() { function createBearerRequest() {
var url = 'http://localhost:8280/js-database/customers'; var url = 'http://localhost:8280/js-database/customers';
@ -167,6 +188,33 @@
req.send(); req.send();
} }
function sendBearerToKeycloak() {
var url = 'http://localhost:8180/auth/admin/realms/example/roles';
if (window.location.href.indexOf("8543") > -1) {
url = url.replace("8180","8543");
url = url.replace("http","https");
}
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('Accept', 'application/json');
req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
output('Success');
} else if (req.status == 403) {
output('Forbidden');
} else if (req.status == 401) {
output('Unauthorized');
}
}
};
req.send();
}
var keycloak; var keycloak;
function keycloakInit() { function keycloakInit() {
@ -182,6 +230,7 @@
keycloak.onAuthRefreshSuccess = function () { keycloak.onAuthRefreshSuccess = function () {
event('Auth Refresh Success'); event('Auth Refresh Success');
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
}; };
keycloak.onAuthRefreshError = function () { keycloak.onAuthRefreshError = function () {

View file

@ -27,7 +27,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.ArrayList; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -52,7 +52,12 @@ public class AdminAlbumService {
List<Album> result = this.entityManager.createQuery("from Album").getResultList(); List<Album> result = this.entityManager.createQuery("from Album").getResultList();
for (Album album : result) { for (Album album : result) {
albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album); //We need to compile this under JDK7 so we can't use lambdas
//albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
if (!albums.containsKey(album.getUserId())) {
albums.put(album.getUserId(), Collections.singletonList(album));
}
} }
return Response.ok(albums).build(); return Response.ok(albums).build();

View file

@ -21,5 +21,18 @@
<module>photoz</module> <module>photoz</module>
<module>hello-world-authz-service</module> <module>hello-world-authz-service</module>
<module>servlet-authz</module> <module>servlet-authz</module>
<module>servlets</module>
</modules> </modules>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>integration-arquillian-test-apps</artifactId>
<groupId>org.keycloak.testsuite</groupId>
<version>2.3.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>integration-arquillian-test-apps-servlets</artifactId>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-adapter-api-public</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -24,12 +24,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.rotation.JWKPublicKeyLocator; import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import javax.servlet.Filter; import javax.servlet.*;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;

Some files were not shown because too many files have changed in this diff Show more