Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
b0464f1751
16 changed files with 404 additions and 207 deletions
|
@ -57,7 +57,17 @@ public class AccessTokenResponse {
|
||||||
|
|
||||||
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
|
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
|
||||||
|
|
||||||
|
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
@JsonProperty("scope")
|
||||||
|
protected String scope;
|
||||||
|
|
||||||
|
public String getScope() {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScope(String scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
return token;
|
return token;
|
||||||
|
|
|
@ -123,8 +123,6 @@ Keycloak servers setup
|
||||||
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
|
<store class="org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
|
||||||
<property name="rawValues">true</property>
|
<property name="rawValues">true</property>
|
||||||
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
||||||
<property name="transportFactory">org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory</property>
|
|
||||||
<property name="remoteServers">localhost:${remote.cache.port}</property>
|
|
||||||
<property name="remoteCacheName">work</property>
|
<property name="remoteCacheName">work</property>
|
||||||
<property name="sessionCache">false</property>
|
<property name="sessionCache">false</property>
|
||||||
</store>
|
</store>
|
||||||
|
|
|
@ -42,7 +42,6 @@ import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder;
|
import org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder;
|
||||||
import org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory;
|
|
||||||
|
|
||||||
import javax.naming.InitialContext;
|
import javax.naming.InitialContext;
|
||||||
|
|
||||||
|
@ -356,7 +355,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
builder.persistence()
|
builder.persistence()
|
||||||
.passivation(false)
|
.passivation(false)
|
||||||
.addStore(KeycloakRemoteStoreConfigurationBuilder.class)
|
.addStore(KeycloakRemoteStoreConfigurationBuilder.class)
|
||||||
.remoteServers(jdgServer + ":" + jdgPort)
|
|
||||||
.sessionCache(sessionCache)
|
.sessionCache(sessionCache)
|
||||||
.fetchPersistentState(false)
|
.fetchPersistentState(false)
|
||||||
.ignoreModifications(false)
|
.ignoreModifications(false)
|
||||||
|
@ -367,10 +365,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
.rawValues(true)
|
.rawValues(true)
|
||||||
.forceReturnValues(false)
|
.forceReturnValues(false)
|
||||||
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
|
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
|
||||||
.transportFactory(KeycloakTcpTransportFactory.class.getName())
|
.addServer()
|
||||||
// .addServer()
|
.host(jdgServer)
|
||||||
// .host(jdgServer)
|
.port(jdgPort)
|
||||||
// .port(jdgPort)
|
|
||||||
// .connectionPool()
|
// .connectionPool()
|
||||||
// .maxActive(100)
|
// .maxActive(100)
|
||||||
// .exhaustedAction(ExhaustedAction.CREATE_NEW)
|
// .exhaustedAction(ExhaustedAction.CREATE_NEW)
|
||||||
|
@ -386,7 +383,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
builder.persistence()
|
builder.persistence()
|
||||||
.passivation(false)
|
.passivation(false)
|
||||||
.addStore(KeycloakRemoteStoreConfigurationBuilder.class)
|
.addStore(KeycloakRemoteStoreConfigurationBuilder.class)
|
||||||
.remoteServers(jdgServer + ":" + jdgPort)
|
|
||||||
.sessionCache(false)
|
.sessionCache(false)
|
||||||
.fetchPersistentState(false)
|
.fetchPersistentState(false)
|
||||||
.ignoreModifications(false)
|
.ignoreModifications(false)
|
||||||
|
@ -397,10 +393,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
.rawValues(true)
|
.rawValues(true)
|
||||||
.forceReturnValues(false)
|
.forceReturnValues(false)
|
||||||
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
|
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
|
||||||
.transportFactory(KeycloakTcpTransportFactory.class.getName())
|
.addServer()
|
||||||
// .addServer()
|
.host(jdgServer)
|
||||||
// .host(jdgServer)
|
.port(jdgPort)
|
||||||
// .port(jdgPort)
|
|
||||||
.async()
|
.async()
|
||||||
.enabled(async);
|
.enabled(async);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan.remotestore;
|
package org.keycloak.models.sessions.infinispan.remotestore;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
import org.infinispan.commons.CacheException;
|
import org.infinispan.commons.CacheException;
|
||||||
|
@ -62,17 +61,10 @@ public class KeycloakRemoteStore extends RemoteStore {
|
||||||
EmbeddedCacheManager cacheManager = ctx.getCache().getCacheManager();
|
EmbeddedCacheManager cacheManager = ctx.getCache().getCacheManager();
|
||||||
cacheManager.getCache(cacheTemplateName, true);
|
cacheManager.getCache(cacheTemplateName, true);
|
||||||
|
|
||||||
Optional<StoreConfiguration> optional = cacheManager.getCacheConfiguration(cacheTemplateName).persistence().stores().stream().filter((StoreConfiguration storeConfig) -> {
|
KeycloakRemoteStoreConfiguration templateConfig = (KeycloakRemoteStoreConfiguration) cacheManager.getCacheConfiguration(cacheTemplateName).persistence().stores().stream()
|
||||||
|
.filter((StoreConfiguration storeConfig) -> storeConfig instanceof KeycloakRemoteStoreConfiguration)
|
||||||
return storeConfig instanceof KeycloakRemoteStoreConfiguration;
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new CacheException("Unable to find remoteStore on cache '" + cacheTemplateName + "."));
|
||||||
}).findFirst();
|
|
||||||
|
|
||||||
if (!optional.isPresent()) {
|
|
||||||
throw new CacheException("Unable to find remoteStore on cache '" + cacheTemplateName + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
KeycloakRemoteStoreConfiguration templateConfig = (KeycloakRemoteStoreConfiguration) optional.get();
|
|
||||||
|
|
||||||
// We have template configuration, so create new configuration from it. Override just remoteCacheName and sessionsCache (not pretty, but works for now)
|
// We have template configuration, so create new configuration from it. Override just remoteCacheName and sessionsCache (not pretty, but works for now)
|
||||||
PersistenceConfigurationBuilder readPersistenceBuilder = new ConfigurationBuilder().read(ctx.getCache().getCacheConfiguration()).persistence();
|
PersistenceConfigurationBuilder readPersistenceBuilder = new ConfigurationBuilder().read(ctx.getCache().getCacheConfiguration()).persistence();
|
||||||
|
|
|
@ -31,18 +31,15 @@ import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
|
||||||
public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration {
|
public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration {
|
||||||
|
|
||||||
static final AttributeDefinition<String> USE_CONFIG_TEMPLATE_FROM_CACHE = AttributeDefinition.builder("useConfigTemplateFromCache", null, String.class).immutable().build();
|
static final AttributeDefinition<String> USE_CONFIG_TEMPLATE_FROM_CACHE = AttributeDefinition.builder("useConfigTemplateFromCache", null, String.class).immutable().build();
|
||||||
static final AttributeDefinition<String> REMOTE_SERVERS = AttributeDefinition.builder("remoteServers", null, String.class).immutable().build();
|
|
||||||
static final AttributeDefinition<Boolean> SESSION_CACHE = AttributeDefinition.builder("sessionCache", null, Boolean.class).immutable().build();
|
static final AttributeDefinition<Boolean> SESSION_CACHE = AttributeDefinition.builder("sessionCache", null, Boolean.class).immutable().build();
|
||||||
|
|
||||||
private final Attribute<String> useConfigTemplateFromCache;
|
private final Attribute<String> useConfigTemplateFromCache;
|
||||||
private final Attribute<String> remoteServers;
|
|
||||||
private final Attribute<Boolean> sessionCache;
|
private final Attribute<Boolean> sessionCache;
|
||||||
|
|
||||||
|
|
||||||
public KeycloakRemoteStoreConfiguration(RemoteStoreConfiguration other) {
|
public KeycloakRemoteStoreConfiguration(RemoteStoreConfiguration other) {
|
||||||
super(other.attributes(), other.async(), other.singletonStore(), other.asyncExecutorFactory(), other.connectionPool());
|
super(other.attributes(), other.async(), other.singletonStore(), other.asyncExecutorFactory(), other.connectionPool());
|
||||||
useConfigTemplateFromCache = attributes.attribute(USE_CONFIG_TEMPLATE_FROM_CACHE.name());
|
useConfigTemplateFromCache = attributes.attribute(USE_CONFIG_TEMPLATE_FROM_CACHE.name());
|
||||||
remoteServers = attributes.attribute(REMOTE_SERVERS.name());
|
|
||||||
sessionCache = attributes.attribute(SESSION_CACHE.name());
|
sessionCache = attributes.attribute(SESSION_CACHE.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,11 +49,6 @@ public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String remoteServers() {
|
|
||||||
return remoteServers.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Boolean sessionCache() {
|
public Boolean sessionCache() {
|
||||||
return sessionCache.get()==null ? false : sessionCache.get();
|
return sessionCache.get()==null ? false : sessionCache.get();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
package org.keycloak.models.sessions.infinispan.remotestore;
|
package org.keycloak.models.sessions.infinispan.remotestore;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.StringTokenizer;
|
|
||||||
|
|
||||||
import org.infinispan.commons.CacheConfigurationException;
|
import org.infinispan.commons.CacheConfigurationException;
|
||||||
import org.infinispan.commons.configuration.attributes.Attribute;
|
import org.infinispan.commons.configuration.attributes.Attribute;
|
||||||
|
@ -49,10 +47,6 @@ public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigur
|
||||||
Attribute<String> attribute = def.toAttribute();
|
Attribute<String> attribute = def.toAttribute();
|
||||||
attributesInternal.put(def.name(), attribute);
|
attributesInternal.put(def.name(), attribute);
|
||||||
|
|
||||||
def = KeycloakRemoteStoreConfiguration.REMOTE_SERVERS;
|
|
||||||
attribute = def.toAttribute();
|
|
||||||
attributesInternal.put(def.name(), attribute);
|
|
||||||
|
|
||||||
AttributeDefinition<Boolean> defBool = KeycloakRemoteStoreConfiguration.SESSION_CACHE;
|
AttributeDefinition<Boolean> defBool = KeycloakRemoteStoreConfiguration.SESSION_CACHE;
|
||||||
Attribute<Boolean> attributeBool = defBool.toAttribute();
|
Attribute<Boolean> attributeBool = defBool.toAttribute();
|
||||||
attributesInternal.put(defBool.name(), attributeBool);
|
attributesInternal.put(defBool.name(), attributeBool);
|
||||||
|
@ -65,12 +59,6 @@ public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigur
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeycloakRemoteStoreConfiguration create() {
|
public KeycloakRemoteStoreConfiguration create() {
|
||||||
String remoteServersAttr = attributes.attribute(KeycloakRemoteStoreConfiguration.REMOTE_SERVERS).get();
|
|
||||||
boolean isServersAlreadySet = isServersAlreadySet();
|
|
||||||
if (remoteServersAttr != null && !isServersAlreadySet) {
|
|
||||||
parseRemoteServersAttr(remoteServersAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteStoreConfiguration cfg = super.create();
|
RemoteStoreConfiguration cfg = super.create();
|
||||||
KeycloakRemoteStoreConfiguration cfg2 = new KeycloakRemoteStoreConfiguration(cfg);
|
KeycloakRemoteStoreConfiguration cfg2 = new KeycloakRemoteStoreConfiguration(cfg);
|
||||||
return cfg2;
|
return cfg2;
|
||||||
|
@ -83,40 +71,8 @@ public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigur
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public KeycloakRemoteStoreConfigurationBuilder remoteServers(String remoteServers) {
|
|
||||||
attributes.attribute(KeycloakRemoteStoreConfiguration.REMOTE_SERVERS).set(remoteServers);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public KeycloakRemoteStoreConfigurationBuilder sessionCache(Boolean sessionCache) {
|
public KeycloakRemoteStoreConfigurationBuilder sessionCache(Boolean sessionCache) {
|
||||||
attributes.attribute(KeycloakRemoteStoreConfiguration.SESSION_CACHE).set(sessionCache);
|
attributes.attribute(KeycloakRemoteStoreConfiguration.SESSION_CACHE).set(sessionCache);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void parseRemoteServersAttr(String remoteServers) {
|
|
||||||
StringTokenizer st = new StringTokenizer(remoteServers, ",");
|
|
||||||
|
|
||||||
while (st.hasMoreElements()) {
|
|
||||||
String nodeStr = st.nextToken();
|
|
||||||
String[] node = nodeStr.trim().split(":", 2);
|
|
||||||
|
|
||||||
addServer()
|
|
||||||
.host(node[0].trim())
|
|
||||||
.port(Integer.parseInt(node[1].trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isServersAlreadySet() {
|
|
||||||
try {
|
|
||||||
Field f = Reflections.findDeclaredField(RemoteStoreConfigurationBuilder.class, "servers");
|
|
||||||
f.setAccessible(true);
|
|
||||||
List originalRemoteServers = (List) f.get(this);
|
|
||||||
return !originalRemoteServers.isEmpty();
|
|
||||||
} catch (IllegalAccessException iae) {
|
|
||||||
throw new RuntimeException(iae);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2017 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.sessions.infinispan.remotestore;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.infinispan.client.hotrod.configuration.Configuration;
|
|
||||||
import org.infinispan.client.hotrod.configuration.ServerConfiguration;
|
|
||||||
import org.infinispan.client.hotrod.event.ClientListenerNotifier;
|
|
||||||
import org.infinispan.client.hotrod.impl.protocol.Codec;
|
|
||||||
import org.infinispan.client.hotrod.impl.transport.tcp.TcpTransportFactory;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.common.util.reflections.Reflections;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class KeycloakTcpTransportFactory extends TcpTransportFactory {
|
|
||||||
|
|
||||||
protected static final Logger logger = Logger.getLogger(KeycloakTcpTransportFactory.class);
|
|
||||||
|
|
||||||
private Collection<SocketAddress> kcInitialServers;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Codec codec, Configuration configuration, AtomicInteger defaultCacheTopologyId, ClientListenerNotifier listenerNotifier) {
|
|
||||||
kcInitialServers = new HashSet<>();
|
|
||||||
|
|
||||||
for (ServerConfiguration server : configuration.servers()) {
|
|
||||||
InetSocketAddress hostnameAddress = new InetSocketAddress(server.host(), server.port());
|
|
||||||
kcInitialServers.add(hostnameAddress);
|
|
||||||
|
|
||||||
// Retrieve servers by IP addresses too, as we need to compare by IP addresses
|
|
||||||
try {
|
|
||||||
String ip = InetAddress.getByName(server.host()).getHostAddress();
|
|
||||||
InetSocketAddress ipAddress = new InetSocketAddress(ip, server.port());
|
|
||||||
kcInitialServers.add(ipAddress);
|
|
||||||
|
|
||||||
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(ip, server.port());
|
|
||||||
kcInitialServers.add(unresolved);
|
|
||||||
} catch (UnknownHostException uhe) {
|
|
||||||
logger.warnf(uhe, "Wasn't able to retrieve IP address for host '%s'", server.host());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debugf("Keycloak initial servers: %s", kcInitialServers);
|
|
||||||
|
|
||||||
super.start(codec, configuration, defaultCacheTopologyId, listenerNotifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateServers(Collection<SocketAddress> newServers, byte[] cacheName, boolean quiet) {
|
|
||||||
try {
|
|
||||||
logger.debugf("Update servers called: %s, cacheName: %s", newServers, new String(cacheName, "UTF-8"));
|
|
||||||
|
|
||||||
Collection<SocketAddress> filteredServers = getFilteredNewServers(newServers);
|
|
||||||
|
|
||||||
logger.debugf("Update servers after filter: %s, cacheName: %s", filteredServers, new String(cacheName, "UTF-8"));
|
|
||||||
|
|
||||||
super.updateServers(filteredServers, cacheName, quiet);
|
|
||||||
|
|
||||||
} catch (UnsupportedEncodingException uee) {
|
|
||||||
throw new RuntimeException(uee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Return just those servers, which are part of the originally configured "kcInitialServers".
|
|
||||||
// Assume that the other JDG servers are part of same cluster, but are in different DC. Hence don't include them in the topology view
|
|
||||||
private Collection<SocketAddress> getFilteredNewServers(Collection<SocketAddress> newServers) {
|
|
||||||
Collection<SocketAddress> initialServers = getInitialServers();
|
|
||||||
Collection<SocketAddress> filteredServers = newServers.stream().filter((SocketAddress newAddress) -> {
|
|
||||||
|
|
||||||
boolean presentInInitialServers = initialServers.contains(newAddress);
|
|
||||||
|
|
||||||
if (!presentInInitialServers) {
|
|
||||||
logger.debugf("Server'%s' not present in initial servers. Probably server from different DC. Will filter it from the view", newAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return presentInInitialServers;
|
|
||||||
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
|
|
||||||
return filteredServers;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected Collection<SocketAddress> getInitialServers() {
|
|
||||||
return kcInitialServers;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -70,6 +70,7 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -854,8 +855,57 @@ public class TokenManager {
|
||||||
if (userNotBefore > notBefore) notBefore = userNotBefore;
|
if (userNotBefore > notBefore) notBefore = userNotBefore;
|
||||||
res.setNotBeforePolicy(notBefore);
|
res.setNotBeforePolicy(notBefore);
|
||||||
|
|
||||||
|
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
String requestedScope = clientSession.getNote(OAuth2Constants.SCOPE);
|
||||||
|
if (accessToken != null && requestedScope != null) {
|
||||||
|
List<String> returnedScopes = new ArrayList<String>();
|
||||||
|
// at attachAuthenticationSession(), take over notes from AuthenticationSessionModel to AuthenticatedClientSessionModel
|
||||||
|
List<String> requestedScopes = Arrays.asList(requestedScope.split(" "));
|
||||||
|
|
||||||
|
// distinguish between so called role scope and oauth scope
|
||||||
|
// only pick up oauth scope following https://tools.ietf.org/html/rfc6749#section-5.1
|
||||||
|
|
||||||
|
// for realm role - scope
|
||||||
|
if (accessToken.getRealmAccess() != null && accessToken.getRealmAccess().getRoles() != null) {
|
||||||
|
addRolesAsScopes(returnedScopes, requestedScopes, accessToken.getRealmAccess().getRoles());
|
||||||
|
}
|
||||||
|
// for client role - scope
|
||||||
|
if (accessToken.getResourceAccess() != null) {
|
||||||
|
for (String clientId : accessToken.getResourceAccess().keySet()) {
|
||||||
|
if (accessToken.getResourceAccess(clientId).getRoles() != null) {
|
||||||
|
addRolesAsScopes(returnedScopes, requestedScopes, accessToken.getResourceAccess(clientId).getRoles(), clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String s : returnedScopes) {
|
||||||
|
builder.append(s).append(" ");
|
||||||
|
}
|
||||||
|
res.setScope(builder.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addRolesAsScopes(List<String> returnedScopes, List<String> requestedScopes, Set<String> roles) {
|
||||||
|
for (String r : roles) {
|
||||||
|
for (String s : requestedScopes) {
|
||||||
|
if (s.equals(r)) {
|
||||||
|
returnedScopes.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRolesAsScopes(List<String> returnedScopes, List<String> requestedScopes, Set<String> roles, String clientId) {
|
||||||
|
for (String r : roles) {
|
||||||
|
for (String s : requestedScopes) {
|
||||||
|
if (s.equals(clientId + "/" + r)) {
|
||||||
|
returnedScopes.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RefreshResult {
|
public class RefreshResult {
|
||||||
|
|
|
@ -433,6 +433,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
|
if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
|
||||||
if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
|
if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
|
||||||
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
||||||
|
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-4
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
||||||
|
|
|
@ -36,6 +36,7 @@ public class AuthorizationEndpointRequest {
|
||||||
String nonce;
|
String nonce;
|
||||||
Integer maxAge;
|
Integer maxAge;
|
||||||
String idpHint;
|
String idpHint;
|
||||||
|
String claims;
|
||||||
Map<String, String> additionalReqParams = new HashMap<>();
|
Map<String, String> additionalReqParams = new HashMap<>();
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
|
@ -86,6 +87,10 @@ public class AuthorizationEndpointRequest {
|
||||||
return idpHint;
|
return idpHint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClaims() {
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> getAdditionalReqParams() {
|
public Map<String, String> getAdditionalReqParams() {
|
||||||
return additionalReqParams;
|
return additionalReqParams;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.endpoints.request;
|
package org.keycloak.protocol.oidc.endpoints.request;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
|
@ -39,7 +39,7 @@ import org.keycloak.util.JsonSerialization;
|
||||||
*/
|
*/
|
||||||
class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
|
|
||||||
private final Map<String, Object> requestParams;
|
private final JsonNode requestParams;
|
||||||
|
|
||||||
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
|
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
|
||||||
JWSInput input = new JWSInput(requestObject);
|
JWSInput input = new JWSInput(requestObject);
|
||||||
|
@ -52,7 +52,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.getAlgorithm() == Algorithm.none) {
|
if (header.getAlgorithm() == Algorithm.none) {
|
||||||
this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
|
this.requestParams = JsonSerialization.readValue(input.getContent(), JsonNode.class);
|
||||||
} else if (header.getAlgorithm() == Algorithm.RS256) {
|
} else if (header.getAlgorithm() == Algorithm.RS256) {
|
||||||
PublicKey clientPublicKey = PublicKeyStorageManager.getClientPublicKey(session, client, input);
|
PublicKey clientPublicKey = PublicKeyStorageManager.getClientPublicKey(session, client, input);
|
||||||
if (clientPublicKey == null) {
|
if (clientPublicKey == null) {
|
||||||
|
@ -64,7 +64,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
throw new RuntimeException("Failed to verify signature on 'request' object");
|
throw new RuntimeException("Failed to verify signature on 'request' object");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
|
this.requestParams = JsonSerialization.readValue(input.getContent(), JsonNode.class);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("Unsupported JWA algorithm used for signed request");
|
throw new RuntimeException("Unsupported JWA algorithm used for signed request");
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,14 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getParameter(String paramName) {
|
protected String getParameter(String paramName) {
|
||||||
Object val = this.requestParams.get(paramName);
|
JsonNode val = this.requestParams.get(paramName);
|
||||||
return val==null ? null : val.toString();
|
if (val == null) {
|
||||||
|
return null;
|
||||||
|
} else if (val.isValueNode()) {
|
||||||
|
return val.asText();
|
||||||
|
} else {
|
||||||
|
return val.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -84,7 +90,9 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Set<String> keySet() {
|
protected Set<String> keySet() {
|
||||||
return requestParams.keySet();
|
HashSet<String> keys = new HashSet<>();
|
||||||
|
requestParams.fieldNames().forEachRemaining(keys::add);
|
||||||
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class TypedHashMap extends HashMap<String, Object> {
|
static class TypedHashMap extends HashMap<String, Object> {
|
||||||
|
|
|
@ -61,6 +61,7 @@ abstract class AuthzEndpointRequestParser {
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||||
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
|
||||||
|
@ -87,6 +88,7 @@ abstract class AuthzEndpointRequestParser {
|
||||||
request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT));
|
request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT));
|
||||||
request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM));
|
request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM));
|
||||||
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
|
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
|
||||||
|
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
||||||
|
|
|
@ -22,8 +22,6 @@ echo ** Update replicated-cache work element **
|
||||||
name=properties, value={ \
|
name=properties, value={ \
|
||||||
rawValues=true, \
|
rawValues=true, \
|
||||||
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \
|
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \
|
||||||
transportFactory=org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory, \
|
|
||||||
remoteServers=localhost:${remote.cache.port}, \
|
|
||||||
remoteCacheName=work, \
|
remoteCacheName=work, \
|
||||||
sessionCache=false \
|
sessionCache=false \
|
||||||
} \
|
} \
|
||||||
|
|
|
@ -949,6 +949,8 @@ public class OAuthClient {
|
||||||
private int expiresIn;
|
private int expiresIn;
|
||||||
private int refreshExpiresIn;
|
private int refreshExpiresIn;
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
|
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
private String scope;
|
||||||
|
|
||||||
private String error;
|
private String error;
|
||||||
private String errorDescription;
|
private String errorDescription;
|
||||||
|
@ -970,6 +972,11 @@ public class OAuthClient {
|
||||||
expiresIn = (Integer) responseJson.get("expires_in");
|
expiresIn = (Integer) responseJson.get("expires_in");
|
||||||
refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in");
|
refreshExpiresIn = (Integer) responseJson.get("refresh_expires_in");
|
||||||
|
|
||||||
|
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
if (responseJson.containsKey(OAuth2Constants.SCOPE)) {
|
||||||
|
scope = (String) responseJson.get(OAuth2Constants.SCOPE);
|
||||||
|
}
|
||||||
|
|
||||||
if (responseJson.containsKey(OAuth2Constants.REFRESH_TOKEN)) {
|
if (responseJson.containsKey(OAuth2Constants.REFRESH_TOKEN)) {
|
||||||
refreshToken = (String) responseJson.get(OAuth2Constants.REFRESH_TOKEN);
|
refreshToken = (String) responseJson.get(OAuth2Constants.REFRESH_TOKEN);
|
||||||
}
|
}
|
||||||
|
@ -1017,6 +1024,11 @@ public class OAuthClient {
|
||||||
public String getTokenType() {
|
public String getTokenType() {
|
||||||
return tokenType;
|
return tokenType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
public String getScope() {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PublicKey getRealmPublicKey(String realm) {
|
public PublicKey getRealmPublicKey(String realm) {
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
|
//OIDC Financial API Read Only Profile : scope MUST be returned in the response from Token Endpoint
|
||||||
|
public class OAuthScopeInTokenResponseTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeAbstractKeycloakTest() throws Exception {
|
||||||
|
super.beforeAbstractKeycloakTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||||
|
testRealms.add(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyNoScopeTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String expectedScope = "";
|
||||||
|
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifySingleScopeAsRealmRoleTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "user";
|
||||||
|
String expectedScope = requestedScope;
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyMultipleScopeAsRealmRoleTest() throws Exception {
|
||||||
|
String loginUser = "rich.roles@redhat.com";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "user realm-composite-role";
|
||||||
|
String expectedScope = requestedScope;
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyNotAssignedScopeAsRealmRoleTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "user realm-composite-role";
|
||||||
|
String expectedScope = "user";
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifySingleScopeAsClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app/customer-user";
|
||||||
|
String expectedScope = requestedScope;
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyMultipleScopeAsClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "rich.roles@redhat.com";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app-scope/test-app-disallowed-by-scope test-app-scope/test-app-allowed-by-scope";
|
||||||
|
String expectedScope = requestedScope;
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyNotAssignedScopeAsClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "rich.roles@redhat.com";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app-scope/test-app-unspecified-by-scope test-app-scope/test-app-allowed-by-scope";
|
||||||
|
String expectedScope = "test-app-scope/test-app-allowed-by-scope";
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyMultipleScopeAsRealmAndClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "rich.roles@redhat.com";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app-scope/test-app-disallowed-by-scope admin test-app/customer-user test-app-scope/test-app-allowed-by-scope";
|
||||||
|
String expectedScope = requestedScope;
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyNotAssignedScopeAsRealmAndClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app/customer-user test-app-scope/test-app-disallowed-by-scope admin test-app/customer-user user test-app-scope/test-app-allowed-by-scope";
|
||||||
|
String expectedScope = "user test-app/customer-user";
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void specifyDuplicatedScopeAsRealmAndClientRoleTest() throws Exception {
|
||||||
|
String loginUser = "john-doh@localhost";
|
||||||
|
String loginPassword = "password";
|
||||||
|
String clientSecret = "password";
|
||||||
|
|
||||||
|
String requestedScope = "test-app/customer-user user user test-app/customer-user";
|
||||||
|
String expectedScope = "user test-app/customer-user";
|
||||||
|
|
||||||
|
oauth.scope(requestedScope);
|
||||||
|
oauth.doLogin(loginUser, loginPassword);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(code, expectedScope, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectSuccessfulResponseFromTokenEndpoint(String code, String expectedScope, String clientSecret) throws Exception {
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, clientSecret);
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
log.info("expectedScopes = " + expectedScope);
|
||||||
|
log.info("receivedScopes = " + response.getScope());
|
||||||
|
Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
|
||||||
|
Collection<String> receivedScopes = Arrays.asList(response.getScope().split(" "));
|
||||||
|
Assert.assertTrue(expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,17 +17,24 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.oidc;
|
package org.keycloak.testsuite.oidc;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
@ -49,10 +56,16 @@ import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
||||||
|
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||||
import org.keycloak.testsuite.util.ClientManager;
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
@ -83,6 +96,10 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
@Page
|
@Page
|
||||||
protected ErrorPage errorPage;
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@Deployment
|
||||||
|
public static WebArchive deploy() {
|
||||||
|
return RunOnServerDeployment.create(OIDCAdvancedRequestParamsTest.class, AbstractTestRealmKeycloakTest.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
@ -478,5 +495,78 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
|
|
||||||
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CLAIMS
|
||||||
|
// included in the session client notes, so custom providers can make use of it
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processClaimsQueryParam() throws IOException {
|
||||||
|
Map<String, Object> claims = ImmutableMap.of(
|
||||||
|
"id_token", ImmutableMap.of(
|
||||||
|
"test_claim", ImmutableMap.of(
|
||||||
|
"essential", true)));
|
||||||
|
|
||||||
|
String claimsJson = JsonSerialization.writeValueAsString(claims);
|
||||||
|
|
||||||
|
driver.navigate().to(oauth.getLoginFormUrl() + "&" + OIDCLoginProtocol.CLAIMS_PARAM + "=" + claimsJson);
|
||||||
|
|
||||||
|
// need to login so session id can be read from event
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String clientId = loginEvent.getClientId();
|
||||||
|
|
||||||
|
testingClient.server("test").run(session -> {
|
||||||
|
RealmModel realmModel = session.getContext().getRealm();
|
||||||
|
String clientUuid = realmModel.getClientByClientId(clientId).getId();
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realmModel, sessionId);
|
||||||
|
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUuid);
|
||||||
|
|
||||||
|
String claimsInSession = clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||||
|
assertEquals(claimsJson, claimsInSession);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processClaimsRequestParam() throws Exception {
|
||||||
|
Map<String, Object> claims = ImmutableMap.of(
|
||||||
|
"id_token", ImmutableMap.of(
|
||||||
|
"test_claim", ImmutableMap.of(
|
||||||
|
"essential", true)));
|
||||||
|
|
||||||
|
String claimsJson = JsonSerialization.writeValueAsString(claims);
|
||||||
|
|
||||||
|
Map<String, Object> oidcRequest = new HashMap<>();
|
||||||
|
oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, "test-app");
|
||||||
|
oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
|
||||||
|
oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri());
|
||||||
|
oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claims);
|
||||||
|
|
||||||
|
String request = new JWSBuilder().jsonContent(oidcRequest).none();
|
||||||
|
|
||||||
|
driver.navigate().to(oauth.getLoginFormUrl() + "&" + OIDCLoginProtocol.REQUEST_PARAM + "=" + request);
|
||||||
|
|
||||||
|
// need to login so session id can be read from event
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String clientId = loginEvent.getClientId();
|
||||||
|
|
||||||
|
testingClient.server("test").run(session -> {
|
||||||
|
RealmModel realmModel = session.getContext().getRealm();
|
||||||
|
String clientUuid = realmModel.getClientByClientId(clientId).getId();
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realmModel, sessionId);
|
||||||
|
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUuid);
|
||||||
|
|
||||||
|
String claimsInSession = clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||||
|
assertEquals(claimsJson, claimsInSession);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue