org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver Maven / Gradle / Ivy
Show all versions of spring-security-oauth2 Show documentation
/*
* Copyright 2002-2019 the original author or authors.
*
* 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
*
* https://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.springframework.security.oauth2.provider.endpoint;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Default implementation for a redirect resolver.
*
* @author Ryan Heaton
* @author Dave Syer
*/
public class DefaultRedirectResolver implements RedirectResolver {
private Collection redirectGrantTypes = Arrays.asList("implicit", "authorization_code");
private boolean matchSubdomains = false;
private boolean matchPorts = true;
/**
* Flag to indicate that requested URIs will match if they are a subdomain of the registered value.
*
* @param matchSubdomains the flag value to set (default true)
*/
public void setMatchSubdomains(boolean matchSubdomains) {
this.matchSubdomains = matchSubdomains;
}
/**
* Flag that enables/disables port matching between the requested redirect URI and the registered redirect URI(s).
*
* @param matchPorts true to enable port matching, false to disable (defaults to true)
*/
public void setMatchPorts(boolean matchPorts) {
this.matchPorts = matchPorts;
}
/**
* Grant types that are permitted to have a redirect uri.
*
* @param redirectGrantTypes the redirect grant types to set
*/
public void setRedirectGrantTypes(Collection redirectGrantTypes) {
this.redirectGrantTypes = new HashSet(redirectGrantTypes);
}
public String resolveRedirect(String requestedRedirect, ClientDetails client) throws OAuth2Exception {
Set authorizedGrantTypes = client.getAuthorizedGrantTypes();
if (authorizedGrantTypes.isEmpty()) {
throw new InvalidGrantException("A client must have at least one authorized grant type.");
}
if (!containsRedirectGrantType(authorizedGrantTypes)) {
throw new InvalidGrantException(
"A redirect_uri can only be used by implicit or authorization_code grant types.");
}
Set registeredRedirectUris = client.getRegisteredRedirectUri();
if (registeredRedirectUris == null || registeredRedirectUris.isEmpty()) {
throw new InvalidRequestException("At least one redirect_uri must be registered with the client.");
}
return obtainMatchingRedirect(registeredRedirectUris, requestedRedirect);
}
/**
* @param grantTypes some grant types
* @return true if the supplied grant types includes one or more of the redirect types
*/
private boolean containsRedirectGrantType(Set grantTypes) {
for (String type : grantTypes) {
if (redirectGrantTypes.contains(type)) {
return true;
}
}
return false;
}
/**
* Whether the requested redirect URI "matches" the specified redirect URI. For a URL, this implementation tests if
* the user requested redirect starts with the registered redirect, so it would have the same host and root path if
* it is an HTTP URL. The port, userinfo, query params also matched. Request redirect uri path can include
* additional parameters which are ignored for the match
*
* For other (non-URL) cases, such as for some implicit clients, the redirect_uri must be an exact match.
*
* @param requestedRedirect The requested redirect URI.
* @param redirectUri The registered redirect URI.
* @return Whether the requested redirect URI "matches" the specified redirect URI.
*/
protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
boolean pathMatch = isEqual(registeredRedirectUri.getPath(),
StringUtils.cleanPath(requestedRedirectUri.getPath()));
boolean queryParamMatch = matchQueryParams(registeredRedirectUri.getQueryParams(),
requestedRedirectUri.getQueryParams());
return schemeMatch && userInfoMatch && hostMatch && portMatch && pathMatch && queryParamMatch;
}
/**
* Checks whether the registered redirect uri query params key and values contains match the requested set
*
* The requested redirect uri query params are allowed to contain additional params which will be retained
*
* @param registeredRedirectUriQueryParams
* @param requestedRedirectUriQueryParams
* @return whether the params match
*/
private boolean matchQueryParams(MultiValueMap registeredRedirectUriQueryParams,
MultiValueMap requestedRedirectUriQueryParams) {
Iterator iter = registeredRedirectUriQueryParams.keySet().iterator();
while (iter.hasNext()) {
String key = iter.next();
List registeredRedirectUriQueryParamsValues = registeredRedirectUriQueryParams.get(key);
List requestedRedirectUriQueryParamsValues = requestedRedirectUriQueryParams.get(key);
if (!registeredRedirectUriQueryParamsValues.equals(requestedRedirectUriQueryParamsValues)) {
return false;
}
}
return true;
}
/**
* Compares two strings but treats empty string or null equal
*
* @param str1
* @param str2
* @return true if strings are equal, false otherwise
*/
private boolean isEqual(String str1, String str2) {
if (StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) {
return true;
} else if (!StringUtils.isEmpty(str1)) {
return str1.equals(str2);
} else {
return false;
}
}
/**
* Check if host matches the registered value.
*
* @param registered the registered host. Can be null.
* @param requested the requested host. Can be null.
* @return true if they match
*/
protected boolean hostMatches(String registered, String requested) {
if (matchSubdomains) {
return isEqual(registered, requested) || (requested != null && requested.endsWith("." + registered));
}
return isEqual(registered, requested);
}
/**
* Attempt to match one of the registered URIs to the that of the requested one.
*
* @param redirectUris the set of the registered URIs to try and find a match. This cannot be null or empty.
* @param requestedRedirect the URI used as part of the request
* @return redirect uri
* @throws RedirectMismatchException if no match was found
*/
private String obtainMatchingRedirect(Set redirectUris, String requestedRedirect) {
Assert.notEmpty(redirectUris, "Redirect URIs cannot be empty");
if (redirectUris.size() == 1 && requestedRedirect == null) {
return redirectUris.iterator().next();
}
for (String redirectUri : redirectUris) {
if (requestedRedirect != null && redirectMatches(requestedRedirect, redirectUri)) {
// Initialize with the registered redirect-uri
UriComponentsBuilder redirectUriBuilder = UriComponentsBuilder.fromUriString(redirectUri);
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
if (this.matchSubdomains) {
redirectUriBuilder.host(requestedRedirectUri.getHost());
}
if (!this.matchPorts) {
redirectUriBuilder.port(requestedRedirectUri.getPort());
}
redirectUriBuilder.replaceQuery(requestedRedirectUri.getQuery()); // retain additional params (if any)
redirectUriBuilder.fragment(null);
return redirectUriBuilder.build().toUriString();
}
}
throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
+ " does not match one of the registered values: " + redirectUris.toString());
}
}