Support for ALL and ANY organization scope values
Related #31438 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
709165a90a
commit
8e0436715c
18 changed files with 483 additions and 186 deletions
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.common.util;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface TriFunction<T, U, V, R> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies this function to the given arguments.
|
||||||
|
*
|
||||||
|
* @param t the first function argument
|
||||||
|
* @param u the second function argument
|
||||||
|
* @param v the third function argument
|
||||||
|
* @return the function result
|
||||||
|
*/
|
||||||
|
R apply(T t, U u, V v);
|
||||||
|
|
||||||
|
}
|
|
@ -30,10 +30,7 @@ import org.keycloak.models.ClientSessionContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
|
||||||
import org.keycloak.organization.utils.Organizations;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
|
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
|
||||||
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
|
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
|
||||||
|
@ -45,8 +42,6 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
||||||
import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent;
|
|
||||||
|
|
||||||
public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, TokenIntrospectionTokenMapper, EnvironmentDependentProviderFactory {
|
public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, TokenIntrospectionTokenMapper, EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "oidc-organization-membership-mapper";
|
public static final String PROVIDER_ID = "oidc-organization-membership-mapper";
|
||||||
|
@ -80,22 +75,14 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {
|
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {
|
||||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
String rawScopes = clientSessionCtx.getScopeString();
|
||||||
|
OrganizationScope scope = OrganizationScope.valueOfScope(rawScopes);
|
||||||
|
|
||||||
if (!isEnabledAndOrganizationsPresent(provider)) {
|
if (scope == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationModel organization = Organizations.resolveOrganizationFromScopeParam(session, clientSessionCtx.getScopeString());
|
Stream<OrganizationModel> organizations = scope.resolveOrganizations(userSession.getUser(), rawScopes, session);
|
||||||
UserModel user = userSession.getUser();
|
|
||||||
Stream<OrganizationModel> organizations = Stream.empty();
|
|
||||||
|
|
||||||
if (organization == null) {
|
|
||||||
organizations = provider.getByMember(user).filter(OrganizationModel::isEnabled);
|
|
||||||
} else if (provider.isMember(organization, user)) {
|
|
||||||
organizations = Stream.of(organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, Object>> claim = new HashMap<>();
|
Map<String, Map<String, Object>> claim = new HashMap<>();
|
||||||
|
|
||||||
organizations.forEach(o -> claim.put(o.getAlias(), Map.of()));
|
organizations.forEach(o -> claim.put(o.getAlias(), Map.of()));
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.organization.protocol.mappers.oidc;
|
||||||
|
|
||||||
|
import static org.keycloak.organization.utils.Organizations.getProvider;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.TriFunction;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ClientScopeDecorator;
|
||||||
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.organization.utils.Organizations;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum with utility methods to process the {@link OIDCLoginProtocolFactory#ORGANIZATION} scope.
|
||||||
|
*/
|
||||||
|
public enum OrganizationScope {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps to any organization a user is a member
|
||||||
|
*/
|
||||||
|
ALL("*"::equals,
|
||||||
|
(organizations) -> true,
|
||||||
|
(user, scopes, session) -> {
|
||||||
|
if (user == null) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
return getProvider(session).getByMember(user).filter(OrganizationModel::isEnabled);
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps to a specific organization the user is a member.
|
||||||
|
*/
|
||||||
|
SINGLE(StringUtil::isNotBlank,
|
||||||
|
(organizations) -> organizations.findAny().isPresent(),
|
||||||
|
(user, scopes, session) -> {
|
||||||
|
OrganizationModel organization = parseScopeParameter(scopes)
|
||||||
|
.map(OrganizationScope::parseScopeValue)
|
||||||
|
.map(alias -> getProvider(session).getByAlias(alias))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(OrganizationModel::isEnabled)
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (organization == null) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null || organization.isMember(user)) {
|
||||||
|
return Stream.of(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.empty();
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps to a single organization if the user is a member of a single organization.
|
||||||
|
*/
|
||||||
|
ANY(""::equals,
|
||||||
|
(organizations) -> true,
|
||||||
|
(user, scopes, session) -> {
|
||||||
|
List<OrganizationModel> organizations = getProvider(session).getByMember(user).toList();
|
||||||
|
|
||||||
|
if (organizations.size() == 1) {
|
||||||
|
return organizations.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
private static final Pattern SCOPE_PATTERN = Pattern.compile(OIDCLoginProtocolFactory.ORGANIZATION + ":*".replace("*", "(.*)"));
|
||||||
|
private final Predicate<String> valueMatcher;
|
||||||
|
private final Predicate<Stream<OrganizationModel>> valueValidator;
|
||||||
|
private final TriFunction<UserModel, String, KeycloakSession, Stream<OrganizationModel>> orgResolver;
|
||||||
|
|
||||||
|
OrganizationScope(Predicate<String> valueMatcher, Predicate<Stream<OrganizationModel>> valueValidator, TriFunction<UserModel, String, KeycloakSession, Stream<OrganizationModel>> orgResolver) {
|
||||||
|
this.valueMatcher = valueMatcher;
|
||||||
|
this.valueValidator = valueValidator;
|
||||||
|
this.orgResolver = orgResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the organizations mapped from the {@code scope} based on the given {@code user}.
|
||||||
|
*
|
||||||
|
* @param user the user. Can be {@code null} depending on how the scope resolves its value.
|
||||||
|
* @param scope the string referencing the scope
|
||||||
|
* @param session the session
|
||||||
|
* @return the organizations mapped to the given {@code user}. Or an empty stream if no organizations were mapped from the {@code scope} parameter.
|
||||||
|
*/
|
||||||
|
public Stream<OrganizationModel> resolveOrganizations(UserModel user, String scope, KeycloakSession session) {
|
||||||
|
if (scope == null) {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
return orgResolver.apply(user, scope, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link ClientScopeModel} with the given {@code name} for this scope.
|
||||||
|
*
|
||||||
|
* @param name the name of the scope
|
||||||
|
* @param user the user
|
||||||
|
* @param session the session
|
||||||
|
* @return the {@link ClientScopeModel}
|
||||||
|
*/
|
||||||
|
public ClientScopeModel toClientScope(String name, UserModel user, KeycloakSession session) {
|
||||||
|
KeycloakContext context = session.getContext();
|
||||||
|
ClientModel client = context.getClient();
|
||||||
|
ClientScopeModel orgScope = getOrganizationClientScope(client, session);
|
||||||
|
|
||||||
|
if (orgScope == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
OrganizationScope scope = OrganizationScope.valueOfScope(name);
|
||||||
|
|
||||||
|
if (scope == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<OrganizationModel> organizations = scope.resolveOrganizations(user, name, session);
|
||||||
|
|
||||||
|
if (valueValidator.test(organizations)) {
|
||||||
|
return new ClientScopeDecorator(orgScope, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link OrganizationScope} instance based on the given {@code rawScope}.
|
||||||
|
*
|
||||||
|
* @param rawScope the string referencing the scope
|
||||||
|
* @return the organization scope that maps the given {@code rawScope}
|
||||||
|
*/
|
||||||
|
public static OrganizationScope valueOfScope(String rawScope) {
|
||||||
|
if (rawScope == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseScopeParameter(rawScope)
|
||||||
|
.map(s -> {
|
||||||
|
for (OrganizationScope scope : values()) {
|
||||||
|
if (scope.valueMatcher.test(parseScopeValue(s))) {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Objects::nonNull)
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseScopeValue(String scope) {
|
||||||
|
if (!hasOrganizationScope(scope)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope.equals(OIDCLoginProtocolFactory.ORGANIZATION)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = SCOPE_PATTERN.matcher(scope);
|
||||||
|
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientScopeModel getOrganizationClientScope(ClientModel client, KeycloakSession session) {
|
||||||
|
if (!Organizations.isEnabledAndOrganizationsPresent(session)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ClientScopeModel> scopes = new HashMap<>(client.getClientScopes(true));
|
||||||
|
scopes.putAll(client.getClientScopes(false));
|
||||||
|
|
||||||
|
return scopes.get(OIDCLoginProtocolFactory.ORGANIZATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasOrganizationScope(String scope) {
|
||||||
|
return scope != null && scope.contains(OIDCLoginProtocolFactory.ORGANIZATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<String> parseScopeParameter(String rawScope) {
|
||||||
|
return TokenManager.parseScopeParameter(rawScope)
|
||||||
|
.filter(OrganizationScope::hasOrganizationScope);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,10 +28,9 @@ import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
|
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
|
@ -48,9 +47,8 @@ import org.keycloak.models.OrganizationDomainModel;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
@ -59,8 +57,6 @@ import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
public class Organizations {
|
public class Organizations {
|
||||||
|
|
||||||
private static final Pattern SCOPE_PATTERN = Pattern.compile("organization:*".replace("*", "(.*)"));
|
|
||||||
|
|
||||||
public static boolean canManageOrganizationGroup(KeycloakSession session, GroupModel group) {
|
public static boolean canManageOrganizationGroup(KeycloakSession session, GroupModel group) {
|
||||||
if (!Type.ORGANIZATION.equals(group.getType())) {
|
if (!Type.ORGANIZATION.equals(group.getType())) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -257,10 +253,21 @@ public class Organizations {
|
||||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
Optional<OrganizationModel> organization = ofNullable(authSession.getAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE)).map(provider::getById);
|
OrganizationModel organization = ofNullable(authSession.getAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE))
|
||||||
|
.map(provider::getById)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
String rawScopes = authSession.getClientNote(OAuth2Constants.SCOPE);
|
||||||
|
OrganizationScope scope = OrganizationScope.valueOfScope(rawScopes);
|
||||||
|
|
||||||
if (organization.isPresent()) {
|
if (OrganizationScope.SINGLE.equals(scope)) {
|
||||||
return organization.get();
|
return scope.resolveOrganizations(user, rawScopes, session).findAny().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization != null) {
|
||||||
|
return organization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,31 +294,7 @@ public class Organizations {
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OrganizationModel resolveOrganizationFromScopeParam(KeycloakSession session, String scopeParam) {
|
public static OrganizationProvider getProvider(KeycloakSession session) {
|
||||||
return TokenManager.parseScopeParameter(scopeParam)
|
|
||||||
.map((s) -> resolveOrganizationFromScope(session, s))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.findAny()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OrganizationModel resolveOrganizationFromScope(KeycloakSession session, String scope) {
|
|
||||||
if (scope == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Matcher matcher = SCOPE_PATTERN.matcher(scope);
|
|
||||||
|
|
||||||
if (matcher.matches()) {
|
|
||||||
return Optional.ofNullable(getProvider(session).getByAlias(matcher.group(1)))
|
|
||||||
.filter(OrganizationModel::isEnabled)
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OrganizationProvider getProvider(KeycloakSession session) {
|
|
||||||
return session.getProvider(OrganizationProvider.class);
|
return session.getProvider(OrganizationProvider.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.http.HttpRequest;
|
import org.keycloak.http.HttpRequest;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
|
@ -61,7 +62,7 @@ import org.keycloak.models.light.LightweightUserAdapter;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.SessionExpirationUtils;
|
import org.keycloak.models.utils.SessionExpirationUtils;
|
||||||
import org.keycloak.models.utils.RoleUtils;
|
import org.keycloak.models.utils.RoleUtils;
|
||||||
import org.keycloak.organization.utils.Organizations;
|
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
|
||||||
import org.keycloak.protocol.ProtocolMapper;
|
import org.keycloak.protocol.ProtocolMapper;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
|
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
|
||||||
|
@ -587,13 +588,14 @@ public class TokenManager {
|
||||||
clientSession.setRedirectUri(authSession.getRedirectUri());
|
clientSession.setRedirectUri(authSession.getRedirectUri());
|
||||||
clientSession.setProtocol(authSession.getProtocol());
|
clientSession.setProtocol(authSession.getProtocol());
|
||||||
|
|
||||||
|
String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
|
||||||
Set<ClientScopeModel> clientScopes;
|
Set<ClientScopeModel> clientScopes;
|
||||||
|
|
||||||
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
||||||
clientScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, authSession.getClientNote(OAuth2Constants.SCOPE))
|
clientScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
} else {
|
} else {
|
||||||
clientScopes = authSession.getClientScopes().stream()
|
clientScopes = getRequestedClientScopes(session, scopeParam, client, userSession.getUser())
|
||||||
.map(id -> KeycloakModelUtils.findClientScopeById(realm, client, id))
|
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -664,7 +666,7 @@ public class TokenManager {
|
||||||
|
|
||||||
|
|
||||||
/** Return client itself + all default client scopes of client + optional client scopes requested by scope parameter **/
|
/** Return client itself + all default client scopes of client + optional client scopes requested by scope parameter **/
|
||||||
public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession session, String scopeParam, ClientModel client) {
|
public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession session, String scopeParam, ClientModel client, UserModel user) {
|
||||||
// Add all default client scopes automatically and client itself
|
// Add all default client scopes automatically and client itself
|
||||||
Stream<ClientScopeModel> clientScopes = Stream.concat(
|
Stream<ClientScopeModel> clientScopes = Stream.concat(
|
||||||
client.getClientScopes(true).values().stream(),
|
client.getClientScopes(true).values().stream(),
|
||||||
|
@ -674,7 +676,6 @@ public class TokenManager {
|
||||||
return clientScopes;
|
return clientScopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean orgEnabled = Organizations.isEnabledAndOrganizationsPresent(session);
|
|
||||||
Map<String, ClientScopeModel> allOptionalScopes = client.getClientScopes(false);
|
Map<String, ClientScopeModel> allOptionalScopes = client.getClientScopes(false);
|
||||||
|
|
||||||
// Add optional client scopes requested by scope parameter
|
// Add optional client scopes requested by scope parameter
|
||||||
|
@ -686,23 +687,26 @@ public class TokenManager {
|
||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgEnabled) {
|
return tryResolveDynamicClientScope(session, scopeParam, client, user, name);
|
||||||
OrganizationModel organization = Organizations.resolveOrganizationFromScope(session, name);
|
|
||||||
|
|
||||||
if (organization != null) {
|
|
||||||
ClientScopeModel orgScope = allOptionalScopes.get(OIDCLoginProtocolFactory.ORGANIZATION);
|
|
||||||
return Optional.ofNullable(orgScope)
|
|
||||||
.map((s) -> new ClientScopeDecorator(s, name))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
})
|
||||||
.filter(Objects::nonNull),
|
.filter(Objects::nonNull),
|
||||||
clientScopes).distinct();
|
clientScopes).distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ClientScopeModel tryResolveDynamicClientScope(KeycloakSession session, String scopeParam, ClientModel client, UserModel user, String name) {
|
||||||
|
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||||
|
OrganizationScope orgScope = OrganizationScope.valueOfScope(scopeParam);
|
||||||
|
|
||||||
|
if (orgScope == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgScope.toClientScope(name, user, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that all the ClientScopes that have been parsed into authorization_resources are actually in the requested scopes
|
* Check that all the ClientScopes that have been parsed into authorization_resources are actually in the requested scopes
|
||||||
* otherwise, the scope wasn't parsed correctly
|
* otherwise, the scope wasn't parsed correctly
|
||||||
|
@ -711,7 +715,7 @@ public class TokenManager {
|
||||||
* @param client
|
* @param client
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isValidScope(KeycloakSession session, String scopes, AuthorizationRequestContext authorizationRequestContext, ClientModel client) {
|
public static boolean isValidScope(KeycloakSession session, String scopes, AuthorizationRequestContext authorizationRequestContext, ClientModel client, UserModel user) {
|
||||||
if (scopes == null) {
|
if (scopes == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -730,7 +734,7 @@ public class TokenManager {
|
||||||
|
|
||||||
if (authorizationRequestContext == null) {
|
if (authorizationRequestContext == null) {
|
||||||
// only true when dynamic scopes feature is enabled
|
// only true when dynamic scopes feature is enabled
|
||||||
clientScopes = getRequestedClientScopes(session, scopes, client)
|
clientScopes = getRequestedClientScopes(session, scopes, client, user)
|
||||||
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
|
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
|
||||||
.map(ClientScopeModel::getName)
|
.map(ClientScopeModel::getName)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
@ -753,19 +757,7 @@ public class TokenManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var orgEnabled = Organizations.isEnabledAndOrganizationsPresent(session);
|
|
||||||
|
|
||||||
for (String requestedScope : rawScopes) {
|
for (String requestedScope : rawScopes) {
|
||||||
if (orgEnabled) {
|
|
||||||
OrganizationModel organization = Organizations.resolveOrganizationFromScope(session, requestedScope);
|
|
||||||
|
|
||||||
if (organization != null) {
|
|
||||||
// propagate the organization to authenticators to provide a hint about the organization the client wants to authenticate
|
|
||||||
session.setAttribute(OrganizationModel.class.getName(), organization);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we also check dynamic scopes in case the client is from a provider that dynamically provides scopes to their clients
|
// we also check dynamic scopes in case the client is from a provider that dynamically provides scopes to their clients
|
||||||
if (!clientScopes.contains(requestedScope) && client.getDynamicClientScope(requestedScope) == null) {
|
if (!clientScopes.contains(requestedScope) && client.getDynamicClientScope(requestedScope) == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -775,8 +767,8 @@ public class TokenManager {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isValidScope(KeycloakSession session, String scopes, ClientModel client) {
|
public static boolean isValidScope(KeycloakSession session, String scopes, ClientModel client, UserModel user) {
|
||||||
return isValidScope(session, scopes, null, client);
|
return isValidScope(session, scopes, null, client, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<String> parseScopeParameter(String scopeParam) {
|
public static Stream<String> parseScopeParameter(String scopeParam) {
|
||||||
|
|
|
@ -226,9 +226,9 @@ public class AuthorizationEndpointChecker {
|
||||||
public void checkValidScope() throws AuthorizationCheckException {
|
public void checkValidScope() throws AuthorizationCheckException {
|
||||||
boolean validScopes;
|
boolean validScopes;
|
||||||
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
||||||
validScopes = TokenManager.isValidScope(session, request.getScope(), request.getAuthorizationRequestContext(), client);
|
validScopes = TokenManager.isValidScope(session, request.getScope(), request.getAuthorizationRequestContext(), client, null);
|
||||||
} else {
|
} else {
|
||||||
validScopes = TokenManager.isValidScope(session, request.getScope(), client);
|
validScopes = TokenManager.isValidScope(session, request.getScope(), client, null);
|
||||||
}
|
}
|
||||||
if (!validScopes) {
|
if (!validScopes) {
|
||||||
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.SCOPE_PARAM);
|
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.SCOPE_PARAM);
|
||||||
|
|
|
@ -187,7 +187,7 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
|
||||||
// Compute client scopes again from scope parameter. Check if user still has them granted
|
// Compute client scopes again from scope parameter. Check if user still has them granted
|
||||||
// (but in code-to-token request, it could just theoretically happen that they are not available)
|
// (but in code-to-token request, it could just theoretically happen that they are not available)
|
||||||
String scopeParam = codeData.getScope();
|
String scopeParam = codeData.getScope();
|
||||||
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(session, scopeParam, client);
|
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(session, scopeParam, client, user);
|
||||||
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) {
|
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) {
|
||||||
String errorMessage = "Client no longer has requested consent from user";
|
String errorMessage = "Client no longer has requested consent from user";
|
||||||
event.detail(Details.REASON, errorMessage);
|
event.detail(Details.REASON, errorMessage);
|
||||||
|
|
|
@ -206,9 +206,9 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
|
||||||
boolean validScopes;
|
boolean validScopes;
|
||||||
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
|
||||||
AuthorizationRequestContext authorizationRequestContext = AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, scope);
|
AuthorizationRequestContext authorizationRequestContext = AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, scope);
|
||||||
validScopes = TokenManager.isValidScope(session, scope, authorizationRequestContext, client);
|
validScopes = TokenManager.isValidScope(session, scope, authorizationRequestContext, client, null);
|
||||||
} else {
|
} else {
|
||||||
validScopes = TokenManager.isValidScope(session, scope, client);
|
validScopes = TokenManager.isValidScope(session, scope, client, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validScopes) {
|
if (!validScopes) {
|
||||||
|
|
|
@ -188,7 +188,7 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
|
||||||
|
|
||||||
if (!TokenManager
|
if (!TokenManager
|
||||||
.verifyConsentStillAvailable(session,
|
.verifyConsentStillAvailable(session,
|
||||||
user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client))) {
|
user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client, user))) {
|
||||||
String errorMessage = "Client no longer has requested consent from user";
|
String errorMessage = "Client no longer has requested consent from user";
|
||||||
event.detail(Details.REASON, errorMessage);
|
event.detail(Details.REASON, errorMessage);
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
|
|
@ -178,7 +178,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : scope",
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : scope",
|
||||||
Response.Status.BAD_REQUEST);
|
Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
if (!TokenManager.isValidScope(session, scope, client)) {
|
if (!TokenManager.isValidScope(session, scope, client, user)) {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid scopes: " + scope,
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid scopes: " + scope,
|
||||||
Response.Status.BAD_REQUEST);
|
Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,7 +336,7 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
|
||||||
// Compute client scopes again from scope parameter. Check if user still has them granted
|
// Compute client scopes again from scope parameter. Check if user still has them granted
|
||||||
// (but in device_code-to-token request, it could just theoretically happen that they are not available)
|
// (but in device_code-to-token request, it could just theoretically happen that they are not available)
|
||||||
String scopeParam = deviceCodeModel.getScope();
|
String scopeParam = deviceCodeModel.getScope();
|
||||||
if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client))) {
|
if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client, user))) {
|
||||||
String errorMessage = "Client no longer has requested consent from user";
|
String errorMessage = "Client no longer has requested consent from user";
|
||||||
event.detail(Details.REASON, errorMessage);
|
event.detail(Details.REASON, errorMessage);
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
|
|
@ -1217,7 +1217,7 @@ public class AuthenticationManager {
|
||||||
// todo scope param protocol independent
|
// todo scope param protocol independent
|
||||||
String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
|
String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
|
||||||
|
|
||||||
Set<String> requestedClientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client)
|
Set<String> requestedClientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client, user)
|
||||||
.map(ClientScopeModel::getId).collect(Collectors.toSet());
|
.map(ClientScopeModel::getId).collect(Collectors.toSet());
|
||||||
|
|
||||||
authSession.setClientScopes(requestedClientScopes);
|
authSession.setClientScopes(requestedClientScopes);
|
||||||
|
|
|
@ -129,7 +129,7 @@ public class ClientScopeEvaluateResource {
|
||||||
public Stream<ProtocolMapperEvaluationRepresentation> getGrantedProtocolMappers(@QueryParam("scope") String scopeParam) {
|
public Stream<ProtocolMapperEvaluationRepresentation> getGrantedProtocolMappers(@QueryParam("scope") String scopeParam) {
|
||||||
auth.clients().requireView(client);
|
auth.clients().requireView(client);
|
||||||
|
|
||||||
return TokenManager.getRequestedClientScopes(session, scopeParam, client)
|
return TokenManager.getRequestedClientScopes(session, scopeParam, client, null)
|
||||||
.flatMap(mapperContainer -> mapperContainer.getProtocolMappersStream()
|
.flatMap(mapperContainer -> mapperContainer.getProtocolMappersStream()
|
||||||
.filter(current -> isEnabled(session, current) && Objects.equals(current.getProtocol(), client.getProtocol()))
|
.filter(current -> isEnabled(session, current) && Objects.equals(current.getProtocol(), client.getProtocol()))
|
||||||
.map(current -> toProtocolMapperEvaluationRepresentation(current, mapperContainer)));
|
.map(current -> toProtocolMapperEvaluationRepresentation(current, mapperContainer)));
|
||||||
|
|
|
@ -112,7 +112,7 @@ public class ClientScopeEvaluateScopeMappingsResource {
|
||||||
return roleContainer.getRolesStream();
|
return roleContainer.getRolesStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client)
|
Set<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client, null)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
Predicate<RoleModel> hasClientScope = role ->
|
Predicate<RoleModel> hasClientScope = role ->
|
||||||
|
|
|
@ -92,7 +92,7 @@ public class DefaultClientSessionContext implements ClientSessionContext {
|
||||||
session.getContext().setClient(clientSession.getClient());
|
session.getContext().setClient(clientSession.getClient());
|
||||||
requestedScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
|
requestedScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
|
||||||
} else {
|
} else {
|
||||||
requestedScopes = TokenManager.getRequestedClientScopes(session, scopeParam, clientSession.getClient());
|
requestedScopes = TokenManager.getRequestedClientScopes(session, scopeParam, clientSession.getClient(), clientSession.getUserSession().getUser());
|
||||||
}
|
}
|
||||||
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), session);
|
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), session);
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||||
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||||
ClientModel client = authenticationSession.getClient();
|
ClientModel client = authenticationSession.getClient();
|
||||||
|
|
||||||
return getRequestedClientScopes(session, requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
|
return getRequestedClientScopes(session, requestedScopesString, client, context.getUser()).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
|
|
@ -18,37 +18,19 @@
|
||||||
package org.keycloak.testsuite.organization.authentication;
|
package org.keycloak.testsuite.organization.authentication;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
|
||||||
import org.keycloak.common.util.UriUtils;
|
|
||||||
import org.keycloak.models.OrganizationModel;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.RefreshToken;
|
|
||||||
import org.keycloak.representations.idm.MemberRepresentation;
|
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
|
|
||||||
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
||||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
|
public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
|
||||||
|
@ -123,67 +105,4 @@ public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
|
||||||
appPage.assertCurrent();
|
appPage.assertCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
|
||||||
public void testOrganizationScopeMapsSpecificOrganization() {
|
|
||||||
OrganizationRepresentation orgA = createOrganization("orga", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
|
||||||
MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName());
|
|
||||||
OrganizationRepresentation orgB = createOrganization("orgb", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
|
||||||
testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close();
|
|
||||||
|
|
||||||
// resolve organization based on the organization scope value
|
|
||||||
oauth.clientId("broker-app");
|
|
||||||
oauth.scope("organization:" + orgA.getAlias());
|
|
||||||
loginPage.open(bc.consumerRealmName());
|
|
||||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
|
||||||
Assert.assertTrue(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
|
||||||
Assert.assertFalse(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
|
||||||
|
|
||||||
// identity-first login will respect the organization provided in the scope even though the user email maps to a different organization
|
|
||||||
oauth.clientId("broker-app");
|
|
||||||
String orgScope = "organization:" + orgB.getAlias();
|
|
||||||
oauth.scope(orgScope);
|
|
||||||
loginPage.open(bc.consumerRealmName());
|
|
||||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
|
||||||
Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
|
||||||
Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
|
||||||
loginPage.loginUsername(member.getEmail());
|
|
||||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
|
||||||
Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
|
||||||
Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
|
||||||
loginPage.login(memberPassword);
|
|
||||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
|
||||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
|
||||||
assertThat(response.getScope(), containsString(orgScope));
|
|
||||||
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
|
||||||
assertThat(accessToken.getScope(), containsString(orgScope));
|
|
||||||
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
|
||||||
Map<String, Object> organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
|
||||||
assertThat(organizations.size(), is(1));
|
|
||||||
assertThat(organizations.containsKey(orgB.getAlias()), is(true));
|
|
||||||
assertThat(response.getRefreshToken(), notNullValue());
|
|
||||||
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
|
||||||
assertThat(refreshToken.getScope(), containsString(orgScope));
|
|
||||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
|
||||||
assertThat(response.getScope(), containsString(orgScope));
|
|
||||||
accessToken = oauth.verifyToken(response.getAccessToken());
|
|
||||||
assertThat(accessToken.getScope(), containsString(orgScope));
|
|
||||||
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
|
||||||
organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
|
||||||
assertThat(organizations.size(), is(1));
|
|
||||||
assertThat(organizations.containsKey(orgB.getAlias()), is(true));
|
|
||||||
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
|
||||||
assertThat(refreshToken.getScope(), containsString(orgScope));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidOrganizationScope() throws MalformedURLException {
|
|
||||||
oauth.clientId("broker-app");
|
|
||||||
oauth.scope("organization:unknown");
|
|
||||||
oauth.realm(TEST_REALM_NAME);
|
|
||||||
oauth.openLoginForm();
|
|
||||||
MultivaluedHashMap<String, String> queryParams = UriUtils.decodeQueryString(new URL(driver.getCurrentUrl()).getQuery());
|
|
||||||
assertEquals("invalid_scope", queryParams.getFirst("error"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,18 @@ package org.keycloak.testsuite.organization.mapper;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
@ -34,22 +39,30 @@ import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
|
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
|
||||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.MemberRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
|
||||||
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest {
|
public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClaim() throws Exception {
|
public void testPasswordGrantType() throws Exception {
|
||||||
OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
|
OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
|
||||||
OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
|
OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
|
||||||
|
|
||||||
|
@ -62,10 +75,8 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
||||||
Assert.assertTrue(orga.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
|
Assert.assertTrue(orga.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
|
||||||
Assert.assertTrue(orgb.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
|
Assert.assertTrue(orgb.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
|
||||||
|
|
||||||
member = getUserRepresentation(memberEmail);
|
|
||||||
|
|
||||||
oauth.clientId("direct-grant");
|
oauth.clientId("direct-grant");
|
||||||
oauth.scope("openid organization");
|
oauth.scope("openid organization:*");
|
||||||
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword);
|
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword);
|
||||||
assertThat(response.getScope(), containsString("organization"));
|
assertThat(response.getScope(), containsString("organization"));
|
||||||
|
|
||||||
|
@ -99,8 +110,135 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
||||||
assertThat(accessToken.getOtherClaims().get("groups"), nullValue());
|
assertThat(accessToken.getOtherClaims().get("groups"), nullValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@SuppressWarnings("unchecked")
|
||||||
private static ProtocolMapperRepresentation createGroupMapper() {
|
@Test
|
||||||
|
public void testOrganizationScopeMapsSpecificOrganization() {
|
||||||
|
driver.manage().timeouts().pageLoadTimeout(Duration.ofDays(1));
|
||||||
|
OrganizationRepresentation orgA = createOrganization("orga", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName());
|
||||||
|
OrganizationRepresentation orgB = createOrganization("orgb", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close();
|
||||||
|
|
||||||
|
// resolve organization based on the organization scope value
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
oauth.scope("organization:" + orgA.getAlias());
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
||||||
|
|
||||||
|
// identity-first login will respect the organization provided in the scope even though the user email maps to a different organization
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
String orgScope = "organization:" + orgB.getAlias();
|
||||||
|
oauth.scope(orgScope);
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
||||||
|
loginPage.loginUsername(member.getEmail());
|
||||||
|
org.keycloak.testsuite.Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
||||||
|
loginPage.login(memberPassword);
|
||||||
|
assertScopeAndClaims(orgScope, orgB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrganizationScopeMapsAllOrganizations() {
|
||||||
|
OrganizationRepresentation orgA = createOrganization("orga", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName());
|
||||||
|
OrganizationRepresentation orgB = createOrganization("orgb", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close();
|
||||||
|
|
||||||
|
// resolve organization based on the organization scope value
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
oauth.scope("organization:" + orgA.getAlias());
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
org.keycloak.testsuite.Assert.assertTrue(loginPage.isSocialButtonPresent(orgA.getAlias() + "-identity-provider"));
|
||||||
|
org.keycloak.testsuite.Assert.assertFalse(loginPage.isSocialButtonPresent(orgB.getAlias() + "-identity-provider"));
|
||||||
|
|
||||||
|
// identity-first login will respect the organization provided in the scope even though the user email maps to a different organization
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
String orgScope = "organization:*";
|
||||||
|
oauth.scope(orgScope);
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
loginPage.loginUsername(member.getEmail());
|
||||||
|
loginPage.login(memberPassword);
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
||||||
|
assertThat(response.getScope(), containsString(orgScope));
|
||||||
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
assertThat(accessToken.getScope(), containsString(orgScope));
|
||||||
|
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||||
|
Map<String, Object> organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
||||||
|
assertThat(organizations.size(), is(2));
|
||||||
|
assertThat(organizations.containsKey(orgA.getAlias()), is(true));
|
||||||
|
assertThat(organizations.containsKey(orgB.getAlias()), is(true));
|
||||||
|
assertThat(response.getRefreshToken(), notNullValue());
|
||||||
|
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
assertThat(refreshToken.getScope(), containsString(orgScope));
|
||||||
|
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
||||||
|
assertThat(response.getScope(), containsString(orgScope));
|
||||||
|
accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
assertThat(accessToken.getScope(), containsString(orgScope));
|
||||||
|
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||||
|
organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
||||||
|
assertThat(organizations.size(), is(2));
|
||||||
|
assertThat(organizations.containsKey(orgA.getAlias()), is(true));
|
||||||
|
assertThat(organizations.containsKey(orgB.getAlias()), is(true));
|
||||||
|
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
assertThat(refreshToken.getScope(), containsString(orgScope));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrganizationScopeAnyMapsSingleOrganization() {
|
||||||
|
OrganizationRepresentation orgA = createOrganization("orga", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName());
|
||||||
|
|
||||||
|
// resolve organization based on the organization scope value
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
String orgScope = "organization";
|
||||||
|
oauth.scope(orgScope);
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
loginPage.loginUsername(member.getEmail());
|
||||||
|
loginPage.login(memberPassword);
|
||||||
|
|
||||||
|
assertScopeAndClaims(orgScope, orgA);
|
||||||
|
String code;
|
||||||
|
AccessTokenResponse response;
|
||||||
|
AccessToken accessToken;
|
||||||
|
|
||||||
|
UserRepresentation account = getUserRepresentation(member.getEmail());
|
||||||
|
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
|
||||||
|
realmsResouce().realm(bc.providerRealmName()).logoutAll();
|
||||||
|
|
||||||
|
OrganizationRepresentation orgB = createOrganization("orgb", Map.of(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()));
|
||||||
|
testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close();
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
loginPage.loginUsername(member.getEmail());
|
||||||
|
loginPage.login(memberPassword);
|
||||||
|
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
||||||
|
// for now, return the organization scope in the response and access token even though no organization is mapped into the token
|
||||||
|
// once we support the user to select an organization, the selected organization will be mapped
|
||||||
|
assertThat(response.getScope(), containsString(orgScope));
|
||||||
|
accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
assertThat(accessToken.getScope(), containsString(orgScope));
|
||||||
|
assertThat(accessToken.getOtherClaims().keySet(), not(hasItem(OAuth2Constants.ORGANIZATION)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidOrganizationScope() throws MalformedURLException {
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
oauth.scope("organization:unknown");
|
||||||
|
oauth.realm(TEST_REALM_NAME);
|
||||||
|
oauth.openLoginForm();
|
||||||
|
MultivaluedHashMap<String, String> queryParams = UriUtils.decodeQueryString(new URL(driver.getCurrentUrl()).getQuery());
|
||||||
|
assertEquals("invalid_scope", queryParams.getFirst("error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProtocolMapperRepresentation createGroupMapper() {
|
||||||
ProtocolMapperRepresentation groupMapper = new ProtocolMapperRepresentation();
|
ProtocolMapperRepresentation groupMapper = new ProtocolMapperRepresentation();
|
||||||
groupMapper.setName("groups");
|
groupMapper.setName("groups");
|
||||||
groupMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
|
groupMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
|
||||||
|
@ -112,4 +250,29 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
||||||
groupMapper.setConfig(config);
|
groupMapper.setConfig(config);
|
||||||
return groupMapper;
|
return groupMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertScopeAndClaims(String orgScope, OrganizationRepresentation orgA) {
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
||||||
|
assertThat(response.getScope(), containsString(orgScope));
|
||||||
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
assertThat(accessToken.getScope(), containsString(orgScope));
|
||||||
|
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||||
|
Map<String, Object> organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
||||||
|
assertThat(organizations.size(), is(1));
|
||||||
|
assertThat(organizations.containsKey(orgA.getAlias()), is(true));
|
||||||
|
assertThat(response.getRefreshToken(), notNullValue());
|
||||||
|
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
assertThat(refreshToken.getScope(), containsString(orgScope));
|
||||||
|
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET);
|
||||||
|
assertThat(response.getScope(), containsString(orgScope));
|
||||||
|
accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
assertThat(accessToken.getScope(), containsString(orgScope));
|
||||||
|
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||||
|
organizations = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
||||||
|
assertThat(organizations.size(), is(1));
|
||||||
|
assertThat(organizations.containsKey(orgA.getAlias()), is(true));
|
||||||
|
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
assertThat(refreshToken.getScope(), containsString(orgScope));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue