ClientScopeAuthorizationRequestParser.java
/*
* Copyright 2022 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.protocol.oidc.rar.parsers;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.rar.AuthorizationRequestParserProvider;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.protocol.oidc.rar.model.IntermediaryScopeRepresentation;
import org.keycloak.rar.AuthorizationDetails;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.rar.AuthorizationRequestSource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.keycloak.representations.AuthorizationDetailsJSONRepresentation.DYNAMIC_SCOPE_RAR_TYPE;
import static org.keycloak.representations.AuthorizationDetailsJSONRepresentation.STATIC_SCOPE_RAR_TYPE;
/**
* @author <a href="mailto:dgozalob@redhat.com">Daniel Gozalo</a>
*/
public class ClientScopeAuthorizationRequestParser implements AuthorizationRequestParserProvider {
protected static final Logger logger = Logger.getLogger(ClientScopeAuthorizationRequestParser.class);
/**
* This parser will be created on a per-request basis. When the adapter is created, the request's client is passed
* as a parameter
*/
private final ClientModel client;
public ClientScopeAuthorizationRequestParser(ClientModel client) {
this.client = client;
}
/**
* Creates a {@link AuthorizationRequestContext} with a list of {@link AuthorizationDetails} that will be parsed from
* the provided OAuth scopes that have been requested in a given Auth request, together with default client scopes.
* <p>
* Dynamic scopes will also be parsed with the extracted parameter, so it can be used later
*
* @param scopeParam the OAuth scope param for the current request
* @return see description
*/
@Override
public AuthorizationRequestContext parseScopes(String scopeParam) {
// Process all the default ClientScopeModels for the current client, and maps them to the DynamicScopeRepresentation to make use of a HashSet
Set<IntermediaryScopeRepresentation> clientScopeModelSet = this.client.getClientScopes(true).values().stream()
.filter(clientScopeModel -> !clientScopeModel.isDynamicScope()) // not strictly needed as Dynamic Scopes are going to be Optional scopes for now
.map(IntermediaryScopeRepresentation::new)
.collect(Collectors.toSet());
Set<IntermediaryScopeRepresentation> intermediaryScopeRepresentations = new HashSet<>();
if (scopeParam != null) {
// Go through the parsed requested scopes and attempt to match them against the optional scopes list
intermediaryScopeRepresentations = TokenManager.parseScopeParameter(scopeParam).collect(Collectors.toSet()).stream()
.map((String requestScope) -> getMatchingClientScope(requestScope, this.client.getClientScopes(false).values()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
}
// merge both sets, avoiding duplicates
intermediaryScopeRepresentations.addAll(clientScopeModelSet);
// Map the intermediary scope representations into the final AuthorizationDetails representation to be included into the RAR context
List<AuthorizationDetails> authorizationDetails = intermediaryScopeRepresentations.stream()
.map(this::buildAuthorizationDetailsJSONRepresentation)
.collect(Collectors.toList());
return new AuthorizationRequestContext(authorizationDetails);
}
/**
* From a {@link IntermediaryScopeRepresentation}, create an {@link AuthorizationDetails} object that serves as the representation of a
* ClientScope inside a Rich Authorization Request object
*
* @param intermediaryScopeRepresentation the intermediary scope representation to be included into the RAR request object
* @return see description
*/
private AuthorizationDetails buildAuthorizationDetailsJSONRepresentation(IntermediaryScopeRepresentation intermediaryScopeRepresentation) {
AuthorizationDetailsJSONRepresentation representation = new AuthorizationDetailsJSONRepresentation();
representation.setCustomData("access", Collections.singletonList(intermediaryScopeRepresentation.getRequestedScopeString()));
representation.setType(STATIC_SCOPE_RAR_TYPE);
if (intermediaryScopeRepresentation.isDynamic() && intermediaryScopeRepresentation.getParameter() != null) {
representation.setType(DYNAMIC_SCOPE_RAR_TYPE);
representation.setCustomData("scope_parameter", intermediaryScopeRepresentation.getParameter());
}
return new AuthorizationDetails(intermediaryScopeRepresentation.getScope(), AuthorizationRequestSource.SCOPE, representation);
}
/**
* Gets one of the requested OAuth scopes and obtains the list of all the optional client scope models for the current client and searches whether
* there is a match.
* Dynamic scopes are matching using the registered Regexp, while static scopes are matched by name.
* It returns an Optional of a {@link IntermediaryScopeRepresentation} with either a static scope datra, a dynamic scope data or an empty Optional
* if there was no match for the regexp.
*
* @param requestScope one of the requested OAuth scopes
* @return see description
*/
private Optional<IntermediaryScopeRepresentation> getMatchingClientScope(String requestScope, Collection<ClientScopeModel> optionalScopes) {
for (ClientScopeModel clientScopeModel : optionalScopes) {
if (clientScopeModel.isDynamicScope()) {
// The regexp has been stored without a capture group to simplify how it's shown to the user, need to transform it now
// to capture the parameter value
Pattern p = Pattern.compile(clientScopeModel.getDynamicScopeRegexp().replace("*", "(.*)"));
Matcher m = p.matcher(requestScope);
if (m.matches()) {
return Optional.of(new IntermediaryScopeRepresentation(clientScopeModel, m.group(1), requestScope));
}
} else {
if (requestScope.equalsIgnoreCase(clientScopeModel.getName())) {
return Optional.of(new IntermediaryScopeRepresentation(clientScopeModel));
}
}
}
// Nothing matched, returning an empty Optional to avoid working with Nulls
return Optional.empty();
}
@Override
public void close() {
}
}