Skip to content
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

OAuth2AuthorizationRequestRedirectFilter redirect should support Ajax request #6638

Closed
rigofunc opened this issue Mar 24, 2019 · 22 comments
Closed
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@rigofunc
Copy link

rigofunc commented Mar 24, 2019

Summary

I need OAuth2AuthorizationRequestRedirectFilter redirect to support Ajax request

Background

I before using spring-security-oauth2 jar, I can provide custom RedirectStrategy to support Ajax request. Currently, I update my code to using spring-security-oauth2-client, one issue is the OAuth2AuthorizationRequestRedirectFilter cannot custom RedirectStrategy, private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();

Version

5.1.4.RELEASE

@jgrandja jgrandja self-assigned this Apr 1, 2019
@jgrandja
Copy link
Contributor

jgrandja commented Apr 1, 2019

@xyting Please provide more information as I don't quite understand what you are trying to achieve. NOTE: Exposing the authorizationRedirectStrategy will not necessarily be the right solution for you.

Please provide detailed information on the oauth client you need to configure and the flow / use case you are trying to acheive.

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: waiting-for-feedback We need additional information before we can continue labels Apr 1, 2019
@rigofunc
Copy link
Author

rigofunc commented Apr 4, 2019

@jgrandja Thanks your reply!

Static Resouces, such as SPA, hosted in Nginx

image

Loading Data by AJAX Request

Because all the data loading by AJAX request, and then Nginx request RESTful API servers to get data, RESTful API using Spring Security, it will redirect the end user to CAS, but the AJAX cannot handle the 302 respone
image

@jgrandja
Copy link
Contributor

jgrandja commented Apr 8, 2019

@xyting Diagrams do not provide the detailed information that I need to help you troubleshoot. In the future, it would be very helpful if you could provide a complete and minimal sample that reproduces the issue and share it via a GitHub repository. This will allow us to efficiently troubleshoot and help resolve the issue. The sample should contain the minimum amount of code to reproduce the issue along with detailed steps on how to reproduce.

Having said that, the main purpose of the OAuth2AuthorizationRequestRedirectFilter is to create the Authorization Request and redirect the user-agent to the Authorization Server for authorization. Please review the Authorization Code Grant as per spec for these details.

I'm going to close this issue as OAuth2AuthorizationRequestRedirectFilter is working as designed.

@jgrandja jgrandja closed this as completed Apr 8, 2019
@jgrandja jgrandja added Works as Designed and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 8, 2019
@rigofunc
Copy link
Author

rigofunc commented Apr 9, 2019

Recently, I read the source code. I know what you say. However, if have an Nginx between User-Agent and backend Spring Security server, many cases, such as Authorization Code Grant get the redirect url will be wrong.

@jgrandja
Copy link
Contributor

jgrandja commented Apr 9, 2019

@xyting

if have an Nginx between User-Agent and backend Spring Security server, many cases, such as Authorization Code Grant get the redirect url will be wrong

This seems to be a separate issue? Please keep issues separate going forward. Looks like you need to configure the ForwardedHeaderFilter. Here is the Spring Security reference and Spring Boot reference.

@rigofunc
Copy link
Author

@jgrandja Thanks!

@simpleway
Copy link

simpleway commented Apr 16, 2019

@jgrandja I bump into the same issue as @xyting I did not use customized RedirectStrategy, but use google OpenIDC IdP with OAuth2Login Sample.

After tracing the code a little bit, and found the request matcher logic in OAuth2LoginConfigurer might contribute to this behavior:

		RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage());
		RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico");
		RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http);
		RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher(
				new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);

		LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
		entryPoints.put(new NegatedRequestMatcher(defaultLoginPageMatcher),
				new LoginUrlAuthenticationEntryPoint(providerLoginPage));

		DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);

The defaultEntryPointMatcher will filter out XMLHttpRequest. Should the entryPoints be something like

entryPoints.put(new OrRequestMatcher(new NegatedRequestMatcher(defaultLoginPageMatcher), defaultEntryPointMatcher),
				new LoginUrlAuthenticationEntryPoint(providerLoginPage));

Then the AJAX call to data will simply got 401 instead of a redirect, which the browser will block since it will be a cross domain redirect.

@rigofunc
Copy link
Author

rigofunc commented Apr 22, 2019

@simpleway

Then the AJAX call to data will simply got 401 instead of a redirect, which the browser will block since it will be a cross domain redirect.

maybe as following will be better:

  1. http status: 401;
  2. custom header, such as redirectUrl = redirect url

@laszlocsontos
Copy link

laszlocsontos commented Apr 26, 2019

I've just run into this as well.

Let me explain my use case.

  1. The user is at https://app.example.com/login, note that this is an SPA
  2. Login with Google is clicked
  3. The SPA performs an Ajax POST request to http://api.example.com/oauth2/authorization with Axios. At this point you'll encounter a CORS error. There's no way to prevent the browser from trying to follow that redirect OAuth2AuthorizationRequestRedirectFilter makes. Further details: Need some advice about handling 302 redirects from Ajax axios/axios#932 (comment)

Let's suppose for a second that it would work, then the process would be the following

  1. The SPA does the redirect itself, with replacing the redirectUri in the returned URL to https://app.example.com/oauth2/callback/google
  2. Google's login page is displayed and the end user logs in
  3. Google redirects back to https://app.example.com/oauth2/callback/google
  4. The SPA performs another POST request to https://api.example.com/login/oauth2/code/google and OAuth2LoginAuthenticationFilter processes the request and after persisting the user to the DB, there's such an AuthenticationSuccessHandler which creates a JWT token.

Note: at this point I encountered #6374, but fortunately OAuth2LoginAuthenticationFilter was extensible enough and was able to add my own CacheOAuth2AuthorizationRequestRepository.

  1. The SPA stores the JWT token in local storage and for subsequent API calls, that is used.

@jgrandja For now I have to copy paste OAuth2AuthorizationRequestRedirectFilter and alter it so that it fits that OAuth2 login flow. Could you please modify it in a way that RedirectStrategy modifiable?

Many thanks!

@rwinch
Copy link
Member

rwinch commented Apr 26, 2019

@laszlocsontos I think it makes more sense to edit the AuthenticationEntryPoint which determines at a high level what happens if you are not authenticated. See gh-6812

@laszlocsontos
Copy link

Thanks @rwinch for your suggestion and although #6812 seems similar, I don't have that problem, because I've already added AuthenticationEntryPoint for that purpose, which indeed returns 401.

There are basically three parts of the OAuth2 process.

  1. Request to /oauth2/authorization/google which redirects to Google and saves the OAuth2AuthorizationRequest to a repository (in-memory cache in my case, because my app is completely stateless)

  2. Authorization with Google

  3. Receiving the authorization server's response at /login/oauth2/code/google. At this point OAuth2LoginAuthenticationFilter checks if that OAuth2AuthorizationRequest it received matches with that one previously saved.

Now that we have the big picture, suppose that we have an SPA and a completely stateless back-end with no session management. The SPA calls every endpoint (even /oauth2/authorization/google and /login/oauth2/code/google) with AJAX calls. The SPA redirects to Google and it also receives the callback from Google, but delegates the token exchange to the back-end.

  1. is already sorted out for me, because OAuth2LoginAuthenticationFilter is itself an AbstractAuthenticationProcessingFilter, that is, after consulting with AuthenticationManager, it handles the request with either the configured AuthenticationSuccessHandler or AuthenticationFailureHandler. I also have an AuthenticationEntryPoint, but this is a completely stateless app, thus is just delegates to the failure handler and returns 401.

  2. Works already when the SPA receives the callback from Google, which is a special VusJS route with no view, it calls /login/oauth2/code/google with an AJAX and the configured AuthenticationSuccessHandler creates a JWT token. That is then saved to the browser's local storage. Note that there's another filter based on AbstractAuthenticationProcessingFilter which processes those JWT tokens.

