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:
parent
310824cc2b
commit
96acc62c00
30 changed files with 666 additions and 253 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -127,7 +127,7 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
|
|||
|
||||
}
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authSession);
|
||||
AuthenticationManager.setClientScopesInSession(session, authSession);
|
||||
|
||||
ClientSessionContext clientSessionCtx = processor.attachSession();
|
||||
UserSessionModel userSession = processor.getUserSession();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue