spring-security: adding query parameter to authorization_uri creates malformed url

Summary

When creating the authorization uri to login with google, there is the option to add a query parameter in order to get back the refresh token. However, when the authorization_uri is set to:

https://accounts.google.com/o/oauth2/v2/auth?access_type=offline

The uri that I get redirect to is:

https://accounts.google.com/o/oauth2/v2/auth?access_type=offline?response_type=code&client_id=[my client id]&scope=[scopes]&state=[state]&redirect_uri=[redirect uri]

Note the ?access_type=offlince?response_type… This url is malformed and google complains saying response_type and basic query params are not passed in.

Actual Behavior

  1. User goes to /login
  2. User sees an error from Google due to malformed URL

Expected Behavior

  1. User goes to /login
  2. User sees the google login page and the following URL in the address bar: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&client_id=[my client id]&scope=[scopes]&state=[state]&redirect_uri=[redirect uri] The access_type query parameter is after the ? and following query parameters should have an & between them. The order of the query params does not matter.

Configuration

My application.yaml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: xxxxx
            client-secret: yyyyy
            scope: profile,email,https://www.googleapis.com/auth/analytics
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline

My WebSecurityConfigurationAdapter

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2Login()
                    //.loginPage("/login")
                        .defaultSuccessUrl("/dashboard")
                        .failureUrl("/loginFailure")
                    .authorizationEndpoint()
                        .authorizationRequestRepository(authorizationRequestRepository())
                    .and()
                        .tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient());
    }

    @Bean
    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
        HttpSessionOAuth2AuthorizationRequestRepository request = new HttpSessionOAuth2AuthorizationRequestRepository();
        return request;
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        return new NimbusAuthorizationCodeTokenResponseClient();
    }
}

My pom.xml (only including security and oauth2 dependencies)

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
            <version>2.1.0.M2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
            <version>5.1.0.RC1</version>
        </dependency>

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 6
  • Comments: 33 (9 by maintainers)

Commits related to this issue

Most upvoted comments

@jgrandja I have the same issue and was about to post something similar, so I’m pleased I found this issue first. I’ll take a look at using a OAuth2AuthorizatioNRequestResolver

@jgrandja

Thanks for your guidance and quick response! That was definitely the problem. I am still getting familiar with how Kotlin handles or does not handle nulls. Basically I needed to add a bunch of ? and a !! in some of my type declarations and statements to get my IDE to stop bugging me about returning nulls.

For anyone who’s interested in doing the same in Kotlin, this is what my resolver ended up looking like:

class LoginGovAuthorizationRequestResolver(clientRegistryRepository: ClientRegistrationRepository) : OAuth2AuthorizationRequestResolver {

    private val REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"
    private var defaultAuthorizationRequestResolver: OAuth2AuthorizationRequestResolver = DefaultOAuth2AuthorizationRequestResolver(
            clientRegistryRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
    )
    private val authorizationRequestMatcher: AntPathRequestMatcher = AntPathRequestMatcher(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}")

    override fun resolve(request: HttpServletRequest?): OAuth2AuthorizationRequest? {
        val authorizationRequest: OAuth2AuthorizationRequest? = defaultAuthorizationRequestResolver.resolve(request)
        return if(authorizationRequest == null)
        { null } else { customAuthorizationRequest(authorizationRequest) }
    }

    override fun resolve(request: HttpServletRequest?, clientRegistrationId: String?): OAuth2AuthorizationRequest? {
        val authorizationRequest: OAuth2AuthorizationRequest? = defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId)
        return if(authorizationRequest == null)
        { null } else { customAuthorizationRequest(authorizationRequest) }
    }

    private fun customAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest?): OAuth2AuthorizationRequest {

        val registrationId: String = this.resolveRegistrationId(authorizationRequest)
        val additionalParameters = LinkedHashMap(authorizationRequest?.additionalParameters)

        // set login.gov specific params
        if(registrationId == "logingov") {
            additionalParameters["dude"] = "whatever"
        }

        return OAuth2AuthorizationRequest
            .from(authorizationRequest)
            .additionalParameters(additionalParameters)
            .build()
    }

    private fun resolveRegistrationId(authorizationRequest: OAuth2AuthorizationRequest?): String {
        return authorizationRequest!!.additionalParameters[OAuth2ParameterNames.REGISTRATION_ID] as String
    }

}

@jgrandja

Just as a follow up, I if I run in the Intellij IDEA debugger, I catch a breakpoint in this function multiple times, but it never seems to run into the return statement that does the customization – which doesn’t really make any sense to me…

Called multiple times:

override fun resolve(request: HttpServletRequest?): OAuth2AuthorizationRequest {
        val authorizationRequest: OAuth2AuthorizationRequest = defaultAuthorizationRequestResolver.resolve(request)
        return customAuthorizationRequest(authorizationRequest)
    }

Never catches breakpoint: private fun customAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest): OAuth2AuthorizationRequest

UPDATE: It appears that OAuth2AuthorizationRequestRedirectFilter is catching an exception in its doFilterInternal method and calling this.unsuccessfulRedirectForAuthorization(request, response, failed)

This appears to be the root cause of why my customization function is never called. Part of the issue seems to be that the registrationId is null, but I’m not exactly clear on what’s going on.

How do I ensure the proper resolve method gets called so that registrationId is not null?

resolve(request: HttpServletRequest?, clientRegistrationId: String?)

instead of:

resolve(request: HttpServletRequest?)

Just a reminder that using access_type=offline will only return a refresh_token on first access, usually when you give consent. Therefore you need to persist the refresh_token somewhere.

Alternatively you can add query parameter prompt=consent to return a refresh_token every time, but this requires the user to consent every time, which I think is annoying from a user perspective !

@matthewbluezyoncom thank you very much! It worked for me. Now I get it and I will fix my PR accordingly

I got this working so I could add access_type=offline to get a refresh token.

See my code below, from the overidden configure method of my WebSecurityConfigurerAdapter configuration class its a bit rough around the edges but it works.

I decided it was safest to create a new redirect filter based on the default filter and append the extra request parameter on the end of the authorization request uri.

@Override
public void configure(HttpSecurity http) throws Exception {

    if (requireSsl) http.requiresChannel().anyRequest().requiresSecure();

    http
        .authorizeRequests()
            .antMatchers("/css/**","/images/**","/built/**","/error","/login").permitAll()
            .anyRequest().authenticated()
        .and()
            //.csrf().disable()
            .addFilter(new JWTAuthorizationFilter(authenticationManagerBean(), oauthClientService, clientRegistrationRepository))
            // this disables session creation on Spring Security
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .and()
            .logout().permitAll()
        .and()
            .oauth2Login()
            .authorizationEndpoint().authorizationRequestResolver(request -> {
                OAuth2AuthorizationRequest authorizationRequest = new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository,
                        OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI)
                        .resolve(request);
                if (authorizationRequest == null) return null;
                return OAuth2AuthorizationRequest
                        .from(authorizationRequest)
                        .authorizationRequestUri(authorizationRequest.getAuthorizationRequestUri() + "&access_type=offline")
                        .build();
    })
            .and()
            .loginPage("/login");
}

NOTE: Since we are releasing 5.1.0.RC2 on Friday and you won’t get to it until this weekend I pushed this back to 5.1.0

@rwinch absolutely 😃 will try to get it done over the weekend 👍