merge conflicts
This commit is contained in:
commit
d4c3fae546
170 changed files with 3439 additions and 690 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -45,3 +45,6 @@ catalog.xml
|
|||
#########
|
||||
target
|
||||
|
||||
# Maven shade
|
||||
#############
|
||||
*dependency-reduced-pom.xml
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<String, PublicKey> 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();
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ public interface OAuth2Constants {
|
|||
|
||||
String MAX_AGE = "max_age";
|
||||
|
||||
String JWT = "JWT";
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,10 @@
|
|||
<local-cache name="loginFailures"/>
|
||||
<local-cache name="authorization"/>
|
||||
<local-cache name="work"/>
|
||||
<local-cache name="keys">
|
||||
<eviction max-entries="1000" strategy="LRU"/>
|
||||
<expiration max-idle="3600000" />
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
</xsl:copy>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,18 +191,18 @@ 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)
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
<K, V> Cache<K, V> getCache(String name);
|
||||
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.infinispan;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.keys.KeyLoader;
|
||||
import org.keycloak.keys.KeyStorageProvider;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanKeyStorageProvider implements KeyStorageProvider {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanKeyStorageProvider.class);
|
||||
|
||||
private final Cache<String, PublicKeysEntry> keys;
|
||||
|
||||
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress;
|
||||
|
||||
private final int minTimeBetweenRequests ;
|
||||
|
||||
public InfinispanKeyStorageProvider(Cache<String, PublicKeysEntry> keys, Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
|
||||
this.keys = keys;
|
||||
this.tasksInProgress = tasksInProgress;
|
||||
this.minTimeBetweenRequests = minTimeBetweenRequests;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader) {
|
||||
// Check if key is in cache
|
||||
PublicKeysEntry entry = keys.get(modelKey);
|
||||
if (entry != null) {
|
||||
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
|
||||
if (publicKey != null) {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
// Check if we are allowed to send request
|
||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||
|
||||
WrapperCallable wrapperCallable = new WrapperCallable(modelKey, loader);
|
||||
FutureTask<PublicKeysEntry> task = new FutureTask<>(wrapperCallable);
|
||||
FutureTask<PublicKeysEntry> existing = tasksInProgress.putIfAbsent(modelKey, task);
|
||||
|
||||
if (existing == null) {
|
||||
task.run();
|
||||
} else {
|
||||
task = existing;
|
||||
}
|
||||
|
||||
try {
|
||||
entry = task.get();
|
||||
|
||||
// Computation finished. Let's see if key is available
|
||||
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
|
||||
if (publicKey != null) {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
} catch (ExecutionException ee) {
|
||||
throw new RuntimeException("Error when loading public keys", ee);
|
||||
} catch (InterruptedException ie) {
|
||||
throw new RuntimeException("Error. Interrupted when loading public keys", ie);
|
||||
} finally {
|
||||
// Our thread inserted the task. Let's clean
|
||||
if (existing == null) {
|
||||
tasksInProgress.remove(modelKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warnf("Won't load the keys for model '%s' . Last request time was %d", modelKey, lastRequestTime);
|
||||
}
|
||||
|
||||
Set<String> availableKids = entry==null ? Collections.emptySet() : entry.getCurrentKeys().keySet();
|
||||
log.warnf("PublicKey wasn't found in the storage. Requested kid: '%s' . Available kids: '%s'", kid, availableKids);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private PublicKey getPublicKey(Map<String, PublicKey> publicKeys, String kid) {
|
||||
// Backwards compatibility
|
||||
if (kid == null && !publicKeys.isEmpty()) {
|
||||
return publicKeys.values().iterator().next();
|
||||
} else {
|
||||
return publicKeys.get(kid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class WrapperCallable implements Callable<PublicKeysEntry> {
|
||||
|
||||
private final String modelKey;
|
||||
private final KeyLoader delegate;
|
||||
|
||||
public WrapperCallable(String modelKey, KeyLoader delegate) {
|
||||
this.modelKey = modelKey;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeysEntry call() throws Exception {
|
||||
PublicKeysEntry entry = keys.get(modelKey);
|
||||
|
||||
int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
// Check again if we are allowed to send request. There is a chance other task was already finished and removed from tasksInProgress in the meantime.
|
||||
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
|
||||
|
||||
Map<String, PublicKey> publicKeys = delegate.loadKeys();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debugf("Public keys retrieved successfully for model %s. New kids: %s", modelKey, publicKeys.keySet().toString());
|
||||
}
|
||||
|
||||
entry = new PublicKeysEntry(currentTime, publicKeys);
|
||||
|
||||
keys.put(modelKey, entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.infinispan;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.keys.KeyStorageProvider;
|
||||
import org.keycloak.keys.KeyStorageProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanKeyStorageProviderFactory implements KeyStorageProviderFactory {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanKeyStorageProviderFactory.class);
|
||||
|
||||
public static final String PROVIDER_ID = "infinispan";
|
||||
|
||||
private Cache<String, PublicKeysEntry> keysCache;
|
||||
|
||||
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
|
||||
|
||||
private int minTimeBetweenRequests;
|
||||
|
||||
@Override
|
||||
public KeyStorageProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new InfinispanKeyStorageProvider(keysCache, tasksInProgress, minTimeBetweenRequests);
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if (keysCache == null) {
|
||||
synchronized (this) {
|
||||
if (keysCache == null) {
|
||||
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
minTimeBetweenRequests = config.getInt("minTimeBetweenRequests", 10);
|
||||
log.debugf("minTimeBetweenRequests is %d", minTimeBetweenRequests);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.infinispan;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class PublicKeysEntry implements Serializable {
|
||||
|
||||
private final int lastRequestTime;
|
||||
|
||||
private final Map<String, PublicKey> currentKeys;
|
||||
|
||||
public PublicKeysEntry(int lastRequestTime, Map<String, PublicKey> currentKeys) {
|
||||
this.lastRequestTime = lastRequestTime;
|
||||
this.currentKeys = currentKeys;
|
||||
}
|
||||
|
||||
public int getLastRequestTime() {
|
||||
return lastRequestTime;
|
||||
}
|
||||
|
||||
public Map<String, PublicKey> getCurrentKeys() {
|
||||
return currentKeys;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.keys.infinispan;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.configuration.cache.Configuration;
|
||||
import org.infinispan.configuration.cache.ConfigurationBuilder;
|
||||
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
|
||||
import org.infinispan.eviction.EvictionStrategy;
|
||||
import org.infinispan.eviction.EvictionType;
|
||||
import org.infinispan.manager.DefaultCacheManager;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.keys.KeyLoader;
|
||||
import org.keycloak.keys.infinispan.InfinispanKeyStorageProvider;
|
||||
import org.keycloak.keys.infinispan.PublicKeysEntry;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanKeyStorageProviderTest {
|
||||
|
||||
private Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
|
||||
|
||||
Cache<String, PublicKeysEntry> keys = getKeysCache();
|
||||
Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
|
||||
int minTimeBetweenRequests = 10;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrency() throws Exception {
|
||||
// Just one thread will execute the task
|
||||
List<Thread> threads = new LinkedList<>();
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
Thread t = new Thread(new SampleWorker("model1"));
|
||||
threads.add(t);
|
||||
}
|
||||
startAndJoinAll(threads);
|
||||
Assert.assertEquals(counters.get("model1").get(), 1);
|
||||
threads.clear();
|
||||
|
||||
// model1 won't be executed due to lastRequestTime. model2 will be executed just with one thread
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
Thread t = new Thread(new SampleWorker("model1"));
|
||||
threads.add(t);
|
||||
}
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
Thread t = new Thread(new SampleWorker("model2"));
|
||||
threads.add(t);
|
||||
}
|
||||
startAndJoinAll(threads);
|
||||
Assert.assertEquals(counters.get("model1").get(), 1);
|
||||
Assert.assertEquals(counters.get("model2").get(), 1);
|
||||
threads.clear();
|
||||
|
||||
// Increase time offset
|
||||
Time.setOffset(20);
|
||||
|
||||
// Time updated. So another thread should successfully run loader for both model1 and model2
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
Thread t = new Thread(new SampleWorker("model1"));
|
||||
threads.add(t);
|
||||
}
|
||||
for (int i=0 ; i<10 ; i++) {
|
||||
Thread t = new Thread(new SampleWorker("model2"));
|
||||
threads.add(t);
|
||||
}
|
||||
startAndJoinAll(threads);
|
||||
Assert.assertEquals(counters.get("model1").get(), 2);
|
||||
Assert.assertEquals(counters.get("model2").get(), 2);
|
||||
threads.clear();
|
||||
}
|
||||
|
||||
|
||||
private void startAndJoinAll(List<Thread> threads) throws Exception {
|
||||
for (Thread t : threads) {
|
||||
t.start();
|
||||
}
|
||||
for (Thread t : threads) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class SampleWorker implements Runnable {
|
||||
|
||||
|
||||
private final String modelKey;
|
||||
|
||||
private SampleWorker(String modelKey) {
|
||||
this.modelKey = modelKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
InfinispanKeyStorageProvider provider = new InfinispanKeyStorageProvider(keys, tasksInProgress, minTimeBetweenRequests);
|
||||
provider.getPublicKey(modelKey, "kid1", new SampleLoader(modelKey));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class SampleLoader implements KeyLoader {
|
||||
|
||||
private final String modelKey;
|
||||
|
||||
private SampleLoader(String modelKey) {
|
||||
this.modelKey = modelKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, PublicKey> loadKeys() throws Exception {
|
||||
counters.putIfAbsent(modelKey, new AtomicInteger(0));
|
||||
AtomicInteger currentCounter = counters.get(modelKey);
|
||||
|
||||
currentCounter.incrementAndGet();
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected Cache<String, PublicKeysEntry> getKeysCache() {
|
||||
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
|
||||
gcb.globalJmxStatistics().allowDuplicateDomains(true);
|
||||
|
||||
final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
|
||||
|
||||
ConfigurationBuilder cb = new ConfigurationBuilder();
|
||||
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
|
||||
Configuration cfg = cb.build();
|
||||
|
||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, cfg);
|
||||
return cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
}
|
||||
}
|
|
@ -1270,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
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<String, String> config = entity.getConfig();
|
||||
Map<String, String> copy = new HashMap<>();
|
||||
|
@ -1309,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
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<RealmEntity> {
|
|||
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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -33,8 +33,9 @@
|
|||
|
||||
<dropColumn tableName="USER_ENTITY" columnName="TOTP" />
|
||||
|
||||
|
||||
|
||||
<addColumn tableName="IDENTITY_PROVIDER">
|
||||
<column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
|
||||
|
|
|
@ -912,6 +912,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
|
||||
identityProviderModel.setProviderId(entity.getProviderId());
|
||||
identityProviderModel.setAlias(entity.getAlias());
|
||||
identityProviderModel.setDisplayName(entity.getDisplayName());
|
||||
identityProviderModel.setInternalId(entity.getInternalId());
|
||||
Map<String, String> config = entity.getConfig();
|
||||
Map<String, String> copy = new HashMap<>();
|
||||
|
@ -948,6 +949,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> 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<MongoRealmEntity> 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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -32,9 +32,11 @@ import javax.ws.rs.core.UriInfo;
|
|||
*/
|
||||
public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
|
||||
|
||||
protected final KeycloakSession session;
|
||||
private final C config;
|
||||
|
||||
public AbstractIdentityProvider(C config) {
|
||||
public AbstractIdentityProvider(KeycloakSession session, C config) {
|
||||
this.session = session;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,10 +39,11 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
|
|||
* <p>Creates an {@link IdentityProvider} based on the configuration contained in
|
||||
* <code>model</code>.</p>
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* <p>Creates an {@link IdentityProvider} based on the configuration from
|
||||
|
|
30
server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
Normal file
30
server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface KeyLoader {
|
||||
|
||||
Map<String, PublicKey> loadKeys() throws Exception;
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface KeyStorageProvider extends Provider {
|
||||
|
||||
|
||||
/**
|
||||
* Get public key to verify messages signed by particular client. Used for example during JWT client authentication
|
||||
*
|
||||
* @param modelKey
|
||||
* @param kid
|
||||
* @param loader
|
||||
* @return
|
||||
*/
|
||||
PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader);
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface KeyStorageProviderFactory extends ProviderFactory<KeyStorageProvider> {
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class KeyStorageSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "keyStorage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return KeyStorageProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return KeyStorageProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
|
|||
|
||||
private String postBrokerLoginFlowId;
|
||||
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
||||
* in the map are understood by the identity provider implementation.</p>
|
||||
|
@ -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<String, String>(model.getConfig());
|
||||
this.enabled = model.isEnabled();
|
||||
this.trustEmail = model.isTrustEmail();
|
||||
|
@ -170,4 +173,12 @@ public class IdentityProviderModel implements Serializable {
|
|||
this.trustEmail = trustEmail;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
package org.keycloak.scripting;
|
||||
|
||||
import org.keycloak.models.ScriptModel;
|
||||
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
public class InvocableScript implements Invocable {
|
||||
|
||||
/**
|
||||
* Holds the script metadata as well as the actual script.
|
||||
*/
|
||||
private final ScriptModel script;
|
||||
|
||||
/**
|
||||
* Holds the {@link ScriptEngine} instance initialized with the script code.
|
||||
*/
|
||||
private final ScriptEngine scriptEngine;
|
||||
|
||||
public InvocableScript(ScriptModel script, ScriptEngine scriptEngine) {
|
||||
this.script = script;
|
||||
this.scriptEngine = scriptEngine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
|
||||
return getInvocableEngine().invokeMethod(thiz, name, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
|
||||
return getInvocableEngine().invokeFunction(name, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInterface(Class<T> clazz) {
|
||||
return getInvocableEngine().getInterface(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInterface(Object thiz, Class<T> clazz) {
|
||||
return getInvocableEngine().getInterface(thiz, clazz);
|
||||
}
|
||||
|
||||
private Invocable getInvocableEngine() {
|
||||
return (Invocable) scriptEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@literal true} iif the {@link ScriptEngine} has a function with the given {@code functionName}.
|
||||
* @param functionName
|
||||
* @return
|
||||
*/
|
||||
public boolean hasFunction(String functionName){
|
||||
|
||||
Object candidate = scriptEngine.getContext().getAttribute(functionName);
|
||||
|
||||
return candidate != null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.scripting;
|
||||
|
||||
import org.keycloak.models.ScriptModel;
|
||||
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
/**
|
||||
* Wraps a {@link ScriptModel} and makes it {@link Invocable}.
|
||||
*
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
public class InvocableScriptAdapter implements Invocable {
|
||||
|
||||
/**
|
||||
* Holds the {@ScriptModel}
|
||||
*/
|
||||
private final ScriptModel scriptModel;
|
||||
|
||||
/**
|
||||
* Holds the {@link ScriptEngine} instance initialized with the script code.
|
||||
*/
|
||||
private final ScriptEngine scriptEngine;
|
||||
|
||||
/**
|
||||
* Creates a new {@link InvocableScriptAdapter} instance.
|
||||
*
|
||||
* @param scriptModel must not be {@literal null}
|
||||
* @param scriptEngine must not be {@literal null}
|
||||
*/
|
||||
public InvocableScriptAdapter(ScriptModel scriptModel, ScriptEngine scriptEngine) {
|
||||
|
||||
if (scriptModel == null) {
|
||||
throw new IllegalArgumentException("scriptModel must not be null");
|
||||
}
|
||||
|
||||
if (scriptEngine == null) {
|
||||
throw new IllegalArgumentException("scriptEngine must not be null");
|
||||
}
|
||||
|
||||
this.scriptModel = scriptModel;
|
||||
this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptExecutionException {
|
||||
|
||||
try {
|
||||
return getInvocableEngine().invokeMethod(thiz, name, args);
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
throw new ScriptExecutionException(scriptModel, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invokeFunction(String name, Object... args) throws ScriptExecutionException {
|
||||
try {
|
||||
return getInvocableEngine().invokeFunction(name, args);
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
throw new ScriptExecutionException(scriptModel, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInterface(Class<T> clazz) {
|
||||
return getInvocableEngine().getInterface(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInterface(Object thiz, Class<T> clazz) {
|
||||
return getInvocableEngine().getInterface(thiz, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@literal true} if the {@link ScriptEngine} has a definition with the given {@code name}.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public boolean isDefined(String name) {
|
||||
|
||||
Object candidate = scriptEngine.getContext().getAttribute(name);
|
||||
|
||||
return candidate != null;
|
||||
}
|
||||
|
||||
private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
|
||||
|
||||
try {
|
||||
engine.eval(script.getCode());
|
||||
} catch (ScriptException se) {
|
||||
throw new ScriptExecutionException(script, se);
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
private Invocable getInvocableEngine() {
|
||||
return (Invocable) scriptEngine;
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
public class Script implements ScriptModel {
|
||||
|
|
|
@ -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}.
|
||||
*
|
||||
* <p>Used by {@link ScriptingProvider}</p>
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ScriptBindingsConfigurer {
|
||||
|
||||
/**
|
||||
* A default {@link ScriptBindingsConfigurer} leaves the Bindings empty.
|
||||
* A default {@link ScriptBindingsConfigurer} that provides no Bindings.
|
||||
*/
|
||||
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}.
|
||||
* <p>The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
|
||||
* Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
|
||||
* <p>The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
|
||||
*
|
||||
* @param script the script to wrap
|
||||
* @param scriptModel the scriptModel to wrap
|
||||
* @param bindingsConfigurer populates the {@link javax.script.Bindings}
|
||||
* @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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
* <p>scripts must provide </p>
|
||||
* <p>
|
||||
* Scripts must at least provide one of the following functions:
|
||||
* <ol>
|
||||
* <li>{@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}</li>
|
||||
* <li>{@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}</li>
|
||||
* </ol>
|
||||
* </p>
|
||||
* <p>
|
||||
* Custom {@link Authenticator Authenticator's} should at least provide the {@code authenticate(..)} function.
|
||||
* The following script {@link javax.script.Bindings} are available for convenient use within script code.
|
||||
* <ol>
|
||||
* <li>{@code script} the {@link ScriptModel} to access script metadata</li>
|
||||
* <li>{@code realm} the {@link RealmModel}</li>
|
||||
* <li>{@code user} the current {@link UserModel}</li>
|
||||
* <li>{@code session} the active {@link KeycloakSession}</li>
|
||||
* <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
|
||||
* <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
|
||||
* </ol>
|
||||
* </p>
|
||||
* <p>
|
||||
* Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
|
||||
* or {@code action(context)} function.
|
||||
* <p>
|
||||
* An example {@link ScriptBasedAuthenticator} definition could look as follows:
|
||||
* <pre>
|
||||
* {@code
|
||||
*
|
||||
* AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
|
||||
*
|
||||
* function authenticate(context) {
|
||||
*
|
||||
* LOG.info(script.name + " --> trace auth for: " + user.username);
|
||||
*
|
||||
* if ( user.username === "tester"
|
||||
* && user.getAttribute("someAttribute")
|
||||
* && user.getAttribute("someAttribute").contains("someValue")) {
|
||||
*
|
||||
* context.failure(AuthenticationFlowError.INVALID_USER);
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* context.success();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||
*/
|
||||
|
@ -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<String, String> 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,8 +73,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
|||
public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type";
|
||||
|
||||
|
||||
public AbstractOAuth2IdentityProvider(C config) {
|
||||
super(config);
|
||||
public AbstractOAuth2IdentityProvider(KeycloakSession session, C config) {
|
||||
super(session, config);
|
||||
|
||||
if (config.getDefaultScope() == null || config.getDefaultScope().isEmpty()) {
|
||||
config.setDefaultScope(getDefaultScopes());
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.constants.AdapterConstants;
|
|||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
|
@ -46,8 +47,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
|||
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
|
||||
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -56,8 +57,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
JsonWebToken access = validateToken(idpKey, response.getToken());
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
||||
JsonWebToken access = validateToken(response.getToken());
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
|
||||
|
@ -76,13 +77,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
|||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
PublicKey key = getExternalIdpKey();
|
||||
if (key != null) {
|
||||
if (!verify(token, key)) {
|
||||
|
||||
if (!verify(token)) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
|
||||
LogoutAction action = null;
|
||||
try {
|
||||
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
|
||||
|
|
|
@ -36,8 +36,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
|
|||
}
|
||||
|
||||
@Override
|
||||
public KeycloakOIDCIdentityProvider create(IdentityProviderModel model) {
|
||||
return new KeycloakOIDCIdentityProvider(new OIDCIdentityProviderConfig(model));
|
||||
public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new KeycloakOIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.events.EventType;
|
|||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.keys.loader.KeyStorageManager;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -70,8 +71,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
|
||||
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
||||
|
||||
public OIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
|
||||
String defaultScope = config.getDefaultScope();
|
||||
|
||||
|
@ -85,21 +86,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
return new OIDCEndpoint(callback, realm, event);
|
||||
}
|
||||
|
||||
protected PublicKey getExternalIdpKey() {
|
||||
String signingCert = getConfig().getCertificateSignatureVerifier();
|
||||
try {
|
||||
if (signingCert != null && !signingCert.trim().equals("")) {
|
||||
return PemUtils.decodeCertificate(signingCert).getPublicKey();
|
||||
} else if (getConfig().getPublicKeySignatureVerifier() != null && !getConfig().getPublicKeySignatureVerifier().trim().equals("")) {
|
||||
return PemUtils.decodePublicKey(getConfig().getPublicKeySignatureVerifier());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
protected class OIDCEndpoint extends Endpoint {
|
||||
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
|
@ -232,7 +218,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
return authorizationUrl;
|
||||
}
|
||||
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -244,14 +230,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
} catch (IOException e) {
|
||||
throw new IdentityBrokerException("Could not decode access token response.", e);
|
||||
}
|
||||
PublicKey key = getExternalIdpKey();
|
||||
String accessToken = verifyAccessToken(key, tokenResponse);
|
||||
String accessToken = verifyAccessToken(tokenResponse);
|
||||
|
||||
String encodedIdToken = tokenResponse.getIdToken();
|
||||
|
||||
|
||||
|
||||
JsonWebToken idToken = validateToken(key, encodedIdToken);
|
||||
JsonWebToken idToken = validateToken(encodedIdToken);
|
||||
|
||||
try {
|
||||
String id = idToken.getSubject();
|
||||
|
@ -273,7 +256,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
|
||||
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
|
||||
processAccessTokenResponse(identity, key, tokenResponse);
|
||||
processAccessTokenResponse(identity, tokenResponse);
|
||||
|
||||
identity.setId(id);
|
||||
identity.setName(name);
|
||||
|
@ -304,7 +287,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
}
|
||||
|
||||
private String verifyAccessToken(PublicKey key, AccessTokenResponse tokenResponse) {
|
||||
private String verifyAccessToken(AccessTokenResponse tokenResponse) {
|
||||
String accessToken = tokenResponse.getToken();
|
||||
|
||||
if (accessToken == null) {
|
||||
|
@ -313,14 +296,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
return accessToken;
|
||||
}
|
||||
|
||||
protected boolean verify(JWSInput jws, PublicKey key) {
|
||||
if (key == null) return true;
|
||||
protected boolean verify(JWSInput jws) {
|
||||
if (!getConfig().isValidateSignature()) return true;
|
||||
return RSAProvider.verify(jws, key);
|
||||
|
||||
PublicKey publicKey = KeyStorageManager.getIdentityProviderPublicKey(session, session.getContext().getRealm(), getConfig(), jws);
|
||||
|
||||
return publicKey != null && RSAProvider.verify(jws, publicKey);
|
||||
}
|
||||
|
||||
protected JsonWebToken validateToken(PublicKey key, String encodedToken) {
|
||||
protected JsonWebToken validateToken(String encodedToken) {
|
||||
if (encodedToken == null) {
|
||||
throw new IdentityBrokerException("No token from server.");
|
||||
}
|
||||
|
@ -328,7 +312,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
JsonWebToken token;
|
||||
try {
|
||||
JWSInput jws = new JWSInput(encodedToken);
|
||||
if (!verify(jws, key)) {
|
||||
if (!verify(jws)) {
|
||||
throw new IdentityBrokerException("token signature validation failed");
|
||||
}
|
||||
token = jws.readJsonContent(JsonWebToken.class);
|
||||
|
|
|
@ -17,12 +17,18 @@
|
|||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
|
||||
private static final String JWKS_URL = "jwksUrl";
|
||||
|
||||
private static final String USE_JWKS_URL = "useJwksUrl";
|
||||
|
||||
|
||||
public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
|
||||
super(identityProviderModel);
|
||||
}
|
||||
|
@ -46,13 +52,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
|||
public void setLogoutUrl(String url) {
|
||||
getConfig().put("logoutUrl", url);
|
||||
}
|
||||
public String getCertificateSignatureVerifier() {
|
||||
return getConfig().get("certificateSignatureVerifier");
|
||||
}
|
||||
|
||||
public void setCertificateSignatureVerifier(String signingCertificate) {
|
||||
getConfig().put("certificateSignatureVerifier", signingCertificate);
|
||||
}
|
||||
public String getPublicKeySignatureVerifier() {
|
||||
return getConfig().get("publicKeySignatureVerifier");
|
||||
}
|
||||
|
@ -69,6 +69,22 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
|||
getConfig().put("validateSignature", String.valueOf(validateSignature));
|
||||
}
|
||||
|
||||
public boolean isUseJwksUrl() {
|
||||
return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
|
||||
}
|
||||
|
||||
public void setUseJwksUrl(boolean useJwksUrl) {
|
||||
getConfig().put(USE_JWKS_URL, String.valueOf(useJwksUrl));
|
||||
}
|
||||
|
||||
public String getJwksUrl() {
|
||||
return getConfig().get(JWKS_URL);
|
||||
}
|
||||
|
||||
public void setJwksUrl(String jwksUrl) {
|
||||
getConfig().put(JWKS_URL, jwksUrl);
|
||||
}
|
||||
|
||||
public boolean isBackchannelSupported() {
|
||||
return Boolean.valueOf(getConfig().get("backchannelSupported"));
|
||||
}
|
||||
|
|
|
@ -19,12 +19,14 @@ package org.keycloak.broker.oidc;
|
|||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSUtils;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -47,8 +49,8 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public OIDCIdentityProvider create(IdentityProviderModel model) {
|
||||
return new OIDCIdentityProvider(new OIDCIdentityProviderConfig(model));
|
||||
public OIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new OIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,24 +77,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
config.setTokenUrl(rep.getTokenEndpoint());
|
||||
config.setUserInfoUrl(rep.getUserinfoEndpoint());
|
||||
if (rep.getJwksUri() != null) {
|
||||
sendJwksRequest(session, rep, config);
|
||||
config.setValidateSignature(true);
|
||||
config.setUseJwksUrl(true);
|
||||
config.setJwksUrl(rep.getJwksUri());
|
||||
}
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||
try {
|
||||
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri());
|
||||
PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||
if (key == null) {
|
||||
logger.supportedJwkNotFound(JWK.Use.SIG.asString());
|
||||
} else {
|
||||
config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
|
||||
config.setValidateSignature(true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to query JWKSet from: " + rep.getJwksUri(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -56,8 +56,8 @@ import java.security.PublicKey;
|
|||
*/
|
||||
public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> {
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<FederatedIdentityEntry> {
|
||||
|
|
|
@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
List<IdentityProviderModel> 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<IdentityProviderModel> 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);
|
||||
|
|
|
@ -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<IdentityProvider> providers;
|
||||
private RealmModel realm;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
|
||||
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
|
||||
this.realm = realm;
|
||||
this.session = session;
|
||||
|
||||
if (!identityProviders.isEmpty()) {
|
||||
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
|
||||
|
@ -59,7 +62,10 @@ public class IdentityProviderBean {
|
|||
|
||||
private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
|
||||
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<IdentityProvider> {
|
||||
|
@ -134,6 +146,5 @@ public class IdentityProviderBean {
|
|||
}
|
||||
return 10000;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.loader;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.keys.KeyLoader;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.util.CertificateInfoHelper;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientPublicKeyLoader implements KeyLoader {
|
||||
|
||||
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ClientModel client;
|
||||
|
||||
public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
|
||||
this.session = session;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, PublicKey> loadKeys() throws Exception {
|
||||
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
|
||||
if (config.isUseJwksUrl()) {
|
||||
String jwksUrl = config.getJwksUrl();
|
||||
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
|
||||
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
|
||||
} else {
|
||||
try {
|
||||
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
|
||||
PublicKey publicKey = getSignatureValidationKey(certInfo);
|
||||
|
||||
// Check if we have kid in DB, generate otherwise
|
||||
String kid = certInfo.getKid() != null ? certInfo.getKid() : JWKBuilder.createKeyId(publicKey);
|
||||
return Collections.singletonMap(kid, publicKey);
|
||||
} catch (ModelException me) {
|
||||
logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static PublicKey getSignatureValidationKey(CertificateRepresentation certInfo) throws ModelException {
|
||||
String encodedCertificate = certInfo.getCertificate();
|
||||
String encodedPublicKey = certInfo.getPublicKey();
|
||||
|
||||
if (encodedCertificate == null && encodedPublicKey == null) {
|
||||
throw new ModelException("Client doesn't have certificate or publicKey configured");
|
||||
}
|
||||
|
||||
if (encodedCertificate != null && encodedPublicKey != null) {
|
||||
throw new ModelException("Client has both publicKey and certificate configured");
|
||||
}
|
||||
|
||||
if (encodedCertificate != null) {
|
||||
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
|
||||
return clientCert.getPublicKey();
|
||||
} else {
|
||||
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.loader;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.keys.KeyStorageProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class KeyStorageManager {
|
||||
|
||||
public static PublicKey getClientPublicKey(KeycloakSession session, ClientModel client, JWSInput input) {
|
||||
String kid = input.getHeader().getKeyId();
|
||||
|
||||
KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
|
||||
|
||||
String modelKey = getModelKey(client);
|
||||
ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client);
|
||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
||||
}
|
||||
|
||||
private static String getModelKey(ClientModel client) {
|
||||
return client.getRealm().getId() + "::client::" + client.getId();
|
||||
}
|
||||
|
||||
|
||||
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
|
||||
String kid = input.getHeader().getKeyId();
|
||||
|
||||
KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
|
||||
|
||||
String modelKey = getModelKey(realm, idpConfig);
|
||||
OIDCIdentityProviderLoader loader = new OIDCIdentityProviderLoader(session, idpConfig);
|
||||
return keyStorage.getPublicKey(modelKey, kid, loader);
|
||||
}
|
||||
|
||||
private static String getModelKey(RealmModel realm, OIDCIdentityProviderConfig idpConfig) {
|
||||
return realm.getId() + "::idp::" + idpConfig.getInternalId();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.keys.loader;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.keys.KeyLoader;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OIDCIdentityProviderLoader implements KeyLoader {
|
||||
|
||||
protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final OIDCIdentityProviderConfig config;
|
||||
|
||||
public OIDCIdentityProviderLoader(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
this.session = session;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, PublicKey> loadKeys() throws Exception {
|
||||
if (config.isUseJwksUrl()) {
|
||||
String jwksUrl = config.getJwksUrl();
|
||||
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
|
||||
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
|
||||
} else {
|
||||
try {
|
||||
PublicKey publicKey = getSavedPublicKey();
|
||||
if (publicKey == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
String kid = JWKBuilder.createKeyId(publicKey);
|
||||
return Collections.singletonMap(kid, publicKey);
|
||||
} catch (Exception e) {
|
||||
logger.warnf(e, "Unable to retrieve publicKey for verify signature of identityProvider '%s' . Error details: %s", config.getAlias(), e.getMessage());
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected PublicKey getSavedPublicKey() throws Exception {
|
||||
if (config.getPublicKeySignatureVerifier() != null && !config.getPublicKeySignatureVerifier().trim().equals("")) {
|
||||
return PemUtils.decodePublicKey(config.getPublicKeySignatureVerifier());
|
||||
} else {
|
||||
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String, Object> requestParams;
|
||||
|
||||
public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception {
|
||||
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
|
||||
JWSInput input = new JWSInput(requestObject);
|
||||
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");
|
||||
|
|
|
@ -30,27 +30,14 @@ import java.io.InputStream;
|
|||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* TODO: Merge with JWKSUtils from keycloak-core?
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JWKSUtils {
|
||||
public class JWKSHttpUtils {
|
||||
|
||||
public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
if (scriptModel == null) {
|
||||
throw new IllegalArgumentException("script must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
|
||||
|
||||
if (script == null) {
|
||||
throw new NullPointerException("script must not be null");
|
||||
}
|
||||
|
||||
if (script.getCode() == null || script.getCode().trim().isEmpty()) {
|
||||
if (scriptModel.getCode() == null || scriptModel.getCode().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("script must not be null or empty");
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
|
||||
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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);
|
||||
} 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;
|
||||
} catch (IllegalStateException ise) {
|
||||
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
} 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;
|
||||
} catch (IllegalStateException ise) {
|
||||
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private CertificateRepresentation getCertFromRequest(UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
|
||||
private CertificateRepresentation getCertFromRequest(MultipartFormDataInput input) throws IOException {
|
||||
auth.requireManage();
|
||||
CertificateRepresentation info = new CertificateRepresentation();
|
||||
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
|
||||
|
@ -217,11 +217,17 @@ 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String keyAlias = uploadForm.get("keyAlias").get(0).getBodyAsString();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -37,8 +38,8 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
|
|||
public static final String PROFILE_URL = "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name";
|
||||
public static final String 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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -37,8 +38,8 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
|
|||
public static final String PROFILE_URL = "https://api.github.com/user";
|
||||
public static final String 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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -30,8 +31,8 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
|
|||
public static final String PROFILE_URL = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
|
||||
public static final String 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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -57,8 +57,8 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
SocialIdentityProvider<OAuth2IdentityProviderConfig> {
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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<OAuth2IdentityProviderConfig> {
|
||||
|
||||
public TestProvider(OAuth2IdentityProviderConfig config) {
|
||||
super(config);
|
||||
super(null, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
],
|
||||
"realmRoles": [ "user" ],
|
||||
"clientRoles": {
|
||||
"realm-management" : [ "view-realm" ],
|
||||
"account": ["view-profile", "manage-account"]
|
||||
}
|
||||
},{
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<button onclick="keycloak.register()">Register</button>
|
||||
<button onclick="refreshToken(9999)">Refresh Token</button>
|
||||
<button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
|
||||
<button onclick="refreshToken(5)">Refresh Token (if <5s validity)</button>
|
||||
<button onclick="showError()">Show Error Response</button>
|
||||
<button onclick="loadProfile()">Get Profile</button>
|
||||
<button onclick="loadUserInfo()">Get User Info</button>
|
||||
|
@ -41,6 +42,13 @@
|
|||
<button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
|
||||
<button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
|
||||
<button onclick="createBearerRequest()">Create Bearer Request</button>
|
||||
<button onclick="output(showTime())">Show current time</button>
|
||||
<input id="timeSkewInput"/>
|
||||
<button onclick="addToTimeSkew()">timeSkew offset</button>
|
||||
<button onclick="refreshTimeSkew()">refresh timeSkew</button>
|
||||
<button onclick="sendBearerToKeycloak()">Bearer to keycloak</button>
|
||||
|
||||
|
||||
<select id="flowSelect">
|
||||
<option value="standard">standard</option>
|
||||
<option value="implicit">implicit</option>
|
||||
|
@ -62,6 +70,9 @@
|
|||
<h2>Events</h2>
|
||||
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
|
||||
|
||||
<h2>Info</h2>
|
||||
TimeSkew: <div id="timeSkew"></div>
|
||||
|
||||
|
||||
<script>
|
||||
function loadProfile() {
|
||||
|
@ -135,6 +146,16 @@
|
|||
document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
|
||||
}
|
||||
|
||||
function addToTimeSkew() {
|
||||
var offset = document.getElementById("timeSkewInput").value;
|
||||
keycloak.timeSkew += parseInt(offset);
|
||||
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
|
||||
}
|
||||
|
||||
function refreshTimeSkew() {
|
||||
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
|
||||
}
|
||||
|
||||
function createBearerRequest() {
|
||||
|
||||
var url = 'http://localhost:8280/js-database/customers';
|
||||
|
@ -167,6 +188,33 @@
|
|||
req.send();
|
||||
}
|
||||
|
||||
function sendBearerToKeycloak() {
|
||||
var url = 'http://localhost:8180/auth/admin/realms/example/roles';
|
||||
if (window.location.href.indexOf("8543") > -1) {
|
||||
url = url.replace("8180","8543");
|
||||
url = url.replace("http","https");
|
||||
}
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', url, true);
|
||||
req.setRequestHeader('Accept', 'application/json');
|
||||
req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
|
||||
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
output('Success');
|
||||
} else if (req.status == 403) {
|
||||
output('Forbidden');
|
||||
} else if (req.status == 401) {
|
||||
output('Unauthorized');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
req.send();
|
||||
}
|
||||
|
||||
var keycloak;
|
||||
|
||||
function keycloakInit() {
|
||||
|
@ -182,6 +230,7 @@
|
|||
|
||||
keycloak.onAuthRefreshSuccess = function () {
|
||||
event('Auth Refresh Success');
|
||||
document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
|
||||
};
|
||||
|
||||
keycloak.onAuthRefreshError = function () {
|
||||
|
|
|
@ -27,7 +27,7 @@ import javax.ws.rs.Produces;
|
|||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -52,7 +52,12 @@ public class AdminAlbumService {
|
|||
List<Album> result = this.entityManager.createQuery("from Album").getResultList();
|
||||
|
||||
for (Album album : result) {
|
||||
albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
|
||||
//We need to compile this under JDK7 so we can't use lambdas
|
||||
//albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
|
||||
|
||||
if (!albums.containsKey(album.getUserId())) {
|
||||
albums.put(album.getUserId(), Collections.singletonList(album));
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok(albums).build();
|
||||
|
|
|
@ -21,5 +21,18 @@
|
|||
<module>photoz</module>
|
||||
<module>hello-world-authz-service</module>
|
||||
<module>servlet-authz</module>
|
||||
<module>servlets</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
56
testsuite/integration-arquillian/test-apps/servlets/pom.xml
Normal file
56
testsuite/integration-arquillian/test-apps/servlets/pom.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>integration-arquillian-test-apps</artifactId>
|
||||
<groupId>org.keycloak.testsuite</groupId>
|
||||
<version>2.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>integration-arquillian-test-apps-servlets</artifactId>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-adapter-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.ws.rs</groupId>
|
||||
<artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-adapter-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-adapter-api-public</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -24,12 +24,7 @@ import org.keycloak.adapters.KeycloakDeployment;
|
|||
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
|
||||
import org.keycloak.common.util.Time;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue