DefaultClientSessionContext.java
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.util;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
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.RoleUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.rar.AuthorizationRequestSource;
import org.keycloak.util.TokenUtil;
/**
* Not thread safe. It's per-request object
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientSessionContext implements ClientSessionContext {
private static final Logger logger = Logger.getLogger(DefaultClientSessionContext.class);
private final AuthenticatedClientSessionModel clientSession;
private final Set<ClientScopeModel> requestedScopes;
private final KeycloakSession session;
private Set<ClientScopeModel> allowedClientScopes;
//
private Set<RoleModel> roles;
private Set<ProtocolMapperModel> protocolMappers;
// All roles of user expanded. It doesn't yet take into account permitted clientScopes
private Set<RoleModel> userRoles;
private final Map<String, Object> attributes = new HashMap<>();
private Set<String> clientScopeIds;
private String scopeString;
private DefaultClientSessionContext(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
this.requestedScopes = requestedScopes;
this.clientSession = clientSession;
this.session = session;
this.session.setAttribute(ClientSessionContext.class.getName(), this);
}
/**
* Useful if we want to "re-compute" client scopes based on the scope parameter
*/
public static DefaultClientSessionContext fromClientSessionScopeParameter(AuthenticatedClientSessionModel clientSession, KeycloakSession session) {
return fromClientSessionAndScopeParameter(clientSession, clientSession.getNote(OAuth2Constants.SCOPE), session);
}
public static DefaultClientSessionContext fromClientSessionAndScopeParameter(AuthenticatedClientSessionModel clientSession, String scopeParam, KeycloakSession session) {
Stream<ClientScopeModel> requestedScopes;
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
session.getContext().setClient(clientSession.getClient());
requestedScopes = AuthorizationContextUtil.getClientScopesStreamFromAuthorizationRequestContextWithClient(session, scopeParam);
} else {
requestedScopes = TokenManager.getRequestedClientScopes(session, scopeParam, clientSession.getClient(), clientSession.getUserSession().getUser());
}
return new DefaultClientSessionContext(clientSession, requestedScopes.collect(Collectors.toSet()), session);
}
public static DefaultClientSessionContext fromClientSessionAndClientScopes(AuthenticatedClientSessionModel clientSession, Set<ClientScopeModel> requestedScopes, KeycloakSession session) {
return new DefaultClientSessionContext(clientSession, requestedScopes, session);
}
@Override
public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
@Override
public Set<String> getClientScopeIds() {
if (clientScopeIds == null) {
clientScopeIds = requestedScopes.stream()
.map(ClientScopeModel::getId)
.collect(Collectors.toSet());
}
return clientScopeIds;
}
@Override
public Stream<ClientScopeModel> getClientScopesStream() {
// Load client scopes if not yet present
if (allowedClientScopes == null) {
allowedClientScopes = requestedScopes.stream().filter(this::isAllowed).collect(Collectors.toSet());
}
return allowedClientScopes.stream();
}
@Override
public Stream<RoleModel> getRolesStream() {
// Load roles if not yet present
if (roles == null) {
roles = loadRoles();
}
return roles.stream();
}
@Override
public Stream<ProtocolMapperModel> getProtocolMappersStream() {
// Load protocolMappers if not yet present
if (protocolMappers == null) {
protocolMappers = loadProtocolMappers();
}
return protocolMappers.stream();
}
private Set<RoleModel> getUserRoles() {
// Load userRoles if not yet present
if (userRoles == null) {
userRoles = loadUserRoles();
}
return userRoles;
}
@Override
public String getScopeString() {
if (scopeString == null) {
scopeString = getScopeString(false);
}
return scopeString;
}
@Override
public String getScopeString(boolean ignoreIncludeInTokenScope) {
if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) {
String scopeParam = buildScopesStringFromAuthorizationRequest(ignoreIncludeInTokenScope);
logger.tracef("Generated scope param with Dynamic Scopes enabled: %1s", scopeParam);
String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE);
if (TokenUtil.isOIDCRequest(scopeSent)) {
scopeParam = TokenUtil.attachOIDCScope(scopeParam);
}
return scopeParam;
}
// Add both default and optional scopes to scope parameter. Don't add client itself
String scopeParam = getClientScopesStream()
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.filter(scope-> scope.isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.map(ClientScopeModel::getName)
.collect(Collectors.joining(" "));
// See if "openid" scope is requested
String scopeSent = clientSession.getNote(OAuth2Constants.SCOPE);
if (TokenUtil.isOIDCRequest(scopeSent)) {
scopeParam = TokenUtil.attachOIDCScope(scopeParam);
}
return scopeParam;
}
/**
* Get all the scopes from the {@link AuthorizationRequestContext} by filtering entries by Source and by whether
* they should be included in tokens or not.
* Then return the scope name from the data stored in the RAR object representation.
*
* @param ignoreIncludeInTokenScope ignore include in token scope from client scope options
*
* @return see description
*/
private String buildScopesStringFromAuthorizationRequest(boolean ignoreIncludeInTokenScope) {
return AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, clientSession.getNote(OAuth2Constants.SCOPE)).getAuthorizationDetailEntries().stream()
.filter(authorizationDetails -> authorizationDetails.getSource().equals(AuthorizationRequestSource.SCOPE))
.filter(authorizationDetails -> authorizationDetails.getClientScope().isIncludeInTokenScope() || ignoreIncludeInTokenScope)
.filter(authorizationDetails -> isClientScopePermittedForUser(authorizationDetails.getClientScope()))
.map(authorizationDetails -> authorizationDetails.getAuthorizationDetails().getScopeNameFromCustomData())
.collect(Collectors.joining(" "));
}
@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
@Override
public <T> T getAttribute(String name, Class<T> clazz) {
Object value = attributes.get(name);
return clazz.cast(value);
}
@Override
public AuthorizationRequestContext getAuthorizationRequestContext() {
return AuthorizationContextUtil.getAuthorizationRequestContextFromScopes(session, clientSession.getNote(OAuth2Constants.SCOPE));
}
// Loading data
private boolean isAllowed(ClientScopeModel clientScope) {
if (isClientScopePermittedForUser(clientScope)) {
return true;
}
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;
}
Set<RoleModel> clientScopeRoles = clientScope.getScopeMappingsStream().collect(Collectors.toSet());
// Client scope is automatically permitted if it doesn't have any role scope mappings
if (clientScopeRoles.isEmpty()) {
return true;
}
// Expand (resolve composite roles)
clientScopeRoles = RoleUtils.expandCompositeRoles(clientScopeRoles);
// Check if expanded roles of clientScope has any intersection with expanded roles of user. If not, it is not permitted
clientScopeRoles.retainAll(getUserRoles());
return !clientScopeRoles.isEmpty();
}
private Set<RoleModel> loadRoles() {
UserModel user = clientSession.getUserSession().getUser();
ClientModel client = clientSession.getClient();
return TokenManager.getAccess(user, client, getClientScopesStream());
}
private Set<ProtocolMapperModel> loadProtocolMappers() {
String protocol = clientSession.getClient().getProtocol();
// Being rather defensive. But protocol should normally always be there
if (protocol == null) {
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration",
clientSession.getClient().getClientId());
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
String finalProtocol = protocol;
return getClientScopesStream()
.flatMap(clientScope -> clientScope.getProtocolMappersStream()
.filter(mapper -> Objects.equals(finalProtocol, mapper.getProtocol()) &&
ProtocolMapperUtils.isEnabled(session, mapper)))
.collect(Collectors.toSet());
}
private Set<RoleModel> loadUserRoles() {
UserModel user = clientSession.getUserSession().getUser();
return RoleUtils.getDeepUserRoleMappings(user);
}
}