Finer variables for OAuth2 redirectUriTemplate expansion

Fixes #6239
This commit is contained in:
Marek Sabo 2019-05-22 01:54:44 +02:00 committed by Rob Winch
parent d285c6ab4c
commit 7cfb17a8a3
3 changed files with 182 additions and 28 deletions

View File

@ -27,6 +27,8 @@ import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
@ -54,6 +56,7 @@ import java.util.Map;
*/
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private static final char PATH_DELIMITER = '/';
private final ClientRegistrationRepository clientRegistrationRepository;
private final AntPathRequestMatcher authorizationRequestMatcher;
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
@ -127,7 +130,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
OAuth2AuthorizationRequest authorizationRequest = builder
.clientId(clientRegistration.getClientId())
@ -149,20 +152,49 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
return null;
}
private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
// Supported URI variables -> baseUrl, action, registrationId
// Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"
/**
* Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/>
* - baseUrl (e.g. https://localhost/app) <br/>
* - baseScheme (e.g. https) <br/>
* - baseHost (e.g. localhost) <br/>
* - basePort (e.g. :8080) <br/>
* - basePath (e.g. /app) <br/>
* - registrationId (e.g. google) <br/>
* - action (e.g. login) <br/>
* <p/>
* Null variables are provided as empty strings.
* <p/>
* Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL
*
* @return expanded URI
*/
private static String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("registrationId", clientRegistration.getRegistrationId());
String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.build()
.toUriString();
uriVariables.put("baseUrl", baseUrl);
if (action != null) {
uriVariables.put("action", action);
.replaceQuery(null)
.fragment(null)
.build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
// following logic is based on HierarchicalUriComponents#toUriString()
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path)) {
if (path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
}
uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
uriVariables.put("action", action == null ? "" : action);
return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate())
.buildAndExpand(uriVariables)
.toUriString();

View File

@ -30,8 +30,10 @@ import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
@ -63,8 +65,9 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
/**
* The default pattern used to resolve the {@link ClientRegistration#getRegistrationId()}
*/
public static final String DEFAULT_AUTHORIZATION_REQUEST_PATTERN = "/oauth2/authorization/{" + DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME
+ "}";
public static final String DEFAULT_AUTHORIZATION_REQUEST_PATTERN = "/oauth2/authorization/{" + DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME + "}";
private static final char PATH_DELIMITER = '/';
private final ServerWebExchangeMatcher authorizationRequestMatcher;
@ -121,8 +124,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
private OAuth2AuthorizationRequest authorizationRequest(ServerWebExchange exchange,
ClientRegistration clientRegistration) {
String redirectUriStr = this
.expandRedirectUri(exchange.getRequest(), clientRegistration);
String redirectUriStr = expandRedirectUri(exchange.getRequest(), clientRegistration);
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
@ -153,23 +155,52 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
.build();
}
private String expandRedirectUri(ServerHttpRequest request, ClientRegistration clientRegistration) {
// Supported URI variables -> baseUrl, action, registrationId
// Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"
/**
* Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/>
* - baseUrl (e.g. https://localhost/app) <br/>
* - baseScheme (e.g. https) <br/>
* - baseHost (e.g. localhost) <br/>
* - basePort (e.g. :8080) <br/>
* - basePath (e.g. /app) <br/>
* - registrationId (e.g. google) <br/>
* - action (e.g. login) <br/>
* <p/>
* Null variables are provided as empty strings.
* <p/>
* Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL
*
* @return expanded URI
*/
private static String expandRedirectUri(ServerHttpRequest request, ClientRegistration clientRegistration) {
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("registrationId", clientRegistration.getRegistrationId());
String baseUrl = UriComponentsBuilder.fromUri(request.getURI())
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
.replaceQuery(null)
.build()
.toUriString();
uriVariables.put("baseUrl", baseUrl);
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
String loginAction = "login";
uriVariables.put("action", loginAction);
.fragment(null)
.build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
// following logic is based on HierarchicalUriComponents#toUriString()
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path)) {
if (path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
}
uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
String action = "";
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
action = "login";
}
uriVariables.put("action", action);
return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate())
.buildAndExpand(uriVariables)

