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 getProviderClass() { + return KeyStorageProvider.class; + } + + @Override + public Class 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: + *

    + *
  1. {@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}
  2. + *
  3. {@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}
  4. + *
+ *

+ *

+ * 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. + *

    + *
  1. {@code script} the {@link ScriptModel} to access script metadata
  2. + *
  3. {@code realm} the {@link RealmModel}
  4. + *
  5. {@code user} the current {@link UserModel}
  6. + *
  7. {@code session} the active {@link KeycloakSession}
  8. + *
  9. {@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}
  10. + *
  11. {@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: + *

+ * {@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();
+ *   }
+ * }
+ * 
* * @author Thomas Darimont */ @@ -29,58 +88,51 @@ public class ScriptBasedAuthenticator implements Authenticator { static final String SCRIPT_NAME = "scriptName"; static final String SCRIPT_DESCRIPTION = "scriptDescription"; - static final String ACTION = "action"; - static final String AUTHENTICATE = "authenticate"; - static final String TEXT_JAVASCRIPT = "text/javascript"; + static final String ACTION_FUNCTION_NAME = "action"; + static final String AUTHENTICATE_FUNCTION_NAME = "authenticate"; @Override public void authenticate(AuthenticationFlowContext context) { - tryInvoke(AUTHENTICATE, context); + tryInvoke(AUTHENTICATE_FUNCTION_NAME, context); } @Override public void action(AuthenticationFlowContext context) { - tryInvoke(ACTION, context); + tryInvoke(ACTION_FUNCTION_NAME, 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; } try { - //should context be wrapped in a readonly wrapper? - script.invokeFunction(functionName, context); - } catch (ScriptException | NoSuchMethodException e) { + //should context be wrapped in a read-only wrapper? + invocableScriptAdapter.invokeFunction(functionName, context); + } catch (ScriptExecutionException e) { LOGGER.error(e); + context.failure(AuthenticationFlowError.INTERNAL_ERROR); } } - private InvocableScript getInvocableScript(final AuthenticationFlowContext context) { - - final Script script = createAdhocScriptFromContext(context); - - ScriptBindingsConfigurer bindingsConfigurer = new ScriptBindingsConfigurer() { - - @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 boolean hasAuthenticatorConfig(AuthenticationFlowContext context) { + return context != null + && context.getAuthenticatorConfig() != null + && context.getAuthenticatorConfig().getConfig() != null + && !context.getAuthenticatorConfig().getConfig().isEmpty(); } - private Script createAdhocScriptFromContext(AuthenticationFlowContext context) { + private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) { Map config = context.getAuthenticatorConfig().getConfig(); @@ -90,21 +142,35 @@ public class ScriptBasedAuthenticator implements Authenticator { 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 public boolean requiresUser() { - return false; + return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return false; + return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + //TODO make RequiredActions configurable in the script //NOOP } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java index b25af8f1fd..0528154522 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java @@ -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; +import org.apache.commons.io.IOUtils; +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; @@ -8,6 +26,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; +import java.io.IOException; import java.util.List; import static java.util.Arrays.asList; @@ -24,7 +43,9 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE; */ 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 = { AuthenticationExecutionModel.Requirement.REQUIRED, @@ -77,7 +98,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory { @Override public boolean isUserSetupAllowed() { - return false; + return true; } @Override @@ -87,12 +108,12 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory { @Override public String getDisplayType() { - return "Script-based Authentication"; + return "Script"; } @Override public String getHelpText() { - return "Script based authentication."; + return "Script based authentication. Allows to define custom authentication logic via JavaScript."; } @Override @@ -114,9 +135,16 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory { script.setType(SCRIPT_TYPE); script.setName(SCRIPT_CODE); 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." + - "This authenticator exposes the following additional variables: 'script', 'LOG'"); + + String scriptTemplate = "//enter your script code here"; + 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); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java index ad9b0efec1..aec70dc917 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java @@ -23,6 +23,7 @@ import org.keycloak.authentication.ClientAuthenticationFlowContext; import org.keycloak.common.util.Time; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.keys.loader.KeyStorageManager; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ModelException; @@ -120,7 +121,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { } // Get client key and validate signature - PublicKey clientPublicKey = getSignatureValidationKey(client, context); + PublicKey clientPublicKey = getSignatureValidationKey(client, context, jws); if (clientPublicKey == null) { // Error response already set to context return; @@ -161,13 +162,14 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator { } } - protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) { - try { - return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX); - } catch (ModelException me) { - Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage()); + protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context, JWSInput jws) { + PublicKey publicKey = KeyStorageManager.getClientPublicKey(context.getSession(), client, jws); + if (publicKey == null) { + Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unable to load public key"); context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse); return null; + } else { + return publicKey; } } diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 322c33225e..c301371f87 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -73,8 +73,8 @@ public abstract class AbstractOAuth2IdentityProvider { protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class); - public SAMLIdentityProvider(SAMLIdentityProviderConfig config) { - super(config); + public SAMLIdentityProvider(KeycloakSession session, SAMLIdentityProviderConfig config) { + super(session, config); } @Override diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java index 261799524e..714c47eebb 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -50,8 +50,8 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public SAMLIdentityProvider create(IdentityProviderModel model) { - return new SAMLIdentityProvider(new SAMLIdentityProviderConfig(model)); + public SAMLIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new SAMLIdentityProvider(session, new SAMLIdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java index c2544e77d4..96f4257558 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java @@ -22,8 +22,9 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; 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.Urls; import javax.ws.rs.core.UriBuilder; import java.net.URI; @@ -69,7 +70,8 @@ public class AccountFederatedIdentityBean { .queryParam("stateChecker", stateChecker) .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); orderedSet.add(entry); } @@ -105,10 +107,12 @@ public class AccountFederatedIdentityBean { private final String providerName; private final String actionUrl; 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.displayName = displayName; this.providerId = providerId; this.providerName = providerName; this.actionUrl = actionUrl; @@ -142,6 +146,11 @@ public class AccountFederatedIdentityBean { public String getGuiOrder() { return guiOrder; } + + public String getDisplayName() { + return displayName; + } + } public static class IdentityProviderComparator implements Comparator { diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index 0f48ae4436..9f60404077 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { List identityProviders = realm.getIdentityProviders(); 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)); @@ -398,7 +398,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { List identityProviders = realm.getIdentityProviders(); 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("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); @@ -425,7 +425,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } } - @Override public Response createLogin() { return createResponse(LoginFormsPages.LOGIN); diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java index 830166039c..87b935a95a 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java @@ -17,7 +17,9 @@ package org.keycloak.forms.login.freemarker.model; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.Urls; import javax.ws.rs.core.UriInfo; @@ -35,12 +37,13 @@ import java.util.TreeSet; public class IdentityProviderBean { private boolean displaySocial; - private List providers; private RealmModel realm; + private final KeycloakSession session; - public IdentityProviderBean(RealmModel realm, List identityProviders, URI baseURI, UriInfo uriInfo) { + public IdentityProviderBean(RealmModel realm, KeycloakSession session, List identityProviders, URI baseURI, UriInfo uriInfo) { this.realm = realm; + this.session = session; if (!identityProviders.isEmpty()) { Set orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE); @@ -59,7 +62,10 @@ public class IdentityProviderBean { private void addIdentityProvider(Set orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) { 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)); } @@ -77,9 +83,11 @@ public class IdentityProviderBean { private final String providerId; // This refer to providerType (facebook, google, etc.) private final String loginUrl; 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.displayName = displayName; this.providerId = providerId; this.loginUrl = loginUrl; this.guiOrder = guiOrder; @@ -100,6 +108,10 @@ public class IdentityProviderBean { public String getGuiOrder() { return guiOrder; } + + public String getDisplayName() { + return displayName; + } } public static class IdentityProviderComparator implements Comparator { @@ -112,7 +124,7 @@ public class IdentityProviderBean { @Override public int compare(IdentityProvider o1, IdentityProvider o2) { - + int o1order = parseOrder(o1); int o2order = parseOrder(o2); @@ -120,7 +132,7 @@ public class IdentityProviderBean { return 1; else if (o1order < o2order) return -1; - + return 1; } @@ -134,6 +146,5 @@ public class IdentityProviderBean { } return 10000; } - } } diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java new file mode 100644 index 0000000000..39524ce388 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -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 Marek Posolda + */ +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 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); + } + } + + +} diff --git a/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java b/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java new file mode 100644 index 0000000000..219afc4864 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java @@ -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 Marek Posolda + */ +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(); + } +} diff --git a/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java new file mode 100644 index 0000000000..79032ff44a --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java @@ -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 Marek Posolda + */ +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 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; + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java index 756c12a4ea..588a06c76d 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java @@ -32,6 +32,10 @@ public class OIDCAdvancedConfigWrapper { 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 ClientRepresentation clientRep; @@ -74,6 +78,23 @@ public class OIDCAdvancedConfigWrapper { 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) { if (clientModel != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 39c6d10313..e8b4e11ffd 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -81,7 +81,6 @@ import java.util.Set; */ public class TokenManager { protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; - private static final String JWT = "JWT"; // Harcoded for now Algorithm jwsAlgorithm = Algorithm.RS256; @@ -621,7 +620,7 @@ public class TokenManager { public String encodeToken(RealmModel realm, Object token) { String encodedToken = new JWSBuilder() - .type(JWT) + .type(OAuth2Constants.JWT) .kid(realm.getKeyId()) .jsonContent(token) .sign(jwsAlgorithm, realm.getPrivateKey()); @@ -747,7 +746,7 @@ public class TokenManager { AccessTokenResponse res = new AccessTokenResponse(); 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.setTokenType("bearer"); res.setSessionState(accessToken.getSessionState()); @@ -765,11 +764,11 @@ public class TokenManager { } 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); } 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); if (refreshToken.getExpiration() != 0) { res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java index faab68db0e..4469c0f0db 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java @@ -52,12 +52,12 @@ public class AuthorizationEndpointRequestParserProcessor { } if (requestParam != null) { - new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request); + new AuthzEndpointRequestObjectParser(session, requestParam, client).parseRequest(request); } else if (requestUriParam != null) { InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam); String retrievedRequest = StreamUtil.readString(is); - new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request); + new AuthzEndpointRequestObjectParser(session, retrievedRequest, client).parseRequest(request); } return request; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java index 616e1dbd1d..62d711f8cf 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -22,7 +22,10 @@ import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSInput; 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.KeycloakSession; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.util.JsonSerialization; @@ -41,7 +44,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { private final Map requestParams; - public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception { + public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception { JWSInput input = new JWSInput(requestObject); JWSHeader header = input.getHeader(); @@ -54,7 +57,11 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { if (header.getAlgorithm() == Algorithm.none) { this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class); } 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); if (!verified) { throw new RuntimeException("Failed to verify signature on 'request' object"); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSHttpUtils.java similarity index 75% rename from services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java rename to services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSHttpUtils.java index dc6896ec3d..8d1e4d641c 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSHttpUtils.java @@ -30,27 +30,14 @@ import java.io.InputStream; import java.security.PublicKey; /** - * TODO: Merge with JWKSUtils from keycloak-core? * * @author Marek Posolda */ -public class JWKSUtils { +public class JWKSHttpUtils { public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException { InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI); String keySetString = StreamUtil.readString(is); 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; - } } diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java index 140ec04fbd..5772f53055 100644 --- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java +++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.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; @@ -6,7 +22,6 @@ import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; -import javax.script.ScriptException; /** * 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; public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) { + + if (scriptEngineManager == null) { + throw new IllegalStateException("scriptEngineManager must not be null!"); + } + 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 - public InvocableScript prepareScript(ScriptModel script) { - return prepareScript(script, ScriptBindingsConfigurer.EMPTY); - } + public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) { - @Override - public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) { - - if (script == null) { - throw new NullPointerException("script must not be null"); + if (scriptModel == null) { + throw new IllegalArgumentException("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"); } 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); } - configureBindings(bindingsConfigurer, engine); + configureBindings(bindingsConfigurer, scriptEngine); - loadScriptIntoEngine(script, engine); - - return new InvocableScript(script, engine); + return scriptEngine; } private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) { @@ -61,15 +106,9 @@ public class DefaultScriptingProvider implements ScriptingProvider { engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE); } - private void loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) { - - try { - engine.eval(script.getCode()); - } catch (ScriptException se) { - throw new ScriptExecutionException(script, se); - } - } - + /** + * Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}. + */ private ScriptEngine lookupScriptEngineFor(ScriptModel script) { return scriptEngineManager.getEngineByMimeType(script.getMimeType()); } diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java index 2e8a431aa0..b00a058eee 100644 --- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java +++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.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.Config; @@ -13,11 +29,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory static final String ID = "script-based-auth"; - private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); - @Override public ScriptingProvider create(KeycloakSession session) { - return new DefaultScriptingProvider(scriptEngineManager); + return new DefaultScriptingProvider(ScriptEngineManagerHolder.SCRIPT_ENGINE_MANAGER); } @Override @@ -39,4 +53,12 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory public String getId() { return ID; } + + /** + * Holder class for lazy initialization of {@link ScriptEngineManager}. + */ + private static class ScriptEngineManagerHolder { + + private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager(); + } } diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index 2024a6fe71..59f36b25bd 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -24,6 +24,7 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKParser; import org.keycloak.jose.jws.Algorithm; import org.keycloak.models.KeycloakSession; 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.mappers.PairwiseSubMapperHelper; 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.PairwiseSubMapperUtils; 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.services.clientregistration.ClientRegistrationException; import org.keycloak.services.util.CertificateInfoHelper; +import org.keycloak.util.JWKSUtils; import java.io.IOException; import java.net.URI; @@ -94,19 +96,11 @@ public class DescriptionConverter { } client.setClientAuthenticatorType(clientAuthFactory.getId()); - PublicKey publicKey = retrievePublicKey(session, clientOIDC); - if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) { + boolean publicKeySet = setPublicKey(clientOIDC, client); + 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()); } - 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); if (clientOIDC.getUserinfoSignedResponseAlg() != null) { 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) { - return null; + return false; } if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) { throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks"); } - JSONWebKeySet keySet; - if (clientOIDC.getJwks() != null) { - keySet = clientOIDC.getJwks(); - } else { - try { - keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri()); - } catch (IOException ioe) { - throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe); - } - } + OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep); - 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) { response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString()); } + if (config.isUseJwksUrl()) { + response.setJwksUri(config.getJwksUrl()); + } List foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client); SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE; diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index fabbae0167..9f7e4bf8c3 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -805,7 +805,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal 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."); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index 77afc30fd3..aa1f08959b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -28,15 +28,17 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKParser; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; 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.idm.CertificateRepresentation; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.util.CertificateInfoHelper; +import org.keycloak.util.JWKSUtils; import org.keycloak.util.JsonSerialization; import javax.ws.rs.Consumes; @@ -148,16 +150,15 @@ public class ClientAttributeCertificateResource { throw new NotFoundException("Could not find client"); } - CertificateRepresentation info = getCertFromRequest(uriInfo, input); - try { + CertificateRepresentation info = getCertFromRequest(input); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); + + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return info; } catch (IllegalStateException ise) { 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"); } - CertificateRepresentation info = getCertFromRequest(uriInfo, input); - info.setPrivateKey(null); - try { + CertificateRepresentation info = getCertFromRequest(input); + info.setPrivateKey(null); CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix); + + adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); + return info; } catch (IllegalStateException ise) { 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(); CertificateRepresentation info = new CertificateRepresentation(); Map> uploadForm = input.getFormDataMap(); @@ -217,10 +217,16 @@ public class ClientAttributeCertificateResource { } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { InputStream stream = inputParts.get(0).getBody(InputStream.class, null); JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class); - PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); - String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); - info.setPublicKey(publicKeyPem); - return info; + JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG); + if (publicKeyJwk == null) { + throw new IllegalStateException("Certificate not found for use sig"); + } else { + PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey(); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); + info.setPublicKey(publicKeyPem); + info.setKid(publicKeyJwk.getKeyId()); + return info; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index af456c6b33..4c40991579 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -228,7 +228,7 @@ public class IdentityProviderResource { try { IdentityProviderFactory factory = getIdentityProviderFactory(); - return factory.create(identityProviderModel).export(uriInfo, realm, format); + return factory.create(session, identityProviderModel).export(uriInfo, realm, format); } catch (Exception e) { return ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND); } diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java index 313ce1d4c8..005ce88b53 100644 --- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java @@ -37,6 +37,8 @@ public class CertificateInfoHelper { public static final String X509CERTIFICATE = "certificate"; public static final String PUBLIC_KEY = "public.key"; + public static final String KID = "kid"; + // CLIENT MODEL METHODS @@ -44,11 +46,13 @@ public class CertificateInfoHelper { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; + String kidAttribute = attributePrefix + "." + KID; CertificateRepresentation rep = new CertificateRepresentation(); rep.setCertificate(client.getAttribute(certificateAttribute)); rep.setPublicKey(client.getAttribute(publicKeyAttribute)); rep.setPrivateKey(client.getAttribute(privateKeyAttribute)); + rep.setKid(client.getAttribute(kidAttribute)); return rep; } @@ -58,6 +62,7 @@ public class CertificateInfoHelper { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; + String kidAttribute = attributePrefix + "." + KID; if (rep.getPublicKey() == null && rep.getCertificate() == null) { throw new IllegalStateException("Both certificate and publicKey are null!"); @@ -70,6 +75,7 @@ public class CertificateInfoHelper { setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); + setOrRemoveAttr(client, kidAttribute, rep.getKid()); } 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 public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) { String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY; String certificateAttribute = attributePrefix + "." + X509CERTIFICATE; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; + String kidAttribute = attributePrefix + "." + KID; if (rep.getPublicKey() == null && rep.getCertificate() == null) { throw new IllegalStateException("Both certificate and publicKey are null!"); @@ -123,6 +106,7 @@ public class CertificateInfoHelper { setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); + setOrRemoveAttr(client, kidAttribute, rep.getKid()); } private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) { diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index 060011414c..f76057ddd2 100755 --- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; /** * @author Stian Thorgersen @@ -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 DEFAULT_SCOPE = "email"; - public FacebookIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); + public FacebookIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java index 70bcd91002..26149a1ef6 100755 --- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java @@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; /** * @author Pedro Igor @@ -34,8 +35,8 @@ public class FacebookIdentityProviderFactory extends AbstractIdentityProviderFac } @Override - public FacebookIdentityProvider create(IdentityProviderModel model) { - return new FacebookIdentityProvider(new OAuth2IdentityProviderConfig(model)); + public FacebookIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new FacebookIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index c10af75cb7..cf2a53e89c 100755 --- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; /** * @author Stian Thorgersen @@ -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 DEFAULT_SCOPE = "user:email"; - public GitHubIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); + public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java index e305959c90..e1c05b85d1 100755 --- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java @@ -18,8 +18,9 @@ package org.keycloak.social.github; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.KeycloakSession; /** * @author Pedro Igor @@ -34,8 +35,8 @@ public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFacto } @Override - public GitHubIdentityProvider create(IdentityProviderModel model) { - return new GitHubIdentityProvider(new OAuth2IdentityProviderConfig(model)); + public GitHubIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new GitHubIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java index 81b349e1e2..060f51a5e6 100755 --- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java @@ -19,6 +19,7 @@ package org.keycloak.social.google; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; /** * @author Stian Thorgersen @@ -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 DEFAULT_SCOPE = "openid profile email"; - public GoogleIdentityProvider(OIDCIdentityProviderConfig config) { - super(config); + public GoogleIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java index 0dfbfe3583..6db6219c1d 100755 --- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java @@ -18,8 +18,9 @@ package org.keycloak.social.google; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.KeycloakSession; /** * @author Pedro Igor @@ -34,8 +35,8 @@ public class GoogleIdentityProviderFactory extends AbstractIdentityProviderFacto } @Override - public GoogleIdentityProvider create(IdentityProviderModel model) { - return new GoogleIdentityProvider(new OIDCIdentityProviderConfig(model)); + public GoogleIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new GoogleIdentityProvider(session, new OIDCIdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java index 3327858486..3c424f6960 100755 --- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java @@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; import java.net.MalformedURLException; 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 DEFAULT_SCOPE = "r_basicprofile r_emailaddress"; - public LinkedInIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); + public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java index 7dafa33e30..10b94b45d9 100755 --- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java @@ -18,8 +18,9 @@ package org.keycloak.social.linkedin; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.KeycloakSession; /** * @author Vlastimil Elias (velias at redhat dot com) @@ -35,8 +36,8 @@ public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFac } @Override - public LinkedInIdentityProvider create(IdentityProviderModel model) { - return new LinkedInIdentityProvider(new OAuth2IdentityProviderConfig(model)); + public LinkedInIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new LinkedInIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java index 7702738846..a93514146b 100755 --- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java @@ -28,6 +28,9 @@ import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.models.KeycloakSession; + 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 DEFAULT_SCOPE = "wl.basic,wl.emails"; - public MicrosoftIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); + public MicrosoftIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java index 15cd91dfa9..eee3e87042 100644 --- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java @@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; /** * @author Vlastimil Elias (velias at redhat dot com) @@ -34,8 +35,8 @@ public class MicrosoftIdentityProviderFactory extends AbstractIdentityProviderFa } @Override - public MicrosoftIdentityProvider create(IdentityProviderModel model) { - return new MicrosoftIdentityProvider(new OAuth2IdentityProviderConfig(model)); + public MicrosoftIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new MicrosoftIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java index 130a8f3fd3..4f9e4b2a79 100755 --- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java @@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.models.KeycloakSession; import java.io.StringWriter; 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 DEFAULT_SCOPE = ""; - public StackoverflowIdentityProvider(StackOverflowIdentityProviderConfig config) { - super(config); + public StackoverflowIdentityProvider(KeycloakSession session, StackOverflowIdentityProviderConfig config) { + super(session, config); config.setAuthorizationUrl(AUTH_URL); config.setTokenUrl(TOKEN_URL); config.setUserInfoUrl(PROFILE_URL); diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java index a0f6cf5ef0..4c8deb9d2a 100755 --- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java @@ -17,8 +17,9 @@ package org.keycloak.social.stackoverflow; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.KeycloakSession; /** * @author Vlastimil Elias (velias at redhat dot com) @@ -35,8 +36,8 @@ public class StackoverflowIdentityProviderFactory extends } @Override - public StackoverflowIdentityProvider create(IdentityProviderModel model) { - return new StackoverflowIdentityProvider(new StackOverflowIdentityProviderConfig(model)); + public StackoverflowIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new StackoverflowIdentityProvider(session, new StackOverflowIdentityProviderConfig(model)); } @Override diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 630f832a05..d0178d680b 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -57,8 +57,8 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider { protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class); - public TwitterIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); + public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { + super(session, config); } @Override diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java index 1e0504d79d..74e1a3d6d0 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java @@ -18,8 +18,9 @@ package org.keycloak.social.twitter; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.AbstractIdentityProviderFactory; -import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.broker.social.SocialIdentityProviderFactory; +import org.keycloak.models.KeycloakSession; /** * @author Pedro Igor @@ -34,8 +35,8 @@ public class TwitterIdentityProviderFactory extends AbstractIdentityProviderFact } @Override - public TwitterIdentityProvider create(IdentityProviderModel model) { - return new TwitterIdentityProvider(new OAuth2IdentityProviderConfig(model)); + public TwitterIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { + return new TwitterIdentityProvider(session, new OAuth2IdentityProviderConfig(model)); } @Override diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js new file mode 100644 index 0000000000..73bb12475f --- /dev/null +++ b/services/src/main/resources/scripts/authenticator-template.js @@ -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(); +} \ No newline at end of file diff --git a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java index 4b97d2b515..e706f54219 100755 --- a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java +++ b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java @@ -24,6 +24,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; import java.io.IOException; import java.util.HashMap; @@ -129,7 +130,7 @@ public class AbstractOAuth2IdentityProviderTest { private static class TestProvider extends AbstractOAuth2IdentityProvider { public TestProvider(OAuth2IdentityProviderConfig config) { - super(config); + super(null, config); } @Override diff --git a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java index ff4c3686a8..0598ef635c 100755 --- a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java +++ b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java @@ -32,32 +32,32 @@ public class IdentityProviderBeanTest { @Test public void testIdentityProviderComparator() { - IdentityProvider o1 = new IdentityProvider("alias1", "id1", "ur1", null); - IdentityProvider o2 = new IdentityProvider("alias2", "id2", "ur2", null); + IdentityProvider o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", null); + IdentityProvider o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", null); // guiOrder not defined at any object - first is always lower Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); // 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(o2, o1)); // 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(o2, o1)); // guiOrder is defined for both but is same - first is always lower - o1 = new IdentityProvider("alias1", "id1", "ur1", "0"); - o2 = new IdentityProvider("alias2", "id2", "ur2", "0"); + o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0"); + o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "0"); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); // guiOrder is reflected - o1 = new IdentityProvider("alias1", "id1", "ur1", "0"); - o2 = new IdentityProvider("alias2", "id2", "ur2", "1"); + o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0"); + o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "1"); Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2)); Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1)); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java index 8b5df6ceb4..6daa6b1a44 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java @@ -120,7 +120,8 @@ public class TestingOIDCEndpointsApplicationResource { } 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 { throw new BadRequestException("Unknown argument: " + jwaAlgorithm); } diff --git a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json b/testsuite/integration-arquillian/test-apps/js-console/example-realm.json index 4e3adca45e..e90217a5cf 100755 --- a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json +++ b/testsuite/integration-arquillian/test-apps/js-console/example-realm.json @@ -19,6 +19,7 @@ ], "realmRoles": [ "user" ], "clientRoles": { + "realm-management" : [ "view-realm" ], "account": ["view-profile", "manage-account"] } },{ diff --git a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html index 468f98e6e6..1c2b61d95d 100755 --- a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html +++ b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html @@ -29,6 +29,7 @@ + @@ -41,6 +42,13 @@ + + + + + + +