Skip to content

Authorization Response should also match on query parameters #7329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

ttddyy
Copy link
Contributor

@ttddyy ttddyy commented Aug 29, 2019

This is about OAuth2 Authorization Code Grant flow.

When a service is registered with redirect_uri that has query parameter (e.g: http://some/auth?param=foo), current OAuth2AuthorizationCodeGrantFilter doesn't work well.

This is because OAuth2AuthorizationCodeGrantFilter#shouldProcessAuthorizationResponse compares registered redirect_uri (AuthRequest: http://some/auth?param=foo) against the redirected request URI without query parameters. (AuthResponse: http://some/auth)

This patch updates the redirect_uri comparison not only checking base uri but also checks query params in the registered redirect_uri whether they are all included in the authorization response's query parameters.

Also, in the patch, it now retrieves final redirect_uri from authorizationRequest since authorizationResponse.getRedirectUri() does not have query parameters.
This might be debatable and also may change based on #7324.

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @ttddyy. Please see my comments.

* @see OAuth2AuthorizationRequest
* @see OAuth2AuthorizationResponse
*/
public final class OAuth2AuthorizationExchangeUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please remove this and keep the changes contained in OAuth2AuthorizationResponseUtils and OAuth2AuthorizationCodeGrantFilter, as well as the Reactive counterparts. I would like to get this into a patch release.

NOTE: In processAuthorizationResponse(), I believe you can build the redirect-uri for OAuth2AuthorizationResponse as follows:

		MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
		String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
				.replaceQueryParam(OAuth2ParameterNames.CODE, (Object) null)
				.replaceQueryParam(OAuth2ParameterNames.STATE, (Object) null)
				.build()
				.toUriString();
		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OAuth2AuthorizationResponseUtils is package scoped to org.springframework.security.oauth2.client.web, so cannot reference from provider/validator(org.springframework.security.oauth2.client.authentication).

So, ok to make it public??

Copy link
Contributor

@jgrandja jgrandja Oct 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth2AuthorizationExchangeValidator does not need to change. What I was suggesting previously is to add the following to OAuth2AuthorizationResponseUtils:

static OAuth2AuthorizationResponse convert(HttpServletRequest request) {
	MultiValueMap<String, String> params = toMultiMap(request.getParameterMap());
	MultiValueMap<String, String> redirectParams = new LinkedMultiValueMap<>(params);
	redirectParams.remove(OAuth2ParameterNames.CODE);
	redirectParams.remove(OAuth2ParameterNames.STATE);
	String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
			.replaceQueryParams(redirectParams)
			.build()
			.toUriString();
	return convert(params, redirectUri);
}

This utility method can than be used in shouldProcessAuthorizationResponse and processAuthorizationResponse. This will contain the changes only in OAuth2AuthorizationCodeGrantFilter.

Copy link
Contributor Author

@ttddyy ttddyy Oct 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but I think there is a problem when multiple parameters are presented.

For example, following should match:

  • http://localhost/auth?foo=FOO&bar=BAR
  • http://localhost/auth?bar=BAR&foo=FOO

If it does simple string match of redirect_uri in OAuth2AuthorizationExchangeValidator, they are considered as different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is an issue. I'm expecting the provider to perform the redirect using the registered redirect-uri as-is so it should be the same order as specified in ClientRegistration.redirect-uri.

Copy link
Contributor

@jgrandja jgrandja Oct 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested with Okta and it's performing an exact match.

The registered redirect-uri is http://locahost:8080/authorized/okta2?param1=value1&param2=value2.

If I trigger the Authorization Request using the redirect-uri http://locahost:8080/authorized/okta2?param2=value2&param1=value1 it fails.

@@ -122,6 +133,7 @@ public static Builder error(String errorCode) {
private String errorCode;
private String errorDescription;
private String errorUri;
private MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed. Please see previous comment on how to build the OAuth2AuthorizationResponse.redirectUri with query parameters.

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Sep 30, 2019
@jgrandja jgrandja added this to the 5.2.x milestone Sep 30, 2019
@ttddyy ttddyy force-pushed the oauth2-code-grant-redirect-with-query-params branch from 457fb2f to 52dcd13 Compare October 1, 2019 06:10
@ttddyy
Copy link
Contributor Author

ttddyy commented Oct 1, 2019

I have updated the PR

  • just kept the OAuth2AuthorizationExchangeUtils as is due to utility class scope
  • removed parameters from OAuth2AuthorizationResponse
  • updated the way to get redirectUri from http request in OAuth2AuthorizationCodeGrantFilter
  • put back to use authorizationResponse to get redirectUrl

@jgrandja jgrandja changed the title Allow query params in OAuth2 Authorization Request redirect_uri Authorization Response should also match on query parameters Oct 1, 2019
@jgrandja jgrandja added type: bug A general bug and removed type: enhancement A general enhancement labels Oct 1, 2019
@ttddyy ttddyy force-pushed the oauth2-code-grant-redirect-with-query-params branch from 52dcd13 to 8741e19 Compare October 2, 2019 19:44
@ttddyy
Copy link
Contributor Author

ttddyy commented Oct 2, 2019

Updated the PR.

Now, simply it checks the redirected url by removing code and state parameters.

Limitations would be:

  • the redirected url parameters need to be exactly same order in OAuth2AuthorizationRequest#redirectUri
  • Also, after successful authentication, the authorization server cannot add any additional parameters to the redirecting url (in order for exact match)

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy Thank you for the updates. Please see my comment for additional changes.

@@ -166,7 +171,8 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle

MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.replaceQueryParam(OAuth2ParameterNames.CODE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy After reviewing the spec:

The redirection endpoint URI MUST be an absolute URI as defined by
[RFC3986] Section 4.3. The endpoint URI MAY include an
"application/x-www-form-urlencoded" formatted (per Appendix B) query
component ([RFC3986] Section 3.4), which MUST be retained when adding
additional query parameters
. The endpoint URI MUST NOT include a
fragment component.

NOTE the bold highlight. As per spec, the Authorization Server will ADD additional query parameters to the redirect-uri. This means that the query parameters in the redirect_uri (if any) should be in the correct order. Also, in addition to code and state, the Authorization Server may add additional query parameters. I've tested this with google and additional parameters are added on top of code and state. Can you please add a test for this scenario and adjust the code for this.

@ttddyy ttddyy force-pushed the oauth2-code-grant-redirect-with-query-params branch from 8741e19 to 3e75e5c Compare October 27, 2019 05:36
@ttddyy
Copy link
Contributor Author

ttddyy commented Oct 27, 2019

@jgrandja
Updated the logic to handle additional params from auth server.
For test, added a case to simulate additional parameter added by authorization server.
Also, rebased to the latest master.

@@ -166,7 +197,8 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle

MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.replaceQueryParam(OAuth2ParameterNames.CODE)
.replaceQueryParam(OAuth2ParameterNames.STATE)
.build()
.toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the authorization server adds additional parameters to the authorization response, than the OAuth2AuthorizationResponse.redirectUri will contain the additional parameters and result in a failed validation check in OAuth2AuthorizationCodeAuthenticationProvider and the call to OAuth2AuthorizationExchangeValidator.validate() when it compares the redirectUri between the authorization request and response. This revealed that we need an integration test for this scenario. Can you please add an integration test and fix so it passes. Take a look at OAuth2LoginTests or OAuth2LoginConfigurerTests for an example for the integration test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, true.
So, now the question is how you want to share the validation logic.
My original PR introduced OAuth2AuthorizationExchangeUtils class to share the validation logic between OAuth2AuthorizationCodeGrantFilter and OAuth2AuthorizationExchangeValidator. (also OidcAuthorizationCodeAuthenticationProvider). (link bit outdated)

This is because OAuth2AuthorizationResponseUtils seems good place to put the validation logic but it is package scoped class.
Since ~CodeGrantFilter and ~ExchangeValidator are in different package, we need a way to share this validation logic. So, if you don't oppose, I'm going to put back OAuth2AuthorizationExchangeUtils to share the redirect url validation logic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, now the question is how you want to share the validation logic.
My original PR introduced OAuth2AuthorizationExchangeUtils class to share the validation logic between OAuth2AuthorizationCodeGrantFilter and OAuth2AuthorizationExchangeValidator

We do not want to share the logic between OAuth2AuthorizationCodeGrantFilter and OAuth2AuthorizationExchangeValidator. Reason being is that OAuth2AuthorizationCodeGrantFilter is in the web sub-package and the OAuth2AuthorizationExchangeValidator is in the authentication sub-package, which is NOT a web component.

The validation check in OAuth2AuthorizationExchangeValidator can simply be changed to:

if (!authorizationResponse.getRedirectUri().startsWith(authorizationRequest.getRedirectUri()))

It's safe to use startsWith, since we know that any additional query parameters will be appended to the registered redirect-uri

The redirection endpoint URI MUST be an absolute URI as defined by
[RFC3986] Section 4.3. The endpoint URI MAY include an
"application/x-www-form-urlencoded" formatted (per Appendix B) query
component ([RFC3986] Section 3.4), which MUST be retained when adding
additional query parameters
. The endpoint URI MUST NOT include a
fragment component.

@jgrandja jgrandja modified the milestones: 5.2.x, 5.2.2 Nov 11, 2019
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&

if (isValidRedirectUrl(request, authorizationRequest.getRedirectUri()) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy This can be simplified as follows:

		String requestUrl = UrlUtils.buildFullRequestUrl(request);
		MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
		if (requestUrl.startsWith(authorizationRequest.getRedirectUri()) &&
				OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
			return true;
		}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jgrandja
There is a problem with startWith() check which is caught by existing test.

Let's say here is changed to use startWith() as below:

String requestUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
    .replaceQueryParam(OAuth2ParameterNames.CODE)
    .replaceQueryParam(OAuth2ParameterNames.STATE)
    .build()
    .toUriString();
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());

if (requestUrl.startsWith(authorizationRequest.getRedirectUri()) &&
    OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
    return true;
}
return false;

This logic matches when:

  • requestUrl = http://localhost/callback/client-1-no-match
  • authorizationRequest.getRedirectUri() = http://localhost/callback/client-1

Thus, simple startWith() check doesn't work.

So, if we don't want to share the code between filter and validator:

  • put same logic in both place
  • or, maybe perform through check(isValidRedirectUrl) in filter, but simply only do startWith() check in validator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy Yes, you are right, there is a problem with using startsWith(). Instead we should perform exact matching on all parts of the URI, with the exception of the query parameters since there may be additional parameters sent in the authorization response.

In the shouldProcessAuthorizationResponse() method, can you make use of UriComponentsBuilder for both the request (authorization response) and authorizationRequest.getRedirectUri() and perform exact matching on each part, eg. scheme, userInfo, host, port, path. The query parameters (if any) in the authorizationRequest.getRedirectUri() should exist in the same order in request (authorization response).

On a side note, the validation logic for redirect-uri in OAuth2AuthorizationExchangeValidator is redundant. The matching happens in shouldProcessAuthorizationResponse() so we don't need to double validate in OAuth2AuthorizationExchangeValidator. Please remove the existing validation which will simply things.

@ttddyy ttddyy force-pushed the oauth2-code-grant-redirect-with-query-params branch from 3e75e5c to a55ff31 Compare November 11, 2019 23:00
@ttddyy
Copy link
Contributor Author

ttddyy commented Nov 11, 2019

Updated the PR adding an integration test, OAuth2AuthorizationCodeGrantRedirectTests.
Currently some of the tests are failing as expected since the validation logic is not yet updated.
Once we solve the validation logic issue, I'll update the PR.

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy Apologies for the delayed response. Please see my additional comments.

* limitations under the License.
*/

package org.springframework.security.config.web.server;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move to same package as org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurerTests


@Configuration
@EnableWebSecurity
public static class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the configuration to bottom of class.

MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&

if (isValidRedirectUrl(request, authorizationRequest.getRedirectUri()) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttddyy Yes, you are right, there is a problem with using startsWith(). Instead we should perform exact matching on all parts of the URI, with the exception of the query parameters since there may be additional parameters sent in the authorization response.

In the shouldProcessAuthorizationResponse() method, can you make use of UriComponentsBuilder for both the request (authorization response) and authorizationRequest.getRedirectUri() and perform exact matching on each part, eg. scheme, userInfo, host, port, path. The query parameters (if any) in the authorizationRequest.getRedirectUri() should exist in the same order in request (authorization response).

On a side note, the validation logic for redirect-uri in OAuth2AuthorizationExchangeValidator is redundant. The matching happens in shouldProcessAuthorizationResponse() so we don't need to double validate in OAuth2AuthorizationExchangeValidator. Please remove the existing validation which will simply things.

@jgrandja
Copy link
Contributor

jgrandja commented Dec 6, 2019

@ttddyy

the validation logic for redirect-uri in OAuth2AuthorizationExchangeValidator is redundant. The matching happens in shouldProcessAuthorizationResponse() so we don't need to double validate in OAuth2AuthorizationExchangeValidator.

I removed the redundant validation in #7708 to help with this PR.

@ttddyy
Copy link
Contributor Author

ttddyy commented Dec 6, 2019

I removed the redundant validation in #7708 to help with this PR.

Thanks, will work on this ticket early next week with rebasing to the latest.

@ttddyy ttddyy force-pushed the oauth2-code-grant-redirect-with-query-params branch from a55ff31 to 008dbee Compare December 9, 2019 06:41
@ttddyy
Copy link
Contributor Author

ttddyy commented Jan 27, 2020

@jgrandja
Hi, the PR has updated. Can you check.
Thanks,

@jgrandja jgrandja modified the milestones: 5.2.2, 5.2.3 Feb 5, 2020
@jgrandja
Copy link
Contributor

@ttddyy Thank you for the updates. After reviewing, I still feel the logic can be simplified and the code can be reduced further. Our ultimate goal with PR's is to introduce the minimal amount of code required to either fix a bug or introduce a new feature. The main reason is because the more code we add the more we need to maintain. It is definitely a challenging task but it is important.

Having said that, I decided to go ahead and apply the fix myself in this commit 3c86239. Thank you for your understanding.

@jgrandja jgrandja closed this Feb 10, 2020
@jgrandja jgrandja removed this from the 5.2.3 milestone Feb 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: bug A general bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants