Permalink
Browse files

SECOAUTH-221 - Support for registering multiple redirect_uri

Change to the ClientDetails to allow for a Set<String> of redirect URIs. This is as per OAuth2 v2-23.

Whilst a seemingly straightforward change, this has some major implications to the JdbcClientDetailsService, as this now requires two separate SQL statements to set (when using a custom schema). It also requires the schema to include a new table to accommodate the 1-to-* relationship.

 If a client has one or more registered URIs, the client MUST specify the redirect_uri when requesting an OAuth token. If this is not present an OAuth2Exception will be thrown with the error message "A redirect_uri must be supplied.".

 If the client has no registered URIs, the behaviour is unchanged.
  • Loading branch information...
1 parent ce53f7a commit ba4956313a58cf3412682ea244c38268c223d7a1 Declan Newman committed with dsyer Mar 12, 2012
View
@@ -11,3 +11,4 @@ target/
*.ipr
.idea/
cargo-installs/
+atlassian-ide-plugin.xml
@@ -1,21 +1,14 @@
package org.springframework.security.oauth2.provider;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.util.StringUtils;
+import java.util.*;
+
/**
* Base implementation of {@link org.springframework.security.oauth2.provider.ClientDetails}.
- *
+ *
* @author Ryan Heaton
* @author Dave Syer
*/
@@ -31,17 +24,17 @@
private Set<String> authorizedGrantTypes = Collections.emptySet();
- private String registeredRedirectUri;
+ private Set<String> registeredRedirectUris;
private List<GrantedAuthority> authorities = Collections.emptyList();
-
- private int accessTokenValiditySeconds = 0;
+
+ private int accessTokenValiditySeconds = 0;
public BaseClientDetails() {
}
public BaseClientDetails(String commaSeparatedResourceIds, String commaSeparatedScopes,
- String commaSeparatedAuthorizedGrantTypes, String commaSeparatedAuthorities) {
+ String commaSeparatedAuthorizedGrantTypes, String commaSeparatedAuthorities) {
if (StringUtils.hasText(commaSeparatedResourceIds)) {
Set<String> resourceIds = StringUtils.commaDelimitedListToSet(commaSeparatedResourceIds);
@@ -117,12 +110,12 @@ public void setAuthorizedGrantTypes(Collection<String> authorizedGrantTypes) {
this.authorizedGrantTypes = new LinkedHashSet<String>(authorizedGrantTypes);
}
- public String getRegisteredRedirectUri() {
- return registeredRedirectUri;
+ public Set<String> getRegisteredRedirectUri() {
+ return registeredRedirectUris;
}
- public void setRegisteredRedirectUri(String registeredRedirectUri) {
- this.registeredRedirectUri = registeredRedirectUri;
+ public void setRegisteredRedirectUri(Set<String> registeredRedirectUris) {
+ this.registeredRedirectUris = registeredRedirectUris;
}
public Collection<GrantedAuthority> getAuthorities() {
@@ -1,88 +1,88 @@
package org.springframework.security.oauth2.provider;
+import org.springframework.security.core.GrantedAuthority;
+
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
-import org.springframework.security.core.GrantedAuthority;
-
/**
* Client details for OAuth 2
- *
+ *
* @author Ryan Heaton
*/
public interface ClientDetails extends Serializable {
/**
* The client id.
- *
+ *
* @return The client id.
*/
String getClientId();
/**
* The resources that this client can access. Ignored if empty.
- *
+ *
* @return The resources of this client.
*/
Set<String> getResourceIds();
/**
* Whether a secret is required to authenticate this client.
- *
+ *
* @return Whether a secret is required to authenticate this client.
*/
boolean isSecretRequired();
/**
* The client secret. Ignored if the {@link #isSecretRequired() secret isn't required}.
- *
+ *
* @return The client secret.
*/
String getClientSecret();
/**
* Whether this client is limited to a specific scope. If false, the scope of the authentication request will be
* ignored.
- *
+ *
* @return Whether this client is limited to a specific scope.
*/
boolean isScoped();
/**
* The scope of this client. Ignored if the {@link #isScoped() client isn't scoped}.
- *
+ *
* @return The scope of this client.
*/
Set<String> getScope();
/**
* The grant types for which this client is authorized.
- *
+ *
* @return The grant types for which this client is authorized.
*/
Set<String> getAuthorizedGrantTypes();
/**
* The pre-defined redirect URI for this client to use during the "authorization_code" access grant. See OAuth spec,
* section 4.1.1.
- *
+ *
* @return The pre-defined redirect URI for this client.
*/
- String getRegisteredRedirectUri();
+ Set<String> getRegisteredRedirectUri();
/**
* Get the authorities that are granted to the OAuth client. Note that these are NOT the authorities that are
* granted to the user with an authorized access token. Instead, these authorities are inherent to the client
* itself.
- *
+ *
* @return The authorities.
*/
Collection<GrantedAuthority> getAuthorities();
/**
* The access token validity period for this client. Zero or negative for unlimited.
- *
+ *
* @return the access token validity period
*/
int getAccessTokenValiditySeconds();
@@ -16,56 +16,67 @@
package org.springframework.security.oauth2.provider;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-
-import javax.sql.DataSource;
-
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Set;
/**
* Basic, JDBC implementation of the client details service.
*/
public class JdbcClientDetailsService implements ClientDetailsService {
- private static final String DEFAULT_SELECT_STATEMENT = "select client_id, resource_ids, client_secret, scope, "
- + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity from oauth_client_details where client_id = ?";
+ private static final String DEFAULT_SELECT_STATEMENT = "select client_id, resource_ids, client_secret, scope, "
+ + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity from oauth_client_details where client_id = ?";
+
+ private String selectClientDetailsSql = DEFAULT_SELECT_STATEMENT;
- private String selectClientDetailsSql = DEFAULT_SELECT_STATEMENT;
+ private final JdbcTemplate jdbcTemplate;
- private final JdbcTemplate jdbcTemplate;
+ public JdbcClientDetailsService(DataSource dataSource) {
+ Assert.notNull(dataSource, "DataSource required");
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
- public JdbcClientDetailsService(DataSource dataSource) {
- Assert.notNull(dataSource, "DataSource required");
- this.jdbcTemplate = new JdbcTemplate(dataSource);
- }
+ public ClientDetails loadClientByClientId(String clientId) throws OAuth2Exception {
+ ClientDetails details;
+ try {
+ details = jdbcTemplate.queryForObject(selectClientDetailsSql, new RowMapper<ClientDetails>() {
+ public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
+ BaseClientDetails details = new BaseClientDetails(rs.getString(2),
+ rs.getString(4), rs.getString(5), rs.getString(7));
+ details.setClientId(rs.getString(1));
+ details.setClientSecret(rs.getString(3));
+ details.setRegisteredRedirectUri(getRedirectUris(rs.getString(6)));
+ details.setAccessTokenValiditySeconds(rs.getInt(8));
+ return details;
+ }
+ }, clientId);
+ } catch (EmptyResultDataAccessException e) {
+ throw new InvalidClientException("Client not found: " + clientId);
+ }
- public ClientDetails loadClientByClientId(String clientId) throws OAuth2Exception {
- ClientDetails details;
- try {
- details = jdbcTemplate.queryForObject(selectClientDetailsSql, new RowMapper<ClientDetails>() {
- public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
- BaseClientDetails details = new BaseClientDetails(rs.getString(2),
- rs.getString(4), rs.getString(5), rs.getString(7));
- details.setClientId(rs.getString(1));
- details.setClientSecret(rs.getString(3));
- details.setRegisteredRedirectUri(rs.getString(6));
- details.setAccessTokenValiditySeconds(rs.getInt(8));
- return details;
- }
- }, clientId);
- } catch (EmptyResultDataAccessException e) {
- throw new InvalidClientException("Client not found: " + clientId);
- }
+ return details;
+ }
- return details;
- }
+ public void setSelectClientDetailsSql(String selectClientDetailsSql) {
+ this.selectClientDetailsSql = selectClientDetailsSql;
+ }
- public void setSelectClientDetailsSql(String selectClientDetailsSql) {
- this.selectClientDetailsSql = selectClientDetailsSql;
- }
+ private Set<String> getRedirectUris(String redirectUris) {
+ if (StringUtils.hasText(redirectUris)) {
+ Set<String> redirectUriSet = StringUtils.commaDelimitedListToSet(redirectUris);
+ if (!redirectUriSet.isEmpty()) {
+ return redirectUriSet;
+ }
+ }
+ return null;
+ }
}
@@ -3,50 +3,67 @@
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.util.Set;
/**
* Default implementation for a redirect resolver.
- *
+ *
* @author Ryan Heaton
* @author Dave Syer
*/
public class DefaultRedirectResolver implements RedirectResolver {
public String resolveRedirect(String requestedRedirect, ClientDetails client) throws OAuth2Exception {
+ Set<String> redirectUris = client.getRegisteredRedirectUri();
- String redirectUri = client.getRegisteredRedirectUri();
-
- if (redirectUri != null && requestedRedirect != null) {
- if (!redirectMatches(requestedRedirect, redirectUri)) {
- throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
- + " does not match registered value: " + redirectUri);
- }
- else {
- redirectUri = requestedRedirect;
- }
+ if (redirectUris != null && !redirectUris.isEmpty() && StringUtils.hasText(requestedRedirect)) {
+ return obtainMatchingRedirect(redirectUris, requestedRedirect);
}
-
- if (redirectUri == null) {
- if (requestedRedirect == null) {
- throw new OAuth2Exception("A redirect_uri must be supplied.");
- }
- redirectUri = requestedRedirect;
+ else if (StringUtils.hasText(requestedRedirect)) {
+ return requestedRedirect;
+ }
+ else {
+ throw new OAuth2Exception("A redirect_uri must be supplied.");
}
-
- return redirectUri;
}
/**
* Whether the requested redirect URI "matches" the specified redirect URI. This implementation tests if the user
* requrested redirect starts with the registered redirect, so it would have the same host and root path if it is an
* HTTP URL.
- *
+ *
* @param requestedRedirect The requested redirect URI.
- * @param redirectUri The registered 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) {
return requestedRedirect.startsWith(redirectUri);
}
+
+ /**
+ * 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");
+ Assert.hasText(requestedRedirect, "Requested redirect must have a value");
+
+ for (String redirectUri : redirectUris) {
+ if (redirectMatches(requestedRedirect, redirectUri)) {
+ return requestedRedirect;
+ }
+ }
+ throw new RedirectMismatchException("Invalid redirect: "
+ + requestedRedirect
+ + " does not match one of the registered values: "
+ + redirectUris.toString());
+ }
}
Oops, something went wrong.

0 comments on commit ba49563

Please sign in to comment.