Summary
I want to pass current url(or arbitrary data for example) through OAuth2 state to be able to return to page from which I start OAuth2 authentication. It is written here that
The state parameter serves two functions ... for example, indicating which of your app’s pages to redirect to after authorization.
Currently I need to use reflection (see oAuth2AuthorizationRequestResolver() method in provided example) to achieve it.
Expected Behavior
I would like to have extra constructor in DefaultOAuth2AuthorizationRequestResolver, like
public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository,
String authorizationRequestBaseUri, StringKeyGenerator stateGenerator)
Version
org.springframework.security:spring-security-oauth2-client:5.2.1.RELEASE
Sample
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Autowired
InMemoryClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login(oauth2Login ->
oauth2Login
.authorizationEndpoint(authorizationEndpointConfig ->
authorizationEndpointConfig.authorizationRequestResolver(oAuth2AuthorizationRequestResolver())
)
.successHandler(new OAuth2AuthenticationSuccessHandler())
);
}
@Bean
OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver() {
Field field = ReflectionUtils.findField(DefaultOAuth2AuthorizationRequestResolver.class, "stateGenerator");
ReflectionUtils.makeAccessible(field);
DefaultOAuth2AuthorizationRequestResolver defaultOAuth2AuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
ReflectionUtils.setField(field, defaultOAuth2AuthorizationRequestResolver, new StateKeyGeneratorWithRedirectUrl());
return defaultOAuth2AuthorizationRequestResolver;
}
...
}
public class StateKeyGeneratorWithRedirectUrl extends Base64StringKeyGenerator {
private final StringKeyGenerator generator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
@Override
public String generateKey() {
HttpServletRequest currentHttpRequest = getCurrentHttpRequest();
if (currentHttpRequest!=null){
String referer = currentHttpRequest.getHeader("Referer");
if (!StringUtils.isEmpty(referer)){
return generator.generateKey()+SEPARATOR+referer;
}
}
return generator.generateKey();
}
private HttpServletRequest getCurrentHttpRequest(){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
return ((ServletRequestAttributes)requestAttributes).getRequest();
}
return null;
}
}
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String DEFAULT = "/";
public static final String SEPARATOR = ",";
@Override
protected String determineTargetUrl(HttpServletRequest request,
HttpServletResponse response) {
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.query(request.getQueryString())
.build();
MultiValueMap<String, String> queryParams = uriComponents.getQueryParams();
String stateEncoded = queryParams.getFirst("state");
if (stateEncoded == null) {
return DEFAULT;
}
String stateDecoded = URLDecoder.decode(stateEncoded, StandardCharsets.UTF_8);
String[] split = stateDecoded.split(SEPARATOR);
if (split.length != 2){
return DEFAULT;
} else {
return split[1];
}
}
}
Comment From: jgrandja
@nkonev The value of the state parameter is used internally by the client. It should not be enhanced by the application with additional data, eg. Referer.
Please see spec for details:
state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.
I want to pass current url(or arbitrary data for example) through OAuth2 state
You could store custom attributes in OAuth2AuthorizationRequest.attributes using a custom OAuth2AuthorizationRequestResolver.
I'm going to close this issue and associated PR as the use of custom application data in the state parameter is not recommended. It's strictly meant to be used internally by the client.
Comment From: nkonev
@jgrandja where I should consume these attributes after OAuth2 redirect finished ? How can I match them with attributes stored before?
Or please show me how I can restore initial url (/post/124 for example) after OAuth2 redirect finished.
If it helps - there is example. As user I read https://nkonev.name/post/124. Next I press login link at top of page. Next I press Facebook button. I want to return to https://nkonev.name/post/124, not https://nkonev.name.
Currently I solve it by reflection https://github.com/nkonev/blog/blob/master/backend/src/main/java/com/github/nkonev/blog/security/SecurityConfig.java#L168.
Comment From: jgrandja
@nkonev If you configure SavedRequestAwareAuthenticationSuccessHandler it will redirect back to the saved request. For example, if you try to access /post/124 unauthenticated and it's protected then you'll be redirected to authenticate first. After you authenticate, the SavedRequestAwareAuthenticationSuccessHandler will replay the saved request /post/124.
Comment From: nkonev
@jgrandja
I just had configured SavedRequestAwareAuthenticationSuccessHandler but it do not helped.
I searched usages of RequestCache#saveRequest method - and its written in ExceptionTranslationFilter and OAuth2AuthorizationRequestRedirectFilter in catch blocks.
In my blog example I never hit these catches - so I never save request consequently I always get null SavedRequestAwareAuthenticationSuccessHandler.
In my example there is not common scenario when user hits "protected resource", somewhere caused exception, failing request saves and after user's successful authentication request replayed.
In my scenario I just get more features if I am authenticated user, by analyzing roles in current security context my backend respond flags like: canChangeBackground, canEditPost, canWritePost and so on.
Open https://nkonev.name/post/124 in your browser - it is not protected resource. Next login with facebook(it don't grab email) - you will redirect back (thanks my state hack). None of protected resources called.
So there is no any points where failing request would saved for further restoring.
So do you see any convenient options to achieve that without state parameter ?
As user I read https://nkonev.name/post/124. Next I press login link at top of page. Next I press Facebook button. I want to return to https://nkonev.name/post/124, not https://nkonev.name.
Comment From: nkonev
So seems I found solution without reflection
@Bean
OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver() {
DefaultOAuth2AuthorizationRequestResolver defaultOAuth2AuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, API_LOGIN_OAUTH);
return new WithRefererOAuth2AuthorizationRequestResolver(defaultOAuth2AuthorizationRequestResolver);
}
static class WithRefererOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final DefaultOAuth2AuthorizationRequestResolver delegate;
public WithRefererOAuth2AuthorizationRequestResolver(DefaultOAuth2AuthorizationRequestResolver delegate) {
this.delegate = delegate;
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = delegate.resolve(request);
return patchState(oAuth2AuthorizationRequest);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = delegate.resolve(request, clientRegistrationId);
return patchState(oAuth2AuthorizationRequest);
}
private OAuth2AuthorizationRequest patchState(OAuth2AuthorizationRequest auth2AuthorizationRequest) {
if (auth2AuthorizationRequest == null) {
return null;
}
return OAuth2AuthorizationRequest.from(auth2AuthorizationRequest).state(auth2AuthorizationRequest.getState()+getSeparatorRefererOrEmpty()).build();
}
private String getSeparatorRefererOrEmpty() {
HttpServletRequest currentHttpRequest = getCurrentHttpRequest();
if (currentHttpRequest!=null){
String referer = currentHttpRequest.getHeader("Referer");
if (!StringUtils.isEmpty(referer)){
return SEPARATOR+referer;
}
}
return "";
}
}
Comment From: j-gurda
@jgrandja
I can't entirely agree that the state parameter is meant to be used internally by the client. It's part of OAuth2 specification, and of course, one of its roles is to prevent CSRF. It may be, however, used for other purposes too (specification does not prohibit that), and I don't see a reason to prevent that.
I see two potential improvements to the DefaultOAuth2AuthorizationRequestResolver and stateGenerator itself:
- make it possible to set the custom implementation of stateGenerator for DefaultOAuth2AuthorizationRequestResolver
- make stateGenerator HTTP request aware. Currently, as @nkonev shown in one of the examples, we need to access the HttpServletRequest through the RequestContextHolder - there may be some information we'd like to extract from HttpServletRequest, put it to state, and later use after the whole OAuth2 flow ends. That'd need a change of the stateGenerator interface from StringKeyGenerator to something else. I'd suggest to call it OAuth2StateGenerator with the single method String buildState(HttpServletRequest request).
Usage of OAuth2AuthorizationRequest.attributes may be troublesome since we don't have access to these attributes in implementations of AuthenticationSuccessHandler nor OAuth2UserService. These attributes vanish in OAuth2LoginAuthenticationFilter.attemptAuthentication method, and they are no longer accessible.
I hope my comment in this matter makes sense, and you find my arguments valid. Best regards.
Comment From: jgrandja
@j-gurda
I can't entirely agree that the state parameter is meant to be used internally by the client. It's part of OAuth2 specification, and of course, one of its roles is to prevent CSRF.
Yes, I'm aware of that. The client has been implemented to spec, as defined in 4.1.1. Authorization Request.
It may be, however, used for other purposes too (specification does not prohibit that), and I don't see a reason to prevent that.
Here is what the spec states:
state RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12.
The current implementation does exactly that, correlates the authorization request to the authorization response and also protects against CSRF.
The spec does not explicitly state that this parameter could be used for application-specific state.
If you are looking to store application-specific state, then you can configure a custom OAuth2AuthorizationRequestResolver, which provides access to HttpServletRequest and therefore allows you to store HttpSession.
Comment From: hello-earth-gh
I would like to add that I also was struggling with the same problem, but fortunately, found what jgrandja mentions on 30/01/2020 to stand true. - after finding out that indeed e.g. passing parameters other than "state" to authorization server does not work - these parameters, although being sent to the server, are not returned with the redirect. What worked for me was to access the URL with the additional parameters already there, before Spring Security / OAuth2 kicks off. In my case, though there were two OAuth2 providers, and the default Spring Security OAuth2 page was showing - which can indeed be avoided by providing a custom endpoint which would finally redirect to different URLs according to this: https://stackoverflow.com/questions/55345833/spring-security-oauth-how-to-disable-login-page