diff --git a/.gitignore b/.gitignore
index 44612eb8b4..6d655dccbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,6 @@ catalog.xml
#########
target
+# Maven shade
+#############
+*dependency-reduced-pom.xml
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
index b5506a5173..25c899eadf 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
@@ -17,11 +17,20 @@
package org.keycloak.adapters;
+import java.security.PublicKey;
+
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.spi.HttpFacade;
import org.keycloak.adapters.spi.UserSessionManagement;
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.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
@@ -85,6 +94,10 @@ public class PreAuthActionsHandler {
if (!resolveDeployment()) return true;
handleTestAvailable();
return true;
+ } else if (requestUri.endsWith(AdapterConstants.K_JWKS)) {
+ if (!resolveDeployment()) return true;
+ handleJwksRequest();
+ return true;
}
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);
+ }
+ }
+
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
index 8e06d5aa2c..8d07c007c2 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
@@ -17,9 +17,17 @@
package org.keycloak.adapters.authentication;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Map;
+
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
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.Time;
import org.keycloak.jose.jws.JWSBuilder;
@@ -38,7 +46,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
public static final String PROVIDER_ID = "jwt";
- private PrivateKey privateKey;
+ private KeyPair keyPair;
+ private JWK publicKeyJwk;
+
private int tokenTimeout;
@Override
@@ -46,8 +56,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
return PROVIDER_ID;
}
- public void setPrivateKey(PrivateKey privateKey) {
- this.privateKey = privateKey;
+ public void setupKeyPair(KeyPair keyPair) {
+ this.keyPair = keyPair;
+ this.publicKeyJwk = JWKBuilder.create().rs256(keyPair.getPublic());
}
public void setTokenTimeout(int tokenTimeout) {
@@ -58,6 +69,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
return tokenTimeout;
}
+ public PublicKey getPublicKey() {
+ return keyPair.getPublic();
+ }
+
@Override
public void init(KeycloakDeployment deployment, Object config) {
if (config == null || !(config instanceof Map)) {
@@ -88,7 +103,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
if (clientKeyAlias == null) {
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);
}
@@ -120,8 +137,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
public String createSignedRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
return new JWSBuilder()
+ .kid(publicKeyJwk.getKeyId())
.jsonContent(jwt)
- .rsa256(privateKey);
+ .rsa256(keyPair.getPrivate());
}
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
index cdb59f5b8a..502d0c06e1 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
@@ -71,8 +71,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
sendRequest(deployment);
lastRequestTime = currentTime;
} else {
- // TODO: debug
- log.infof("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
+ log.debugf("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) {
- // Send the request
- // TODO: trace or remove?
- log.infof("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
+ if (log.isTraceEnabled()) {
+ log.tracef("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
+ }
HttpGet getMethod = new HttpGet(deployment.getJwksUrl());
try {
@@ -93,8 +92,9 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
Map publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
- // TODO: Debug with condition
- log.infof("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
+ if (log.isDebugEnabled()) {
+ log.debugf("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
+ }
// Update current keys
currentKeys.clear();
diff --git a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
index 88ecd1942f..dad2a31eff 100755
--- a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
+++ b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
@@ -22,8 +22,10 @@ import org.keycloak.common.constants.GenericConstants;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
+import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
+import java.security.PublicKey;
/**
* @author Bill Burke
@@ -49,7 +51,7 @@ public class KeystoreUtil {
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);
try {
@@ -61,11 +63,12 @@ public class KeystoreUtil {
}
keyStore.load(stream, storePassword.toCharArray());
- PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
- if (key == null) {
+ PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
+ if (privateKey == null) {
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) {
throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 2a7d37b4f7..cf52423efe 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -81,6 +81,8 @@ public interface OAuth2Constants {
String MAX_AGE = "max_age";
+ String JWT = "JWT";
+
}
diff --git a/core/src/main/java/org/keycloak/constants/AdapterConstants.java b/core/src/main/java/org/keycloak/constants/AdapterConstants.java
index 6458d9056f..ea1086ed6c 100755
--- a/core/src/main/java/org/keycloak/constants/AdapterConstants.java
+++ b/core/src/main/java/org/keycloak/constants/AdapterConstants.java
@@ -29,6 +29,7 @@ public interface AdapterConstants {
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_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
// org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor. We have this value in
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
index 824ff55b5c..ae37ebf80a 100644
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
@@ -34,6 +34,7 @@ public class JWKBuilder {
public static final String DEFAULT_PUBLIC_KEY_USE = "sig";
public static final String DEFAULT_MESSAGE_DIGEST = "SHA-256";
+ private String kid;
private JWKBuilder() {
}
@@ -42,11 +43,18 @@ public class JWKBuilder {
return new JWKBuilder();
}
+ public JWKBuilder kid(String kid) {
+ this.kid = kid;
+ return this;
+ }
+
public JWK rs256(PublicKey key) {
RSAPublicKey rsaKey = (RSAPublicKey) key;
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.setAlgorithm(RSAPublicJWK.RS256);
k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE);
@@ -56,7 +64,7 @@ public class JWKBuilder {
return k;
}
- private String createKeyId(Key key) {
+ public static String createKeyId(Key key) {
try {
return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded()));
} catch (NoSuchAlgorithmException e) {
diff --git a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
index 03539ffd43..d10616caaf 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
@@ -27,6 +27,7 @@ public class CertificateRepresentation {
protected String privateKey;
protected String publicKey;
protected String certificate;
+ protected String kid;
public String getPrivateKey() {
return privateKey;
@@ -52,5 +53,11 @@ public class CertificateRepresentation {
this.certificate = certificate;
}
+ public String getKid() {
+ return kid;
+ }
+ public void setKid(String kid) {
+ this.kid = kid;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
index 2466f12e4f..40b530181e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
@@ -25,6 +25,7 @@ import java.util.Map;
public class IdentityProviderRepresentation {
protected String alias;
+ protected String displayName;
protected String internalId;
protected String providerId;
protected boolean enabled = true;
@@ -176,4 +177,12 @@ public class IdentityProviderRepresentation {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
}
diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java
index dbf6cd38ea..8db20e854c 100644
--- a/core/src/main/java/org/keycloak/util/JWKSUtils.java
+++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java
@@ -42,4 +42,15 @@ public class JWKSUtils {
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;
+ }
}
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index d67ce63f02..856980ad54 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -89,6 +89,10 @@
+
+
+
+
diff --git a/distribution/server-overlay/cli/keycloak-install-ha.cli b/distribution/server-overlay/cli/keycloak-install-ha.cli
index bc7d8639d2..17fd5f0d25 100644
--- a/distribution/server-overlay/cli/keycloak-install-ha.cli
+++ b/distribution/server-overlay/cli/keycloak-install-ha.cli
@@ -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=authorization:add(mode="SYNC",owners="1")
/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)
run-batch --file=default-keycloak-subsys-config.cli
diff --git a/distribution/server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/cli/keycloak-install.cli
index dbb2c32eac..d62bcf1b66 100644
--- a/distribution/server-overlay/cli/keycloak-install.cli
+++ b/distribution/server-overlay/cli/keycloak-install.cli
@@ -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=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=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)
run-batch --file=default-keycloak-subsys-config.cli
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 7b5f1d9e2e..8ad75fd6a8 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -17,6 +17,8 @@
package org.keycloak.connections.infinispan;
+import java.util.concurrent.TimeUnit;
+
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
@@ -98,7 +100,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
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);
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;
}
- 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.AUTHORIZATION_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
+
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) {
throw new RuntimeException("Failed to retrieve cache container", e);
@@ -116,6 +120,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
protected void initEmbedded() {
+
+
+
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = config.getBoolean("clustered", false);
@@ -176,7 +183,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
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);
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;
}
- 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.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();
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.
- // After start+end revisions batch is left the JTA transaction in committed state. This is incorrect and causes other issues afterwards.
- // TODO: Investigate
- // if (!managed)
- cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
+ // Use Dummy manager even in managed ( wildfly/eap ) environment. We don't want infinispan to participate in global transaction
+ cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
cb.transaction().lockingMode(LockingMode.PESSIMISTIC);
@@ -204,4 +211,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
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();
+ }
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 143056ca9a..146846039e 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -39,6 +39,10 @@ public interface InfinispanConnectionProvider extends Provider {
String WORK_CACHE_NAME = "work";
String AUTHORIZATION_CACHE_NAME = "authorization";
+ String KEYS_CACHE_NAME = "keys";
+ int KEYS_CACHE_DEFAULT_MAX = 500;
+ int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
+
Cache getCache(String name);
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java
new file mode 100644
index 0000000000..9010b16b1f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java
@@ -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 Marek Posolda
+ */
+public class InfinispanKeyStorageProvider implements KeyStorageProvider {
+
+ private static final Logger log = Logger.getLogger(InfinispanKeyStorageProvider.class);
+
+ private final Cache keys;
+
+ private final Map> tasksInProgress;
+
+ private final int minTimeBetweenRequests ;
+
+ public InfinispanKeyStorageProvider(Cache keys, Map> 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 task = new FutureTask<>(wrapperCallable);
+ FutureTask 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 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 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 {
+
+ 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 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;
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java
new file mode 100644
index 0000000000..3a4ba5a809
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java
@@ -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 Marek Posolda
+ */
+public class InfinispanKeyStorageProviderFactory implements KeyStorageProviderFactory {
+
+ private static final Logger log = Logger.getLogger(InfinispanKeyStorageProviderFactory.class);
+
+ public static final String PROVIDER_ID = "infinispan";
+
+ private Cache keysCache;
+
+ private final Map> 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;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java
new file mode 100644
index 0000000000..cb0561cc14
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java
@@ -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 Marek Posolda
+ */
+public class PublicKeysEntry implements Serializable {
+
+ private final int lastRequestTime;
+
+ private final Map currentKeys;
+
+ public PublicKeysEntry(int lastRequestTime, Map currentKeys) {
+ this.lastRequestTime = lastRequestTime;
+ this.currentKeys = currentKeys;
+ }
+
+ public int getLastRequestTime() {
+ return lastRequestTime;
+ }
+
+ public Map getCurrentKeys() {
+ return currentKeys;
+ }
+}
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory
new file mode 100644
index 0000000000..bed25ead63
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory
@@ -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
\ No newline at end of file
diff --git a/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java b/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java
new file mode 100644
index 0000000000..99a5f07cde
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java
@@ -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 Marek Posolda
+ */
+public class InfinispanKeyStorageProviderTest {
+
+ private Map counters = new ConcurrentHashMap<>();
+
+ Cache keys = getKeysCache();
+ Map> 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 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 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 loadKeys() throws Exception {
+ counters.putIfAbsent(modelKey, new AtomicInteger(0));
+ AtomicInteger currentCounter = counters.get(modelKey);
+
+ currentCounter.incrementAndGet();
+ return Collections.emptyMap();
+ }
+ }
+
+
+ protected Cache 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);
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 71abbad30d..21aa8d0648 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1270,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel {
for (IdentityProviderEntity entity: entities) {
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
-
identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias());
+ identityProviderModel.setDisplayName(entity.getDisplayName());
+
identityProviderModel.setInternalId(entity.getInternalId());
Map config = entity.getConfig();
Map copy = new HashMap<>();
@@ -1309,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel {
entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled());
entity.setStoreToken(identityProvider.isStoreToken());
@@ -1342,6 +1344,7 @@ public class RealmAdapter implements RealmModel, JpaModel {
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
index 53856fdbde..d8a9d7eb78 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
@@ -58,6 +58,9 @@ public class IdentityProviderEntity {
@Column(name="PROVIDER_ALIAS")
private String alias;
+ @Column(name="PROVIDER_DISPLAY_NAME")
+ private String displayName;
+
@Column(name="ENABLED")
private boolean enabled;
@@ -181,6 +184,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
index 6a3f9feb73..a73ee6bc5d 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
@@ -33,8 +33,9 @@
-
-
+
+
+
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index f88fe5224e..4ebfca851d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -912,6 +912,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias());
+ identityProviderModel.setDisplayName(entity.getDisplayName());
identityProviderModel.setInternalId(entity.getInternalId());
Map config = entity.getConfig();
Map copy = new HashMap<>();
@@ -948,6 +949,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
@@ -978,6 +980,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
index 1cfeb19ff0..e536fc8ef8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
@@ -26,6 +26,7 @@ public class IdentityProviderEntity {
private String internalId;
private String alias;
+ private String displayName;
private String providerId;
private String name;
private boolean enabled;
@@ -134,6 +135,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index 5532606270..8f571331a9 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -32,9 +32,11 @@ import javax.ws.rs.core.UriInfo;
*/
public abstract class AbstractIdentityProvider implements IdentityProvider {
+ protected final KeycloakSession session;
private final C config;
- public AbstractIdentityProvider(C config) {
+ public AbstractIdentityProvider(KeycloakSession session, C config) {
+ this.session = session;
this.config = config;
}
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
index 93ef720fad..c189316288 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
@@ -39,10 +39,11 @@ public interface IdentityProviderFactory extends Pro
*
Creates an {@link IdentityProvider} based on the configuration contained in
* model.
*
+ * @param session
* @param model The configuration to be used to create the identity provider.
* @return
*/
- T create(IdentityProviderModel model);
+ T create(KeycloakSession session, IdentityProviderModel model);
/**
*
Creates an {@link IdentityProvider} based on the configuration from
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java b/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
new file mode 100644
index 0000000000..aa5631dacc
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
@@ -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 Marek Posolda
+ */
+public interface KeyLoader {
+
+ Map loadKeys() throws Exception;
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java
new file mode 100644
index 0000000000..3033f2b062
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java
@@ -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 Marek Posolda
+ */
+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);
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java
new file mode 100644
index 0000000000..2ae9f1d41b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java
@@ -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 Marek Posolda
+ */
+public interface KeyStorageProviderFactory extends ProviderFactory {
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java
new file mode 100644
index 0000000000..162e186219
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java
@@ -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 Marek Posolda
+ */
+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;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
index 2425c7d877..41a1e41fee 100755
--- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
@@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
private String postBrokerLoginFlowId;
+ private String displayName;
+
/**
*
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.
@@ -70,6 +72,7 @@ public class IdentityProviderModel implements Serializable {
this.internalId = model.getInternalId();
this.providerId = model.getProviderId();
this.alias = model.getAlias();
+ this.displayName = model.getDisplayName();
this.config = new HashMap(model.getConfig());
this.enabled = model.isEnabled();
this.trustEmail = model.isTrustEmail();
@@ -169,5 +172,13 @@ public class IdentityProviderModel implements Serializable {
public void setTrustEmail(boolean trustEmail) {
this.trustEmail = trustEmail;
}
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
index 8d6d5fd665..c4b9735030 100644
--- a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
@@ -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;
/**
- * Denotes an executable Script with metadata.
+ * A representation of a Script with some additional meta-data.
*
* @author Thomas Darimont
*/
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.
*/
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index 6774b6f430..c291c42bb6 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -18,6 +18,8 @@
package org.keycloak.models.utils;
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.CertificateUtils;
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();
+ }
+ }
+
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 36b1d2b762..193bbc2d58 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -626,6 +626,7 @@ public class ModelToRepresentation {
providerRep.setInternalId(identityProviderModel.getInternalId());
providerRep.setProviderId(identityProviderModel.getProviderId());
providerRep.setAlias(identityProviderModel.getAlias());
+ providerRep.setDisplayName(identityProviderModel.getDisplayName());
providerRep.setEnabled(identityProviderModel.isEnabled());
providerRep.setStoreToken(identityProviderModel.isStoreToken());
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 5e60d43d66..54acd0e7f6 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1523,6 +1523,7 @@ public class RepresentationToModel {
identityProviderModel.setInternalId(representation.getInternalId());
identityProviderModel.setAlias(representation.getAlias());
+ identityProviderModel.setDisplayName(representation.getDisplayName());
identityProviderModel.setProviderId(representation.getProviderId());
identityProviderModel.setEnabled(representation.isEnabled());
identityProviderModel.setTrustEmail(representation.isTrustEmail());
diff --git a/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java b/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java
deleted file mode 100644
index 342652fa17..0000000000
--- a/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java
+++ /dev/null
@@ -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 Thomas Darimont
- */
-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 getInterface(Class clazz) {
- return getInvocableEngine().getInterface(clazz);
- }
-
- @Override
- public T getInterface(Object thiz, Class 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;
- }
-}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java b/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
new file mode 100644
index 0000000000..c3859aba35
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
@@ -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 Thomas Darimont
+ */
+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 getInterface(Class clazz) {
+ return getInvocableEngine().getInterface(clazz);
+ }
+
+ @Override
+ public T getInterface(Object thiz, Class 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;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/Script.java b/server-spi/src/main/java/org/keycloak/scripting/Script.java
index 2e81372655..ef86902e24 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/Script.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/Script.java
@@ -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;
import org.keycloak.models.ScriptModel;
/**
+ * A {@link ScriptModel} which holds some meta-data.
+ *
* @author Thomas Darimont
*/
public class Script implements ScriptModel {
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
index 9d55195977..9613eb64e4 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
@@ -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;
import javax.script.Bindings;
/**
* Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}.
- *
+ *
Used by {@link ScriptingProvider}
* @author Thomas Darimont
*/
@FunctionalInterface
public interface ScriptBindingsConfigurer {
/**
- * A default {@link ScriptBindingsConfigurer} leaves the Bindings empty.
+ * A default {@link ScriptBindingsConfigurer} that provides no Bindings.
*/
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
index e912ca915e..2063bd21b9 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
@@ -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;
import org.keycloak.models.ScriptModel;
@@ -11,7 +27,7 @@ import javax.script.ScriptException;
*/
public class ScriptExecutionException extends RuntimeException {
- public ScriptExecutionException(ScriptModel script, ScriptException se) {
- super("Error executing script '" + script.getName() + "'", se);
+ public ScriptExecutionException(ScriptModel script, Exception ex) {
+ super("Could not execute script '" + script.getName() + "' problem was: " + ex.getMessage(), ex);
}
}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 163120bc7c..67bad5a9c9 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -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;
import org.keycloak.models.ScriptModel;
@@ -13,20 +29,23 @@ import javax.script.ScriptEngine;
public interface ScriptingProvider extends Provider {
/**
- * Returns an {@link InvocableScript} based on the given {@link ScriptModel}.
- *
The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}
+ * Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
+ *
The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}
*
- * @param script the script to wrap
+ * @param scriptModel the scriptModel to wrap
* @param bindingsConfigurer populates the {@link javax.script.Bindings}
* @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}.
- * @see #prepareScript(ScriptModel, ScriptBindingsConfigurer)
- * @param script
+ * Creates a new {@link ScriptModel} instance.
+ *
+ * @param realmId
+ * @param scriptName
+ * @param scriptCode
+ * @param scriptDescription
* @return
*/
- InvocableScript prepareScript(ScriptModel script);
+ ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription);
}
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 889c7a48cf..5b1f52966b 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -65,3 +65,4 @@ org.keycloak.policy.PasswordPolicyManagerSpi
org.keycloak.transaction.TransactionManagerLookupSpi
org.keycloak.credential.hash.PasswordHashSpi
org.keycloak.credential.CredentialSpi
+org.keycloak.keys.KeyStorageSpi
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
index 895f0356f0..85a217ff1e 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
@@ -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;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.ScriptModel;
import org.keycloak.models.UserModel;
-import org.keycloak.scripting.InvocableScript;
-import org.keycloak.scripting.Script;
-import org.keycloak.scripting.ScriptBindingsConfigurer;
+import org.keycloak.scripting.InvocableScriptAdapter;
+import org.keycloak.scripting.ScriptExecutionException;
import org.keycloak.scripting.ScriptingProvider;
-import javax.script.Bindings;
-import javax.script.ScriptException;
import java.util.Map;
/**
* An {@link Authenticator} that can execute a configured script during authentication flow.
- *
scripts must provide
+ *
+ * Scripts must at least provide one of the following functions:
+ *
+ *
{@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}
+ *
{@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}
+ *
+ *
+ *
+ * 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.
+ *
+ *
{@code script} the {@link ScriptModel} to access script metadata
+ *
{@code realm} the {@link RealmModel}
+ *
{@code user} the current {@link UserModel}
+ *
{@code session} the active {@link KeycloakSession}
+ *
{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}
+ *
{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
+ *
+ *
+ *
+ * Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
+ * or {@code action(context)} function.
+ *
+ * An example {@link ScriptBasedAuthenticator} definition could look as follows:
+ *