diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java index b211aef7f3..6539cd9420 100755 --- a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java +++ b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java @@ -57,7 +57,17 @@ public class AccessTokenResponse { protected Map otherClaims = new HashMap(); + // 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() { return token; diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md index 7d2d8418d2..21dd586654 100644 --- a/misc/CrossDataCenter.md +++ b/misc/CrossDataCenter.md @@ -123,8 +123,6 @@ Keycloak servers setup true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory - org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory - localhost:${remote.cache.port} work false diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 24ef8737bb..6c84385b54 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -42,7 +42,6 @@ import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.sessions.infinispan.remotestore.KeycloakRemoteStoreConfigurationBuilder; -import org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory; import javax.naming.InitialContext; @@ -356,7 +355,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon builder.persistence() .passivation(false) .addStore(KeycloakRemoteStoreConfigurationBuilder.class) - .remoteServers(jdgServer + ":" + jdgPort) .sessionCache(sessionCache) .fetchPersistentState(false) .ignoreModifications(false) @@ -367,10 +365,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon .rawValues(true) .forceReturnValues(false) .marshaller(KeycloakHotRodMarshallerFactory.class.getName()) - .transportFactory(KeycloakTcpTransportFactory.class.getName()) -// .addServer() -// .host(jdgServer) -// .port(jdgPort) + .addServer() + .host(jdgServer) + .port(jdgPort) // .connectionPool() // .maxActive(100) // .exhaustedAction(ExhaustedAction.CREATE_NEW) @@ -386,7 +383,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon builder.persistence() .passivation(false) .addStore(KeycloakRemoteStoreConfigurationBuilder.class) - .remoteServers(jdgServer + ":" + jdgPort) .sessionCache(false) .fetchPersistentState(false) .ignoreModifications(false) @@ -397,10 +393,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon .rawValues(true) .forceReturnValues(false) .marshaller(KeycloakHotRodMarshallerFactory.class.getName()) - .transportFactory(KeycloakTcpTransportFactory.class.getName()) -// .addServer() -// .host(jdgServer) -// .port(jdgPort) + .addServer() + .host(jdgServer) + .port(jdgPort) .async() .enabled(async); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStore.java index 88df0492e6..a6f526dbe9 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStore.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStore.java @@ -17,7 +17,6 @@ package org.keycloak.models.sessions.infinispan.remotestore; -import java.util.Optional; import java.util.concurrent.Executor; import org.infinispan.commons.CacheException; @@ -62,17 +61,10 @@ public class KeycloakRemoteStore extends RemoteStore { EmbeddedCacheManager cacheManager = ctx.getCache().getCacheManager(); cacheManager.getCache(cacheTemplateName, true); - Optional optional = cacheManager.getCacheConfiguration(cacheTemplateName).persistence().stores().stream().filter((StoreConfiguration storeConfig) -> { - - return storeConfig instanceof KeycloakRemoteStoreConfiguration; - - }).findFirst(); - - if (!optional.isPresent()) { - throw new CacheException("Unable to find remoteStore on cache '" + cacheTemplateName + "."); - } - - KeycloakRemoteStoreConfiguration templateConfig = (KeycloakRemoteStoreConfiguration) optional.get(); + KeycloakRemoteStoreConfiguration templateConfig = (KeycloakRemoteStoreConfiguration) cacheManager.getCacheConfiguration(cacheTemplateName).persistence().stores().stream() + .filter((StoreConfiguration storeConfig) -> storeConfig instanceof KeycloakRemoteStoreConfiguration) + .findFirst() + .orElseThrow(() -> new CacheException("Unable to find remoteStore on cache '" + cacheTemplateName + ".")); // 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(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java index 3f7d258186..fda2c76a61 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java @@ -31,18 +31,15 @@ import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration { static final AttributeDefinition USE_CONFIG_TEMPLATE_FROM_CACHE = AttributeDefinition.builder("useConfigTemplateFromCache", null, String.class).immutable().build(); - static final AttributeDefinition REMOTE_SERVERS = AttributeDefinition.builder("remoteServers", null, String.class).immutable().build(); static final AttributeDefinition SESSION_CACHE = AttributeDefinition.builder("sessionCache", null, Boolean.class).immutable().build(); private final Attribute useConfigTemplateFromCache; - private final Attribute remoteServers; private final Attribute sessionCache; public KeycloakRemoteStoreConfiguration(RemoteStoreConfiguration other) { super(other.attributes(), other.async(), other.singletonStore(), other.asyncExecutorFactory(), other.connectionPool()); useConfigTemplateFromCache = attributes.attribute(USE_CONFIG_TEMPLATE_FROM_CACHE.name()); - remoteServers = attributes.attribute(REMOTE_SERVERS.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() { return sessionCache.get()==null ? false : sessionCache.get(); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfigurationBuilder.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfigurationBuilder.java index a6ea1e6269..373411472b 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfigurationBuilder.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfigurationBuilder.java @@ -18,9 +18,7 @@ package org.keycloak.models.sessions.infinispan.remotestore; import java.lang.reflect.Field; -import java.util.List; import java.util.Map; -import java.util.StringTokenizer; import org.infinispan.commons.CacheConfigurationException; import org.infinispan.commons.configuration.attributes.Attribute; @@ -49,10 +47,6 @@ public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigur Attribute attribute = def.toAttribute(); attributesInternal.put(def.name(), attribute); - def = KeycloakRemoteStoreConfiguration.REMOTE_SERVERS; - attribute = def.toAttribute(); - attributesInternal.put(def.name(), attribute); - AttributeDefinition defBool = KeycloakRemoteStoreConfiguration.SESSION_CACHE; Attribute attributeBool = defBool.toAttribute(); attributesInternal.put(defBool.name(), attributeBool); @@ -65,12 +59,6 @@ public class KeycloakRemoteStoreConfigurationBuilder extends RemoteStoreConfigur @Override public KeycloakRemoteStoreConfiguration create() { - String remoteServersAttr = attributes.attribute(KeycloakRemoteStoreConfiguration.REMOTE_SERVERS).get(); - boolean isServersAlreadySet = isServersAlreadySet(); - if (remoteServersAttr != null && !isServersAlreadySet) { - parseRemoteServersAttr(remoteServersAttr); - } - RemoteStoreConfiguration cfg = super.create(); KeycloakRemoteStoreConfiguration cfg2 = new KeycloakRemoteStoreConfiguration(cfg); 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) { attributes.attribute(KeycloakRemoteStoreConfiguration.SESSION_CACHE).set(sessionCache); 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); - } - } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakTcpTransportFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakTcpTransportFactory.java deleted file mode 100644 index a8985f248d..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakTcpTransportFactory.java +++ /dev/null @@ -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 Marek Posolda - */ -public class KeycloakTcpTransportFactory extends TcpTransportFactory { - - protected static final Logger logger = Logger.getLogger(KeycloakTcpTransportFactory.class); - - private Collection 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 newServers, byte[] cacheName, boolean quiet) { - try { - logger.debugf("Update servers called: %s, cacheName: %s", newServers, new String(cacheName, "UTF-8")); - - Collection 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 getFilteredNewServers(Collection newServers) { - Collection initialServers = getInitialServers(); - Collection 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 getInitialServers() { - return kcInitialServers; - } - - - -} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index d4bb40ffa0..39a4215b84 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -70,6 +70,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.security.PublicKey; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -854,8 +855,57 @@ public class TokenManager { if (userNotBefore > notBefore) notBefore = userNotBefore; 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 returnedScopes = new ArrayList(); + // at attachAuthenticationSession(), take over notes from AuthenticationSessionModel to AuthenticatedClientSessionModel + List 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; } + + private void addRolesAsScopes(List returnedScopes, List requestedScopes, Set roles) { + for (String r : roles) { + for (String s : requestedScopes) { + if (s.equals(r)) { + returnedScopes.add(s); + } + } + } + } + + private void addRolesAsScopes(List returnedScopes, List requestedScopes, Set roles, String clientId) { + for (String r : roles) { + for (String s : requestedScopes) { + if (s.equals(clientId + "/" + r)) { + returnedScopes.add(s); + } + } + } + } } public class RefreshResult { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 5fa46a0da1..7a1f005711 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -433,6 +433,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { 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.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 if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java index a0f874b8de..29b734f76e 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java @@ -36,6 +36,7 @@ public class AuthorizationEndpointRequest { String nonce; Integer maxAge; String idpHint; + String claims; Map additionalReqParams = new HashMap<>(); // https://tools.ietf.org/html/rfc7636#section-6.1 @@ -86,6 +87,10 @@ public class AuthorizationEndpointRequest { return idpHint; } + public String getClaims() { + return claims; + } + public Map getAdditionalReqParams() { return additionalReqParams; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java index 06de42c3db..6803680c94 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java @@ -14,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.keycloak.protocol.oidc.endpoints.request; +import com.fasterxml.jackson.databind.JsonNode; import java.security.PublicKey; import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; import java.util.Set; import org.keycloak.jose.jws.Algorithm; @@ -39,7 +39,7 @@ import org.keycloak.util.JsonSerialization; */ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { - private final Map requestParams; + private final JsonNode requestParams; public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception { JWSInput input = new JWSInput(requestObject); @@ -52,7 +52,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { } 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) { PublicKey clientPublicKey = PublicKeyStorageManager.getClientPublicKey(session, client, input); if (clientPublicKey == null) { @@ -64,7 +64,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { 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 { throw new RuntimeException("Unsupported JWA algorithm used for signed request"); } @@ -72,8 +72,14 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { @Override protected String getParameter(String paramName) { - Object val = this.requestParams.get(paramName); - return val==null ? null : val.toString(); + JsonNode val = this.requestParams.get(paramName); + if (val == null) { + return null; + } else if (val.isValueNode()) { + return val.asText(); + } else { + return val.toString(); + } } @Override @@ -84,7 +90,9 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser { @Override protected Set keySet() { - return requestParams.keySet(); + HashSet keys = new HashSet<>(); + requestParams.fieldNames().forEachRemaining(keys::add); + return keys; } static class TypedHashMap extends HashMap { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java index 346b1a6625..0f9d1527cb 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java @@ -61,6 +61,7 @@ abstract class AuthzEndpointRequestParser { KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM); KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_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 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.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_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 request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM)); diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli index 836cde1d82..3a0a7b0cd5 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli @@ -22,8 +22,6 @@ echo ** Update replicated-cache work element ** name=properties, value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ - transportFactory=org.keycloak.models.sessions.infinispan.remotestore.KeycloakTcpTransportFactory, \ - remoteServers=localhost:${remote.cache.port}, \ remoteCacheName=work, \ sessionCache=false \ } \ diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 7a3a5b3bd6..53d3f48eb8 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -949,6 +949,8 @@ public class OAuthClient { private int expiresIn; private int refreshExpiresIn; 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 errorDescription; @@ -970,6 +972,11 @@ public class OAuthClient { expiresIn = (Integer) responseJson.get("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)) { refreshToken = (String) responseJson.get(OAuth2Constants.REFRESH_TOKEN); } @@ -1017,6 +1024,11 @@ public class OAuthClient { public String getTokenType() { 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) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java new file mode 100644 index 0000000000..e6c8253cb4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthScopeInTokenResponseTest.java @@ -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 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 expectedScopes = Arrays.asList(expectedScope.split(" ")); + Collection receivedScopes = Arrays.asList(response.getScope().split(" ")); + Assert.assertTrue(expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes)); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index f558ede702..5bbf71ed58 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -17,17 +17,24 @@ package org.keycloak.testsuite.oidc; +import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.Time; import org.keycloak.events.Details; 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.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; 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.OAuthGrantPage; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import org.keycloak.testsuite.util.ClientManager; 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.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -83,6 +96,10 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Page protected ErrorPage errorPage; + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(OIDCAdvancedRequestParamsTest.class, AbstractTestRealmKeycloakTest.class); + } @Override public void configureTestRealm(RealmRepresentation testRealm) { @@ -478,5 +495,78 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest 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 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 claims = ImmutableMap.of( + "id_token", ImmutableMap.of( + "test_claim", ImmutableMap.of( + "essential", true))); + + String claimsJson = JsonSerialization.writeValueAsString(claims); + + Map 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); + }); + } }