1) The problem is that /oauth2/authorization/google isn't AJAX compatible.

  • OAuth2LoginAuthenticationFilter needs an OAuth2AuthorizationRequest persisted through AuthorizationRequestRepository, if it doesn't exist, it wouldn't carry on. As OAuth2AuthorizationRequestRedirectFilter is that party which creates and saves that request, it cannot be ruled out.
  • OAuth2AuthorizationRequestRedirectFilter redirects no matter what and that cannot be customized, because you're using a hard wired RedirectStrategy.

All that said, this would be the expected behaviour of /oauth2/authorization/*, provided that I could customize it for the use case at hand.

  • It responds with HTTP 200 (or maybe 204) and sends the redirect URL to the SPA. The important point is that this must not be an 302 response.
  • It responds with HTTP 404 if the given registration ID doesn't exist.

@rigofunc
Copy link
Author

rigofunc commented Apr 30, 2019

@laszlocsontos I also solve this by providing custom AuthenticationEntryPoint, like this:

http.exceptionHandling().authenticationEntryPoint(AjaxSupportedAuthenticationEntryPoint())

@laszlocsontos
Copy link

@xyting I've already got a custom AuthenticationEntryPoint, but that doesn't seem to solve this problem for me. Actually I don't see what code path would lead to that AuthenticationEntryPoint from OAuth2AuthorizationRequestRedirectFilter.

Could you please elaborate a bit more on how you've successfully integrated the OAuth2 flow with a single page app?

@jgrandja
Copy link
Contributor

@laszlocsontos It can be challenging integrating a SPA with oauth2Login() and we need to provide better documentation and samples for these use cases. There is some context in #6461, however it's more around native-based apps and is a similar challenge as SPA.

You mention that your app is completely stateless, however, this is not actually true. After you authenticate with Spring Security, there is an authenticated session and therefore an Authentication is stored in the HttpSession (by default). So it's not 100% stateless even though your app might not store any other state in session.

Having the ajax client initiate the authorization request /oauth2/authorization/google and also handle the authorization response to /login/oauth2/code/google is not the way oauth2Login() was meant to be used. As per spec:

The authorization code grant type is used to obtain both access
tokens and refresh tokens and is optimized for confidential clients.
Since this is a redirection-based flow, the client must be capable of
interacting with the resource owner's user-agent (typically a web
browser) and capable of receiving incoming requests (via redirection)

from the authorization server.

It would be quite complicated to implement this with ajax and quite honestly I don't think it's even possible without introducing unnecessary complexity into the mix.

My suggestion is to implement your setup like this:

  1. When the user tries to access your app they will automatically get redirected to Google for authentication, as demonstrated in the following config:
http
	.authorizeRequests()
		.anyRequest().authenticated()
		.and()
	.oauth2Login()
		.loginPage("/oauth2/authorization/google")
                 ...

The end-user agent (browser) is in control here (not ajax), which is how Authorization Code Grant and OpenID Connect is designed for.

  1. After the user authenticates, they will be redirected to /login/oauth2/code/google to complete the authentication process. After authentication is successful, than redirect to the SPA as follows:
http
	.authorizeRequests()
		.anyRequest().authenticated()
		.and()
	.oauth2Login()
		.loginPage("/oauth2/authorization/google")
                 .defaultSuccessUrl("/the-page-that-delivers-spa")
                 ...

After user is redirected to /the-page-that-delivers-spa, the ajax client and SPA can take over and proceed with whatever it does. Make sense?

@laszlocsontos
Copy link

Yes @jgrandja, it's challenging indeed. :)

You mention that your app is completely stateless, however, this is not actually true. After you authenticate with Spring Security, there is an authenticated session and therefore an Authentication is stored in the HttpSession (by default). So it's not 100% stateless even though your app might not store any other state in session.

I've actually disabled that with sessionManagement().sessionCreationPolicy(STATELESS). Does Spring Security store Authentication in the session nonetheless?

Having the ajax client initiate the authorization request /oauth2/authorization/google and also handle the authorization response to /login/oauth2/code/google is not the way oauth2Login() was meant to be used.

Yeah, currently I couldn't achieve that with Spring Security's OAuth2 support indeed, but it would be possible to that with minimal engineering effort I believe. I'll explain below.

It would be quite complicated to implement this with ajax and quite honestly I don't think it's even possible without introducing unnecessary complexity into the mix.

After trying to figure this out for quite a few days, I can say that there are two issues to solve which now prevents Spring Security's OAuth2 support from being a fully SPA friendly.

1) Unable to customize redirect_uri.

You tell Google (or whatever OAuth2) provider that your redirect_uri will be https://spa.example.com/login/code/google. When that happens Google redirect there and the SPA calls back-end https://api.example.com/login/code/google, which happens to be handled by OAuth2LoginAuthenticationFilter.

I've setup AuthenticationSuccessHandler and other handlers to make OAuth2LoginAuthenticationFilter correctly behave with an Ajax client, so it wouldn't redirect, it just generates a JWT token which the SPA would use for subsequent requests.

The problem is that OAuth2LoginAuthenticationFilter want the redirect_uri to be https://api.example.com/login/code/google, but it's https://spa.example.com/login/code/google instead and rejects the request.

String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
	OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
			"Client Registration not found with Id: " + registrationId, null);
	throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}

String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
		.replaceQuery(null)
		.build()
		.toUriString();

OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);

Suggestion: If you used a the Converter interface here instead of a static util class, I could customize that and tell OAuth2LoginAuthenticationFilter the correct redirect_uri is https://spa.example.com/login/code/google

2) OAuth2AuthorizationRequestRedirectFilter redirects not matter what, that doesn't work with an SPA.

Yeah, I know there's redirect is in its name after all. :)

If I could customize OAuth2AuthorizationRequestRedirectFilter thought it's RedirectStrategy it would do something like this.

public class OAuth2InitController {

    private final OAuth2AuthorizationRequestResolver authorizationRequestResolver;
    private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;

    @PostMapping(path = OAUTH2_INIT_REQUEST_URI)
    public HttpEntity<?> init(
            HttpServletRequest request, HttpServletResponse response) {

        OAuth2AuthorizationRequest authorizationRequest;

        try {
            authorizationRequest = authorizationRequestResolver.resolve(request);
        } catch (IllegalArgumentException e) {
            log.debug(e.getMessage(), e);
            return status(NOT_FOUND).body(RestErrorResponse.of(NOT_FOUND, e));
        }

        if (authorizationRequest != null) {
            authorizationRequestRepository.saveAuthorizationRequest(
                    authorizationRequest, request, response
            );

            return ok(authorizationRequest);
        }

        return status(NOT_FOUND).body(RestErrorResponse.of(NOT_FOUND));
    }

}

Suggestion: You could expose that RedirectStrategy to be customizable.

The end-user agent (browser) is in control here (not ajax), which is how Authorization Code Grant and OpenID Connect is designed for.

The reason I'm doing this is because using the IMPLICIT grant is discouraged, because it comes back to the front-end with the access token appended to the URL fragment which is now considered insecure. Using the authentication code grant to advised instead.

After user is redirected to /the-page-that-delivers-spa, the ajax client and SPA can take over and proceed with whatever it does. Make sense?

Okay, so how do I make that redirect?

Should OAuth2AuthorizationRequestRedirectFilter ...

  1. Append the JWT token to the URL? Like this https://spa.example.com/#JWT

That was the argument against using implicit grants. JWTs cannot be revoked, if it's stolen all bets are off.

  1. Set a cookie and just redirect to https://spa.example.com. As CORS is setup the front-end would be able to send cookies back to https://api.example.com

That kind of defeats the purpose of designing a stateless app and requires other strategies for handling pobbile CSRF attacks.

  1. Set a cookie and just redirect to https://spa.example.com. At this point the front-end would call another custom endpoint like /oauth2/finish to get that JWT token, delete the cookie and put it to local storage finally.

That's another round-trip for authentication.


All that said, using IMPLICIT grant is ruled out, CODE remains and it wouldn't practically work without going against the OAuth2 standard in a way the SPA is in control.

I think Spring Security could be a bit more SPA friendly so that folks can build completely stateless apps with it.

For the time being I'll be going with the second option, that is, will set a OAuth2AuthorizationRequestRedirectFilter cookie and redirect.

@jgrandja
Copy link
Contributor

@laszlocsontos I didn't really get a response from you regarding my comments/suggestions. Based on your comments it seems to me that you're doing things differently than how OpenID Connect is designed to work. FYI, there have been a few SPA implementations using oauth2Login() successfully based on feedback from other users over the last while.

Please try the suggestion I have provided to re-configure your application setup. If you're still having issues then the next step is to provide a minimal sample with detailed steps on how to get up and running. Please see https://stackoverflow.com/help/mcve for what the expectation is for a minimal sample. It's much more efficient this way rather than having longer dialogue that can easily get lost in translation.

@jgrandja
Copy link
Contributor

jgrandja commented May 2, 2019

@xyting @laszlocsontos We discovered a bug and the fix has been applied. It may fix the issue you are having. Please see this comment for more details.

@rwinch rwinch added the status: declined A suggestion or change that we don't feel we should currently apply label May 3, 2019
@rigofunc
Copy link
Author

rigofunc commented May 9, 2019

@jgrandja Thanks

@sergeevo
Copy link

sergeevo commented Apr 22, 2021

@jgrandja My team is migrating from Spring Security OAuth 2.5 to Spring Security 5. Previously we were leveraging the capability to set a custom RedirectStrategy using OAuth2ClientContextFilter. We use this to add some custom OIDC query parameters to the authorization request, e.g. acr_values, login_hint, ui_locales. We would like to use OAuth2AuthorizationRequestRedirectFilter but it doesn't allow us to set a custom RedirectStrategy. Is there any chance that a setter could be added in future releases?

@jgrandja
Copy link
Contributor

jgrandja commented Apr 22, 2021

@sergeevo

to add some custom OIDC query parameters to the authorization request

This capability is available. Please review the reference documentation on Customizing the Authorization Request.

@sergeevo
Copy link

sergeevo commented Apr 22, 2021

@jgrandja Oh, I'm dumb, totally missed that. Thank you!

@jessym
Copy link

jessym commented Sep 11, 2021

Workaround for setting a custom redirection strategy by

  • extending the default OAuth2AuthorizationRequestRedirectFilter
  • using a reflection hack to overwrite its RedirectionStrategy field
  • inserting the new filter just before the original filter in the chain

CustomAuthorizationRedirectFilter.java

@Component
public class CustomAuthorizationRedirectFilter extends OAuth2AuthorizationRequestRedirectFilter {

    @SneakyThrows
    public CustomAuthorizationRedirectFilter(
            OAuth2AuthorizationRequestResolver authorizationRequestResolver,
            AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository
    ) {
        super(authorizationRequestResolver);
        super.setAuthorizationRequestRepository(authorizationRequestRepository);
        // Reflection hack to overwrite the parent's redirect strategy
        RedirectStrategy customStrategy = new CustomStrategy();
        Field field = OAuth2AuthorizationRequestRedirectFilter.class.getDeclaredField("authorizationRedirectStrategy");
        field.setAccessible(true);
        field.set(this, customStrategy);
    }

    private static class CustomStrategy implements RedirectStrategy {

        @Override
        public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json");
            response.getWriter().write("{ \"redirectUrl\": \"%s\" }".formatted(url));
        }

    }

}

SecurityConfig.java

@Component
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomAuthorizationRedirectFilter customRedirectFilter;

    @Override
    protected void configure(HttpSecurity httpSecurity) {
        httpSecurity
                .addFilterBefore(this.customRedirectFilter, OAuth2AuthorizationRequestRedirectFilter.class);
    }

}

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) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

7 participants