Closed rigofunc closed 5 years ago
@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 Thanks your reply!
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
@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.
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.
@xyting
if have an
Nginx
betweenUser-Agent
andbackend Spring Security server
, many cases, such asAuthorization 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.
@jgrandja Thanks!
@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.
@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:
redirectUrl
= redirect urlI'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: https://github.com/axios/axios/issues/932#issuecomment-307390761
Let's suppose for a second that it would work, then the process would be the following
4) The SPA does the redirect itself, with replacing the redirectUri
in the returned URL to https://app.example.com/oauth2/callback/google
5) Google's login page is displayed and the end user logs in
6) Google redirects back to https://app.example.com/oauth2/callback/google
7) 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 https://github.com/spring-projects/spring-security/issues/6374, but fortunately OAuth2LoginAuthenticationFilter
was extensible enough and was able to add my own CacheOAuth2AuthorizationRequestRepository
.
8) 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!
@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
Thanks @rwinch for your suggestion and although https://github.com/spring-projects/spring-security/issues/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.
3) 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.**
@laszlocsontos I also solve this by providing custom AuthenticationEntryPoint
, like this:
http.exceptionHandling().authenticationEntryPoint(AjaxSupportedAuthenticationEntryPoint())
@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?
@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.
2) 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?
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.
2) 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.
3) 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.
@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.
@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.
@jgrandja Thanks
@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?
@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.
@jgrandja Oh, I'm dumb, totally missed that. Thank you!
Workaround for setting a custom redirection strategy by
OAuth2AuthorizationRequestRedirectFilter
RedirectionStrategy
fieldCustomAuthorizationRedirectFilter.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);
}
}
Summary
I need
OAuth2AuthorizationRequestRedirectFilter
redirect
to support Ajax requestBackground
I before using
spring-security-oauth2
jar, I can provide customRedirectStrategy
to support Ajax request. Currently, I update my code to usingspring-security-oauth2-client
, one issue is theOAuth2AuthorizationRequestRedirectFilter
cannot customRedirectStrategy
, private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();Version
5.1.4.RELEASE