/*
* Copyright 2002-2011 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
*
* 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.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.StringUtils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* Default implementation for a redirect resolver.
*
* @author Ryan Heaton
* @author Dave Syer
*/
public class DefaultRedirectResolver implements RedirectResolver {
private Collection<String> redirectGrantTypes = Arrays.asList("implicit", "authorization_code");
private boolean matchSubdomains = true;
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 (deafult 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<String> redirectGrantTypes) {
this.redirectGrantTypes = new HashSet<String>(redirectGrantTypes);
}
public String resolveRedirect(String requestedRedirect, ClientDetails client) throws OAuth2Exception {
Set<String> 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<String> redirectUris = client.getRegisteredRedirectUri();
if (redirectUris != null && !redirectUris.isEmpty()) {
return obtainMatchingRedirect(redirectUris, requestedRedirect);
}
else if (StringUtils.hasText(requestedRedirect)) {
return requestedRedirect;
}
else {
throw new InvalidRequestException("A redirect_uri must be supplied.");
}
}
/**
* @param grantTypes some grant types
* @return true if the supplied grant types includes one or more of the redirect types
*/
private boolean containsRedirectGrantType(Set<String> 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 is also matched.
* <p>
* 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) {
try {
URL req = new URL(requestedRedirect);
URL reg = new URL(redirectUri);
int requestedPort = req.getPort() != -1 ? req.getPort() : req.getDefaultPort();
int registeredPort = reg.getPort() != -1 ? reg.getPort() : reg.getDefaultPort();
boolean portsMatch = matchPorts ? (registeredPort == requestedPort) : true;
if (reg.getProtocol().equals(req.getProtocol()) &&
hostMatches(reg.getHost(), req.getHost()) &&
portsMatch) {
return StringUtils.cleanPath(req.getPath()).startsWith(StringUtils.cleanPath(reg.getPath()));
}
}
catch (MalformedURLException e) {
}
return requestedRedirect.equals(redirectUri);
}
/**
* Check if host matches the registered value.
*
* @param registered the registered host
* @param requested the requested host
* @return true if they match
*/
protected boolean hostMatches(String registered, String requested) {
if (matchSubdomains) {
return registered.equals(requested) || requested.endsWith("." + registered);
}
return registered.equals(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 the matching URI
* @throws RedirectMismatchException if no match was found
*/
private String obtainMatchingRedirect(Set<String> 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)) {
return requestedRedirect;
}
}
throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
+ " does not match one of the registered values: " + redirectUris.toString());
}
}