View File

@ -41,15 +41,17 @@ import static org.assertj.core.api.Assertions.entry;
public class DefaultOAuth2AuthorizationRequestResolverTests {
private ClientRegistration registration1;
private ClientRegistration registration2;
private ClientRegistration fineRedirectUriTemplateRegistration;
private ClientRegistration pkceRegistration;
private ClientRegistrationRepository clientRegistrationRepository;
private String authorizationRequestBaseUri = "/oauth2/authorization";
private final String authorizationRequestBaseUri = "/oauth2/authorization";
private DefaultOAuth2AuthorizationRequestResolver resolver;
@Before
public void setUp() {
this.registration1 = TestClientRegistrations.clientRegistration().build();
this.registration2 = TestClientRegistrations.clientRegistration2().build();
this.fineRedirectUriTemplateRegistration = fineRedirectUriTemplateClientRegistration().build();
this.pkceRegistration = TestClientRegistrations.clientRegistration()
.registrationId("pkce-client-registration-id")
.clientId("pkce-client-id")
@ -58,7 +60,7 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.build();
this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(
this.registration1, this.registration2, this.pkceRegistration);
this.registration1, this.registration2, this.fineRedirectUriTemplateRegistration, this.pkceRegistration);
this.resolver = new DefaultOAuth2AuthorizationRequestResolver(
this.clientRegistrationRepository, this.authorizationRequestBaseUri);
}
@ -152,6 +154,80 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
"http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
@Test
public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenHttpRedirectUriWithExtraVarsExpanded() {
ClientRegistration clientRegistration = this.fineRedirectUriTemplateRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServerPort(8080);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo(clientRegistration.getRedirectUriTemplate());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(
"http://localhost:8080/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
@Test
public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenHttpsRedirectUriWithExtraVarsExpanded() {
ClientRegistration clientRegistration = this.fineRedirectUriTemplateRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("https");
request.setServerPort(8081);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo(clientRegistration.getRedirectUriTemplate());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(
"https://localhost:8081/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
@Test
public void resolveWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriWithExtraVarsExcludesPort() {
ClientRegistration clientRegistration = this.fineRedirectUriTemplateRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("http");
request.setServerPort(80);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo(clientRegistration.getRedirectUriTemplate());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(
"http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
@Test
public void resolveWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUriWithExtraVarsExcludesPort() {
ClientRegistration clientRegistration = this.fineRedirectUriTemplateRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("https");
request.setServerPort(443);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo(clientRegistration.getRedirectUriTemplate());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(
"https://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
@Test
public void resolveWhenAuthorizationRequestHasNoPortThenExpandedRedirectUriWithExtraVarsExcludesPort() {
ClientRegistration clientRegistration = this.fineRedirectUriTemplateRegistration;
String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId();
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("https");
request.setServerPort(-1);
request.setServletPath(requestUri);
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo(clientRegistration.getRedirectUriTemplate());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(
"https://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId());
}
// gh-5520
@Test
public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpandedExcludesQueryString() {
@ -301,4 +377,19 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
"code_challenge_method=S256&" +
"code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
private static ClientRegistration.Builder fineRedirectUriTemplateClientRegistration() {
return ClientRegistration.withRegistrationId("fine-redirect-uri-template-client-registration")
.redirectUriTemplate("{baseScheme}://{baseHost}{basePort}{basePath}/{action}/oauth2/code/{registrationId}")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read:user")
.authorizationUri("https://example.com/login/oauth/authorize")
.tokenUri("https://example.com/login/oauth/access_token")
.userInfoUri("https://api.example.com/user")
.userNameAttributeName("id")
.clientName("Fine Redirect Uri Template Client")
.clientId("fine-redirect-uri-template-client")
.clientSecret("client-secret");
}
}