-
Notifications
You must be signed in to change notification settings - Fork 6.1k
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
Authorization Response should also match on query parameters #7329
Conversation
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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??
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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¶m2=value2
.
If I trigger the Authorization Request using the redirect-uri
http://locahost:8080/authorized/okta2?param2=value2¶m1=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<>(); |
There was a problem hiding this comment.
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.
457fb2f
to
52dcd13
Compare
I have updated the PR
|
52dcd13
to
8741e19
Compare
Updated the PR. Now, simply it checks the redirected url by removing Limitations would be:
|
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
8741e19
to
3e75e5c
Compare
@jgrandja |
@@ -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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 introducedOAuth2AuthorizationExchangeUtils
class to share the validation logic betweenOAuth2AuthorizationCodeGrantFilter
andOAuth2AuthorizationExchangeValidator
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.
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); | ||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) && | ||
|
||
if (isValidRedirectUrl(request, authorizationRequest.getRedirectUri()) && |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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 dostartWith()
check in validator
There was a problem hiding this comment.
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.
3e75e5c
to
a55ff31
Compare
Updated the PR adding an integration test, |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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()) && |
There was a problem hiding this comment.
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.
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. |
a55ff31
to
008dbee
Compare
@jgrandja |
@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. |
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
), currentOAuth2AuthorizationCodeGrantFilter
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 registeredredirect_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
sinceauthorizationResponse.getRedirectUri()
does not have query parameters.This might be debatable and also may change based on #7324.