Support for resolving organization based on the organization scope

Closes #31438

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-08-13 18:14:00 -03:00
parent 310824cc2b
commit 96acc62c00
30 changed files with 666 additions and 253 deletions

View file

@ -0,0 +1,207 @@
/*
* 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.models;
import java.util.Map;
import java.util.stream.Stream;
public class ClientScopeDecorator implements ClientScopeModel {
private final ClientScopeModel delegate;
private final String name;
public ClientScopeDecorator(ClientScopeModel delegate, String name) {
this.delegate = delegate;
this.name = name;
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public String getName() {
return name;
}
@Override
public RealmModel getRealm() {
return delegate.getRealm();
}
@Override
public void setName(String name) {
delegate.setName(name);
}
@Override
public String getDescription() {
return delegate.getDescription();
}
@Override
public void setDescription(String description) {
delegate.setDescription(description);
}
@Override
public String getProtocol() {
return delegate.getProtocol();
}
@Override
public void setProtocol(String protocol) {
delegate.setProtocol(protocol);
}
@Override
public void setAttribute(String name, String value) {
delegate.setAttribute(name, value);
}
@Override
public void removeAttribute(String name) {
delegate.removeAttribute(name);
}
@Override
public String getAttribute(String name) {
return delegate.getAttribute(name);
}
@Override
public Map<String, String> getAttributes() {
return delegate.getAttributes();
}
@Override
public boolean isDisplayOnConsentScreen() {
return delegate.isDisplayOnConsentScreen();
}
@Override
public void setDisplayOnConsentScreen(boolean displayOnConsentScreen) {
delegate.setDisplayOnConsentScreen(displayOnConsentScreen);
}
@Override
public String getConsentScreenText() {
return delegate.getConsentScreenText();
}
@Override
public void setConsentScreenText(String consentScreenText) {
delegate.setConsentScreenText(consentScreenText);
}
@Override
public String getGuiOrder() {
return delegate.getGuiOrder();
}
@Override
public void setGuiOrder(String guiOrder) {
delegate.setGuiOrder(guiOrder);
}
@Override
public boolean isIncludeInTokenScope() {
return delegate.isIncludeInTokenScope();
}
@Override
public void setIncludeInTokenScope(boolean includeInTokenScope) {
delegate.setIncludeInTokenScope(includeInTokenScope);
}
@Override
public boolean isDynamicScope() {
return delegate.isDynamicScope();
}
@Override
public void setIsDynamicScope(boolean isDynamicScope) {
delegate.setIsDynamicScope(isDynamicScope);
}
@Override
public String getDynamicScopeRegexp() {
return delegate.getDynamicScopeRegexp();
}
@Override
public Stream<ProtocolMapperModel> getProtocolMappersStream() {
return delegate.getProtocolMappersStream();
}
@Override
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
return delegate.addProtocolMapper(model);
}
@Override
public void removeProtocolMapper(ProtocolMapperModel mapping) {
delegate.removeProtocolMapper(mapping);
}
@Override
public void updateProtocolMapper(ProtocolMapperModel mapping) {
delegate.updateProtocolMapper(mapping);
}
@Override
public ProtocolMapperModel getProtocolMapperById(String id) {
return delegate.getProtocolMapperById(id);
}
@Override
public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
return delegate.getProtocolMapperByName(protocol, name);
}
@Override
public Stream<RoleModel> getScopeMappingsStream() {
return delegate.getScopeMappingsStream();
}
@Override
public Stream<RoleModel> getRealmScopeMappingsStream() {
return delegate.getRealmScopeMappingsStream();
}
@Override
public void addScopeMapping(RoleModel role) {
delegate.addScopeMapping(role);
}
@Override
public void deleteScopeMapping(RoleModel role) {
delegate.deleteScopeMapping(role);
}
@Override
public boolean hasDirectScope(RoleModel role) {
return delegate.hasDirectScope(role);
}
@Override
public boolean hasScope(RoleModel role) {
return delegate.hasScope(role);
}
}

View file

@ -232,4 +232,14 @@ public interface OrganizationProvider extends Provider {
* @return long Number of organizations
*/
long count();
/**
* Returns an {@link OrganizationModel} with the given {@code alias}.
*
* @param alias the alias
* @return the organization
*/
default OrganizationModel getByAlias(String alias) {
return getAllStream(Map.of(OrganizationModel.ALIAS, alias), 0, 1).findAny().orElse(null);
}
}

View file

@ -1170,7 +1170,7 @@ public class AuthenticationProcessor {
protected Response authenticationComplete() {
// attachSession(); // Session will be attached after requiredActions + consents are finished.
AuthenticationManager.setClientScopesInSession(authenticationSession);
AuthenticationManager.setClientScopesInSession(session, authenticationSession);
String nextRequiredAction = nextRequiredAction();
if (nextRequiredAction != null) {

View file

@ -142,11 +142,11 @@ public class PolicyEvaluationService {
private EvaluationDecisionCollector evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext, AuthorizationRequest request) {
List<ResourcePermission> permissions = createPermissions(evaluationRequest, evaluationContext, authorization, request);
if (permissions.isEmpty()) {
return authorization.evaluators().from(evaluationContext, resourceServer, request).evaluate(new EvaluationDecisionCollector(authorization, resourceServer, request));
}
return authorization.evaluators().from(permissions, evaluationContext).evaluate(new EvaluationDecisionCollector(authorization, resourceServer, request));
}
@ -261,7 +261,7 @@ public class PolicyEvaluationService {
UserSessionModel userSession = null;
if (subject != null) {
UserModel userModel = keycloakSession.users().getUserById(realm, subject);
if (userModel == null) {
userModel = keycloakSession.users().getUserByUsername(realm, subject);
}
@ -283,7 +283,7 @@ public class PolicyEvaluationService {
userSession = new UserSessionManager(keycloakSession).createUserSession(authSession.getParentSession().getId(), realm, userModel,
userModel.getUsername(), "127.0.0.1", "passwd", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(keycloakSession, authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(keycloakSession, userSession, authSession);
accessToken = new TokenManager().createClientAccessToken(keycloakSession, realm, clientModel, userModel, userSession, clientSessionCtx);
@ -350,4 +350,4 @@ public class PolicyEvaluationService {
return results.values();
}
}
}
}

View file

@ -161,7 +161,7 @@ public class AuthorizationTokenService {
}
KeycloakIdentity identity;
try {
identity = new KeycloakIdentity(keycloakSession, idToken);
} catch (Exception cause) {
@ -214,7 +214,7 @@ public class AuthorizationTokenService {
if (identity != null) {
event.user(identity.getId());
}
ResourceServer resourceServer = getResourceServer(ticket, request);
Collection<Permission> permissions;
@ -346,7 +346,7 @@ public class AuthorizationTokenService {
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(keycloakSession.getContext().getUri().getBaseUri(), realm.getName()));
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(keycloakSession, authSession);
clientSessionCtx = TokenManager.attachAuthenticationSession(keycloakSession, userSessionModel, authSession);
} else {
clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, keycloakSession);
@ -726,7 +726,7 @@ public class AuthorizationTokenService {
limit.decrementAndGet();
}
}
return permission;
}
@ -869,7 +869,7 @@ public class AuthorizationTokenService {
} else {
// resource uri and scopes are specified, or only scopes are specified
String[] scopes = parts[1].split(",");
if (uri.isEmpty()) {
// only scopes are specified
addPermission("", scopes);

View file

@ -45,6 +45,7 @@ import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareAuthenticationContextBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareRealmBean;
import org.keycloak.sessions.AuthenticationSessionModel;
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
@ -63,7 +64,16 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return;
}
initialChallenge(context);
OrganizationModel organization = resolveOrganization(session);
if (organization == null) {
initialChallenge(context);
} else {
// make sure the organization is set to the auth session to remember it when processing subsequent requests
AuthenticationSessionModel authSession = context.getAuthenticationSession();
authSession.setAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId());
action(context);
}
}
@Override
@ -148,6 +158,10 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
}
private UserModel resolveUser(AuthenticationFlowContext context, String username) {
if (username == null) {
return null;
}
UserProvider users = session.users();
RealmModel realm = session.getContext().getRealm();
UserModel user = Optional.ofNullable(users.getUserByEmail(realm, username)).orElseGet(() -> users.getUserByUsername(realm, username));

View file

@ -33,6 +33,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
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.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
@ -78,18 +79,26 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
}
@Override
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
OrganizationProvider provider = keycloakSession.getProvider(OrganizationProvider.class);
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
if (!isEnabledAndOrganizationsPresent(provider)) {
return;
}
OrganizationModel organization = Organizations.resolveOrganizationFromScopeParam(session, clientSessionCtx.getScopeString());
UserModel user = userSession.getUser();
Stream<OrganizationModel> organizations = provider.getByMember(user).filter(OrganizationModel::isEnabled);
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<>();
organizations.forEach(organization -> claim.put(organization.getAlias(), Map.of()));
organizations.forEach(o -> claim.put(o.getAlias(), Map.of()));
if (claim.isEmpty()) {
return;

View file

@ -28,6 +28,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.keycloak.TokenVerifier;
@ -46,14 +48,19 @@ import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.utils.StringUtil;
public class Organizations {
private static final Pattern SCOPE_PATTERN = Pattern.compile("organization:*".replace("*", "(.*)"));
public static boolean canManageOrganizationGroup(KeycloakSession session, GroupModel group) {
if (!Type.ORGANIZATION.equals(group.getType())) {
return true;
@ -69,7 +76,7 @@ public class Organizations {
}
public static List<IdentityProviderModel> resolveHomeBroker(KeycloakSession session, UserModel user) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
OrganizationProvider provider = getProvider(session);
RealmModel realm = session.getContext().getRealm();
List<OrganizationModel> organizations = Optional.ofNullable(user).stream().flatMap(provider::getByMember)
.filter(OrganizationModel::isEnabled)
@ -117,7 +124,7 @@ public class Organizations {
OrganizationModel current = resolveOrganization(session);
try {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
OrganizationProvider provider = getProvider(session);
session.setAttribute(OrganizationModel.class.getName(), provider.getById(group.getName()));
@ -136,6 +143,16 @@ public class Organizations {
return orgProvider != null && orgProvider.isEnabled() && orgProvider.count() != 0;
}
public static boolean isEnabledAndOrganizationsPresent(KeycloakSession session) {
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
return false;
}
OrganizationProvider provider = getProvider(session);
return isEnabledAndOrganizationsPresent(provider);
}
public static void checkEnabled(OrganizationProvider provider) {
if (provider == null || !provider.isEnabled()) {
throw ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND);
@ -236,14 +253,23 @@ public class Organizations {
}
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user, String domain) {
OrganizationProvider provider = getProvider(session);
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
if (authSession != null) {
Optional<OrganizationModel> organization = ofNullable(authSession.getAuthNote(OrganizationModel.ORGANIZATION_ATTRIBUTE)).map(provider::getById);
if (organization.isPresent()) {
return organization.get();
}
}
Optional<OrganizationModel> organization = Optional.ofNullable((OrganizationModel) session.getAttribute(OrganizationModel.class.getName()));
if (organization.isPresent()) {
return organization.get();
}
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
organization = ofNullable(user).stream().flatMap(provider::getByMember)
.filter(o -> o.isEnabled() && provider.isManagedMember(o, user))
.findAny();
@ -260,4 +286,32 @@ public class Organizations {
.map(provider::getByDomainName)
.orElse(null);
}
public static OrganizationModel resolveOrganizationFromScopeParam(KeycloakSession session, String scopeParam) {
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);
}
}

View file

@ -132,7 +132,7 @@ public abstract class AuthorizationEndpointBase {
}
}
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
if (processor.nextRequiredAction() != null) {
Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED);
@ -224,4 +224,4 @@ public abstract class AuthorizationEndpointBase {
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
return authSession;
}
}
}

View file

@ -415,7 +415,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
event.session(targetUserSession);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, targetUserSession, authSession);
updateUserSessionFromClientAuth(targetUserSession);
@ -480,7 +480,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
event.session(targetUserSession);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, targetUserSession,
authSession);

View file

@ -46,7 +46,9 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.ClientScopeDecorator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@ -59,6 +61,7 @@ import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SessionExpirationUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
@ -573,23 +576,25 @@ public class TokenManager {
ClientModel client = authSession.getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null && !AuthenticationManager.isClientSessionValid(userSession.getRealm(), client, userSession, clientSession)) {
RealmModel realm = userSession.getRealm();
if (clientSession != null && !AuthenticationManager.isClientSessionValid(realm, client, userSession, clientSession)) {
// session exists but not active so re-start it
clientSession.restartClientSession();
} else if (clientSession == null) {
clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
clientSession = session.sessions().createClientSession(realm, client, userSession);
}
clientSession.setRedirectUri(authSession.getRedirectUri());
clientSession.setProtocol(authSession.getProtocol());
Set<String> clientScopeIds;
Set<ClientScopeModel> clientScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
clientScopeIds = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, authSession.getClientNote(OAuth2Constants.SCOPE))
.map(ClientScopeModel::getId)
clientScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, authSession.getClientNote(OAuth2Constants.SCOPE))
.collect(Collectors.toSet());
} else {
clientScopeIds = authSession.getClientScopes();
clientScopes = authSession.getClientScopes().stream()
.map(id -> KeycloakModelUtils.findClientScopeById(realm, client, id))
.collect(Collectors.toSet());
}
Map<String, String> transferredNotes = authSession.getClientNotes();
@ -605,10 +610,9 @@ public class TokenManager {
clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(session, authSession).getLevelOfAuthenticationFromCurrentAuthentication()));
clientSession.setTimestamp(userSession.getLastSessionRefresh());
// Remove authentication session now (just current tab, not whole "rootAuthenticationSession" in case we have more browser tabs with "authentications in progress")
new AuthenticationSessionManager(session).updateAuthenticationSessionAfterSuccessfulAuthentication(userSession.getRealm(), authSession);
new AuthenticationSessionManager(session).updateAuthenticationSessionAfterSuccessfulAuthentication(realm, authSession);
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopeIds(clientSession, clientScopeIds, session);
return clientSessionCtx;
return DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession, clientScopes, session);
}
@ -660,7 +664,7 @@ public class TokenManager {
/** Return client itself + all default client scopes of client + optional client scopes requested by scope parameter **/
public static Stream<ClientScopeModel> getRequestedClientScopes(String scopeParam, ClientModel client) {
public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession session, String scopeParam, ClientModel client) {
// Add all default client scopes automatically and client itself
Stream<ClientScopeModel> clientScopes = Stream.concat(
client.getClientScopes(true).values().stream(),
@ -670,9 +674,32 @@ public class TokenManager {
return clientScopes;
}
boolean orgEnabled = Organizations.isEnabledAndOrganizationsPresent(session);
Map<String, ClientScopeModel> allOptionalScopes = client.getClientScopes(false);
// Add optional client scopes requested by scope parameter
return Stream.concat(parseScopeParameter(scopeParam).map(allOptionalScopes::get).filter(Objects::nonNull),
return Stream.concat(parseScopeParameter(scopeParam)
.map(name -> {
ClientScopeModel scope = allOptionalScopes.get(name);
if (scope != null) {
return scope;
}
if (orgEnabled) {
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),
clientScopes).distinct();
}
@ -684,59 +711,61 @@ public class TokenManager {
* @param client
* @return
*/
public static boolean isValidScope(String scopes, AuthorizationRequestContext authorizationRequestContext, ClientModel client) {
public static boolean isValidScope(KeycloakSession session, String scopes, AuthorizationRequestContext authorizationRequestContext, ClientModel client) {
if (scopes == null) {
return true;
}
Collection<String> requestedScopes = TokenManager.parseScopeParameter(scopes).collect(Collectors.toSet());
Set<String> rarScopes = authorizationRequestContext.getAuthorizationDetailEntries()
.stream()
.map(AuthorizationDetails::getAuthorizationDetails)
.map(AuthorizationDetailsJSONRepresentation::getScopeNameFromCustomData)
.collect(Collectors.toSet());
Collection<String> rawScopes = TokenManager.parseScopeParameter(scopes).collect(Collectors.toSet());
if (TokenUtil.isOIDCRequest(scopes)) {
requestedScopes.remove(OAuth2Constants.SCOPE_OPENID);
rawScopes.remove(OAuth2Constants.SCOPE_OPENID);
}
if ((authorizationRequestContext.getAuthorizationDetailEntries() == null || authorizationRequestContext.getAuthorizationDetailEntries().isEmpty()) && requestedScopes.size()>0) {
return false;
if (rawScopes.isEmpty()) {
return true;
}
Set<String> clientScopes;
if (authorizationRequestContext == null) {
// only true when dynamic scopes feature is enabled
clientScopes = getRequestedClientScopes(session, scopes, client)
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.map(ClientScopeModel::getName)
.collect(Collectors.toSet());
} else {
List<AuthorizationDetails> details = Optional.ofNullable(authorizationRequestContext.getAuthorizationDetailEntries()).orElse(List.of());
clientScopes = details
.stream()
.map(AuthorizationDetails::getAuthorizationDetails)
.map(AuthorizationDetailsJSONRepresentation::getScopeNameFromCustomData)
.collect(Collectors.toSet());
}
if (logger.isTraceEnabled()) {
logger.tracef("Rar scopes to validate requested scopes against: %1s", String.join(" ", rarScopes));
logger.tracef("Requested scopes: %1s", String.join(" ", requestedScopes));
logger.tracef("Scopes to validate requested scopes against: %1s", String.join(" ", clientScopes));
logger.tracef("Requested scopes: %1s", String.join(" ", rawScopes));
}
for (String requestedScope : requestedScopes) {
// We keep the check to the getDynamicClientScope for the OpenshiftSAClientAdapter
if (!rarScopes.contains(requestedScope) && client.getDynamicClientScope(requestedScope) == null) {
return false;
}
}
return true;
}
public static boolean isValidScope(String scopes, ClientModel client) {
if (scopes == null) {
return true;
}
Set<String> clientScopes = getRequestedClientScopes(scopes, client)
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.map(ClientScopeModel::getName)
.collect(Collectors.toSet());
Collection<String> requestedScopes = TokenManager.parseScopeParameter(scopes).collect(Collectors.toSet());
if (TokenUtil.isOIDCRequest(scopes)) {
requestedScopes.remove(OAuth2Constants.SCOPE_OPENID);
}
if (!requestedScopes.isEmpty() && clientScopes.isEmpty()) {
if (clientScopes.isEmpty()) {
return false;
}
for (String requestedScope : requestedScopes) {
var orgEnabled = Organizations.isEnabledAndOrganizationsPresent(session);
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
if (!clientScopes.contains(requestedScope) && client.getDynamicClientScope(requestedScope) == null) {
return false;
@ -746,6 +775,10 @@ public class TokenManager {
return true;
}
public static boolean isValidScope(KeycloakSession session, String scopes, ClientModel client) {
return isValidScope(session, scopes, null, client);
}
public static Stream<String> parseScopeParameter(String scopeParam) {
return Arrays.stream(scopeParam.split(" ")).distinct();
}

View file

@ -226,9 +226,9 @@ public class AuthorizationEndpointChecker {
public void checkValidScope() throws AuthorizationCheckException {
boolean validScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
validScopes = TokenManager.isValidScope(request.getScope(), request.getAuthorizationRequestContext(), client);
validScopes = TokenManager.isValidScope(session, request.getScope(), request.getAuthorizationRequestContext(), client);
} else {
validScopes = TokenManager.isValidScope(request.getScope(), client);
validScopes = TokenManager.isValidScope(session, request.getScope(), client);
}
if (!validScopes) {
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.SCOPE_PARAM);

View file

@ -187,7 +187,7 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
// 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)
String scopeParam = codeData.getScope();
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(scopeParam, client);
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(session, scopeParam, client);
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);

View file

@ -61,7 +61,7 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase {
@Override
public Response process(Context context) {
setContext(context);
if (client.isBearerOnly()) {
event.detail(Details.REASON, "Bearer-only client not allowed to retrieve service account");
event.error(Errors.INVALID_CLIENT);
@ -120,7 +120,7 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase {
clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, sessionPersistenceState);
event.session(userSession);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
// Notes about client details

View file

@ -206,9 +206,9 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
boolean validScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
AuthorizationRequestContext authorizationRequestContext = AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, scope);
validScopes = TokenManager.isValidScope(scope, authorizationRequestContext, client);
validScopes = TokenManager.isValidScope(session, scope, authorizationRequestContext, client);
} else {
validScopes = TokenManager.isValidScope(scope, client);
validScopes = TokenManager.isValidScope(session, scope, client);
}
if (!validScopes) {

View file

@ -127,7 +127,7 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
}
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext clientSessionCtx = processor.attachSession();
UserSessionModel userSession = processor.getUserSession();

View file

@ -188,7 +188,7 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
if (!TokenManager
.verifyConsentStillAvailable(session,
user, client, TokenManager.getRequestedClientScopes(scopeParam, client))) {
user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client))) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
@ -248,7 +248,7 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext context = AuthenticationProcessor
.attachSession(authSession, null, session, realm, session.getContext().getConnection(), event);

View file

@ -178,7 +178,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "missing parameter : scope",
Response.Status.BAD_REQUEST);
}
if (!TokenManager.isValidScope(scope, client)) {
if (!TokenManager.isValidScope(session, scope, client)) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid scopes: " + scope,
Response.Status.BAD_REQUEST);
}

View file

@ -336,7 +336,7 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
// 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)
String scopeParam = deviceCodeModel.getScope();
if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(scopeParam, client))) {
if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client))) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);

View file

@ -1210,14 +1210,14 @@ public class AuthenticationManager {
}
public static void setClientScopesInSession(AuthenticationSessionModel authSession) {
public static void setClientScopesInSession(KeycloakSession session, AuthenticationSessionModel authSession) {
ClientModel client = authSession.getClient();
UserModel user = authSession.getAuthenticatedUser();
// todo scope param protocol independent
String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
Set<String> requestedClientScopes = TokenManager.getRequestedClientScopes(scopeParam, client)
Set<String> requestedClientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client)
.map(ClientScopeModel::getId).collect(Collectors.toSet());
authSession.setClientScopes(requestedClientScopes);

View file

@ -531,7 +531,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
context.setToken(null);
}
StatusResponseType loginResponse = (StatusResponseType) context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
if (loginResponse != null) {
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
@ -886,7 +886,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
logger.debugf("Performing local authentication for user [%s].", federatedUser);
}
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
String nextRequiredAction = AuthenticationManager.nextRequiredAction(session, authSession, request, event);
if (nextRequiredAction != null) {
@ -999,7 +999,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
context.getIdp().authenticationFinished(authSession, context);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
TokenManager.attachAuthenticationSession(session, userSession, authSession);
if (isDebugEnabled()) {

View file

@ -109,7 +109,7 @@ public class ClientScopeEvaluateResource {
throw new NotFoundException("Role Container not found");
}
return new ClientScopeEvaluateScopeMappingsResource(roleContainer, auth, client, scopeParam);
return new ClientScopeEvaluateScopeMappingsResource(session, roleContainer, auth, client, scopeParam);
}
@ -129,7 +129,7 @@ public class ClientScopeEvaluateResource {
public Stream<ProtocolMapperEvaluationRepresentation> getGrantedProtocolMappers(@QueryParam("scope") String scopeParam) {
auth.clients().requireView(client);
return TokenManager.getRequestedClientScopes(scopeParam, client)
return TokenManager.getRequestedClientScopes(session, scopeParam, client)
.flatMap(mapperContainer -> mapperContainer.getProtocolMappersStream()
.filter(current -> isEnabled(session, current) && Objects.equals(current.getProtocol(), client.getProtocol()))
.map(current -> toProtocolMapperEvaluationRepresentation(current, mapperContainer)));
@ -252,7 +252,7 @@ public class ClientScopeEvaluateResource {
UserSessionModel userSession = new UserSessionManager(session).createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(),
clientConnection.getRemoteAddr(), "example-auth", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
return function.apply(userSession, clientSessionCtx);

View file

@ -33,6 +33,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.ModelToRepresentation;
@ -47,13 +48,15 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class ClientScopeEvaluateScopeMappingsResource {
private final KeycloakSession session;
private final RoleContainerModel roleContainer;
private final AdminPermissionEvaluator auth;
private final ClientModel client;
private final String scopeParam;
public ClientScopeEvaluateScopeMappingsResource(RoleContainerModel roleContainer, AdminPermissionEvaluator auth, ClientModel client,
public ClientScopeEvaluateScopeMappingsResource(KeycloakSession session, RoleContainerModel roleContainer, AdminPermissionEvaluator auth, ClientModel client,
String scopeParam) {
this.session = session;
this.roleContainer = roleContainer;
this.auth = auth;
this.client = client;
@ -77,7 +80,7 @@ public class ClientScopeEvaluateScopeMappingsResource {
@Operation( summary = "Get effective scope mapping of all roles of particular role container, which this client is defacto allowed to have in the accessToken issued for him.",
description = "This contains scope mappings, which this client has directly, as well as scope mappings, which are granted to all client scopes, which are linked with this client.")
public Stream<RoleRepresentation> getGrantedScopeMappings() {
return getGrantedRoles().map(ModelToRepresentation::toBriefRepresentation);
return getGrantedRoles(session).map(ModelToRepresentation::toBriefRepresentation);
}
@ -94,7 +97,7 @@ public class ClientScopeEvaluateScopeMappingsResource {
@Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS)
@Operation( summary = "Get roles, which this client doesn't have scope for and can't have them in the accessToken issued for him.", description = "Defacto all the other roles of particular role container, which are not in {@link #getGrantedScopeMappings()}")
public Stream<RoleRepresentation> getNotGrantedScopeMappings() {
Set<RoleModel> grantedRoles = getGrantedRoles().collect(Collectors.toSet());
Set<RoleModel> grantedRoles = getGrantedRoles(session).collect(Collectors.toSet());
return roleContainer.getRolesStream()
.filter(((Predicate<RoleModel>) grantedRoles::contains).negate())
@ -104,12 +107,12 @@ public class ClientScopeEvaluateScopeMappingsResource {
private Stream<RoleModel> getGrantedRoles() {
private Stream<RoleModel> getGrantedRoles(KeycloakSession session) {
if (client.isFullScopeAllowed()) {
return roleContainer.getRolesStream();
}
Set<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(scopeParam, client)
Set<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(session, scopeParam, client)
.collect(Collectors.toSet());
Predicate<RoleModel> hasClientScope = role ->

View file

@ -37,7 +37,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -53,13 +52,13 @@ import org.keycloak.util.TokenUtil;
*/
public class DefaultClientSessionContext implements ClientSessionContext {
private static Logger logger = Logger.getLogger(DefaultClientSessionContext.class);
private static final Logger logger = Logger.getLogger(DefaultClientSessionContext.class);
private final AuthenticatedClientSessionModel clientSession;
private final Set<String> clientScopeIds;
private final Set<ClientScopeModel> requestedScopes;
private final KeycloakSession session;
private Set<ClientScopeModel> clientScopes;
private Set<ClientScopeModel> allowedClientScopes;
//
private Set<RoleModel> roles;
@ -68,10 +67,12 @@ public class DefaultClientSessionContext implements ClientSessionContext {
// All roles of user expanded. It doesn't yet take into account permitted clientScopes
private Set<RoleModel> userRoles;
private Map<String, Object> attributes = new HashMap<>();
private final Map<String, Object> attributes = new HashMap<>();
private Set<String> clientScopeIds;
private String scopeString;
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds, KeycloakSession session) {
this.clientScopeIds = clientScopeIds;
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
this.requestedScopes = requestedScopes;
this.clientSession = clientSession;
this.session = session;
}
@ -86,33 +87,21 @@ public class DefaultClientSessionContext implements ClientSessionContext {
public static DefaultClientSessionContext fromClientSessionAndScopeParameter(AuthenticatedClientSessionModel clientSession, String scopeParam, KeycloakSession session) {
Stream<ClientScopeModel> requestedClientScopes;
Stream<ClientScopeModel> requestedScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
session.getContext().setClient(clientSession.getClient());
requestedClientScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
requestedScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
} else {
requestedClientScopes = TokenManager.getRequestedClientScopes(scopeParam, clientSession.getClient());
requestedScopes = TokenManager.getRequestedClientScopes(session, scopeParam, clientSession.getClient());
}
return fromClientSessionAndClientScopes(clientSession, requestedClientScopes, session);
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), session);
}
public static DefaultClientSessionContext fromClientSessionAndClientScopeIds(AuthenticatedClientSessionModel clientSession, Set<String> clientScopeIds, KeycloakSession session) {
return new DefaultClientSessionContext(clientSession, clientScopeIds, session);
public static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
return new DefaultClientSessionContext(clientSession, requestedScopes, session);
}
// in order to standardize the way we create this object and with that data, it's better to compute the client scopes internally instead of relying on external sources
// i.e: the TokenManager.getRequestedClientScopes was being called in many places to obtain the ClientScopeModel stream.
// by changing this method to private, we'll only call it in this class, while also having a single place to put the DYNAMIC_SCOPES feature flag condition
private static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession,
Stream<ClientScopeModel> clientScopes,
KeycloakSession session) {
Set<String> clientScopeIds = clientScopes.map(ClientScopeModel::getId).collect(Collectors.toSet());
return new DefaultClientSessionContext(clientSession, clientScopeIds, session);
}
@Override
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
@ -121,6 +110,11 @@ public class DefaultClientSessionContext implements ClientSessionContext {
@Override
public Set<String> getClientScopeIds() {
if (clientScopeIds == null) {
clientScopeIds = requestedScopes.stream()
.map(ClientScopeModel::getId)
.collect(Collectors.toSet());
}
return clientScopeIds;
}
@ -128,10 +122,10 @@ public class DefaultClientSessionContext implements ClientSessionContext {
@Override
public Stream<ClientScopeModel> getClientScopesStream() {
// Load client scopes if not yet present
if (clientScopes == null) {
clientScopes = loadClientScopes();
if (allowedClientScopes == null) {
allowedClientScopes = requestedScopes.stream().filter(this::isAllowed).collect(Collectors.toSet());
}
return clientScopes.stream();
return allowedClientScopes.stream();
}
@ -166,7 +160,10 @@ public class DefaultClientSessionContext implements ClientSessionContext {
@Override
public String getScopeString() {
return getScopeString(false);
if (scopeString == null) {
scopeString = getScopeString(false);
}
return scopeString;
}
@Override
@ -234,27 +231,25 @@ public class DefaultClientSessionContext implements ClientSessionContext {
// Loading data
private Set<ClientScopeModel> loadClientScopes() {
Set<ClientScopeModel> clientScopes = new HashSet<>();
for (String scopeId : clientScopeIds) {
ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(clientSession.getClient().getRealm(), getClientSession().getClient(), scopeId);
if (clientScope != null) {
if (isClientScopePermittedForUser(clientScope)) {
clientScopes.add(clientScope);
} else {
if (logger.isTraceEnabled()) {
logger.tracef("User '%s' not permitted to have client scope '%s'",
clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
}
}
}
private boolean isAllowed(ClientScopeModel clientScope) {
if (isClientScopePermittedForUser(clientScope)) {
return true;
}
return clientScopes;
}
if (logger.isTraceEnabled()) {
logger.tracef("User '%s' not permitted to have client scope '%s'",
clientSession.getUserSession().getUser().getUsername(), clientScope.getName());
}
return false;
}
// Return true if clientScope can be used by the user.
private boolean isClientScopePermittedForUser(ClientScopeModel clientScope) {
if (clientScope == null) {
return false;
}
if (clientScope instanceof ClientModel) {
return true;
}

View file

@ -96,7 +96,7 @@ public class UserSessionUtil {
authSession.setAuthenticatedUser(userSession.getUser());
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
AuthenticationManager.setClientScopesInSession(authSession);
AuthenticationManager.setClientScopesInSession(session, authSession);
TokenManager.attachAuthenticationSession(session, userSession, authSession);
return userSession;
}

View file

@ -87,7 +87,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
ClientModel client = authenticationSession.getClient();
return getRequestedClientScopes(requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
return getRequestedClientScopes(session, requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
}
private final KeycloakSession session;

View file

@ -117,6 +117,12 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
return createOrganization(realm, getCleanup(), name, brokerConfigFunction.apply(name).setUpIdentityProvider(), orgDomains);
}
protected OrganizationRepresentation createOrganization(String name, Map<String, String> brokerConfig) {
IdentityProviderRepresentation broker = brokerConfigFunction.apply(name).setUpIdentityProvider();
broker.getConfig().putAll(brokerConfig);
return createOrganization(testRealm(), getCleanup(), name, broker, name + ".org");
}
protected OrganizationRepresentation createOrganization(RealmResource testRealm, TestCleanup testCleanup, String name,
IdentityProviderRepresentation broker, String... orgDomains) {
OrganizationRepresentation org = createRepresentation(name, orgDomains);

View file

@ -0,0 +1,189 @@
/*
* 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.testsuite.organization.authentication;
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 java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.OrganizationResource;
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.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.OAuthClient;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationAuthenticationTest extends AbstractOrganizationTest {
@Test
public void testAuthenticateUnmanagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization, "contractor@contractor.org");
// first try to log in using only the email
openIdentityFirstLoginPage(member.getEmail(), false, null, false, false);
Assert.assertTrue(loginPage.isPasswordInputPresent());
// no idp should be shown because there is only a single idp that is bound to an organization
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// the member should be able to log in using the credentials
loginPage.login(memberPassword);
appPage.assertCurrent();
}
@Test
public void testTryLoginWithUsernameNotAnEmail() {
testRealm().organizations().get(createOrganization().getId());
openIdentityFirstLoginPage("user", false, null, false, false);
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
testRealm().organizations().get(createOrganization().getId());
openIdentityFirstLoginPage("user@noorg.org", false, null, false, false);
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testAuthenticateUnmanagedMemberWhenProviderDisabled() throws IOException {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization, "contractor@contractor.org");
// first try to access login page
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// disable the organization provider
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
.setOrganizationsEnabled(Boolean.FALSE)
.update()) {
// access the page again, now it should be present username and password fields
loginPage.open(bc.consumerRealmName());
waitForPage(driver, "sign in to", true);
assertThat("Driver should be on the consumer realm page right now",
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.consumerRealmName() + "/"));
Assert.assertTrue(loginPage.isPasswordInputPresent());
// no idp should be shown because there is only a single idp that is bound to an organization
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// the member should be able to log in using the credentials
loginPage.login(member.getEmail(), memberPassword);
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"));
}
}

View file

@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.testsuite.organization.member;
package org.keycloak.testsuite.organization.authentication;
import org.keycloak.common.Profile.Feature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;

View file

@ -1,107 +0,0 @@
/*
* 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.testsuite.organization.member;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import java.io.IOException;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTest {
@Test
public void testAuthenticateUnmanagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization, "contractor@contractor.org");
// first try to log in using only the email
openIdentityFirstLoginPage(member.getEmail(), false, null, false, false);
Assert.assertTrue(loginPage.isPasswordInputPresent());
// no idp should be shown because there is only a single idp that is bound to an organization
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// the member should be able to log in using the credentials
loginPage.login(memberPassword);
appPage.assertCurrent();
}
@Test
public void testTryLoginWithUsernameNotAnEmail() {
testRealm().organizations().get(createOrganization().getId());
openIdentityFirstLoginPage("user", false, null, false, false);
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
testRealm().organizations().get(createOrganization().getId());
openIdentityFirstLoginPage("user@noorg.org", false, null, false, false);
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testAuthenticateUnmanagedMemberWhenProviderDisabled() throws IOException {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization, "contractor@contractor.org");
// first try to access login page
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// disable the organization provider
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
.setOrganizationsEnabled(Boolean.FALSE)
.update()) {
// access the page again, now it should be present username and password fields
loginPage.open(bc.consumerRealmName());
waitForPage(driver, "sign in to", true);
assertThat("Driver should be on the consumer realm page right now",
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.consumerRealmName() + "/"));
Assert.assertTrue(loginPage.isPasswordInputPresent());
// no idp should be shown because there is only a single idp that is bound to an organization
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
// the member should be able to log in using the credentials
loginPage.login(member.getEmail(), memberPassword);
appPage.assertCurrent();
}
}
}