Describe the bug A normally successful (using Postman) OAuth2 authentication using a WordPress OAuth2 plugin as a custom provider fails when using Spring security 6 / OAuth2. The issue seems to be related to the way the state parameter value is handled in org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(HttpServletRequest request, HttpServletResponse response)

I debugged into the method and discovered state value (for example) "affffff1233414313d9d9d9d_adddefefasdfadsfeadf2343%3D" from the original request stored in the saved session then sent out to the OAuth2 provider was being compared with "affffff1233414313d9d9d9d_adddefefasdfadsfeadf2343", in other words, the padding '=' %3D had been stripped. The comparison fails and the original request is not found generating an [authorization_request_not_found] response and overall authentication failure.

To Reproduce I have set up a minimal project which reproduces the issue here: I have set up a minimal project which reproduces the issue here: https://github.com/dannz89/oauth.. I can't include my client id / client secret and site in the project but the WordPress plugin home page is in the project README. But I can provide them privately if you need to test. Or I can provide TRACE logging if that helps.

Expected behavior Authentication should work correctly and redirect back to secured area origionally requested by the browser client. Using Postman to authenticate using the same OAuth2 provider (WordPress plugin described in project README), the authentication works fine using the same values as in application.properties on my local installation. So it seems to be an issue with how spring boot is handling the state parameter.

Sample A link to a GitHub repository with a minimal, reproducible sample.

Comment From: jzheaux

Can you please post the /oauth2/authorize endpoint that Spring Security redirects to, including the state parameter? And can you post also the URL that WordPress POSTs to in your application, again including the state parameter?

In the meantime, can you please try and customize the OAuth2AuthorizationRequestResolver like so:

@Bean 
OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clients) {
    StringKeyGenerator stateGenerator = new HexEncodingStringKeyGenerator();
    DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
        new DefaultOAuth2AuthorizationRequestResolver(clients);
    authorizationRequestResolver.setAuthorizationRequestCustomizer((request) -> request
        .state(stateGenerator.generateKey())
    );
    return authorizationRequestResolver;
}

// ...

@Bean 
public SecurityFilterChain securityFilterChain(
        HttpSecurity http, OAuth2AuthorizationRequestResolver authorizationRequestResolver) throws Exception {
    http
        .oauth2Login((oauth2) -> oauth2
            .authorizationEndpoint((authz) -> authz
                .authorizationRequestResolver(authorizationRequestResolver)
            )
        )
        // ...

    return http.build();
}

and tell me if that allows you to get logged in?

Comment From: dannz89

The endpoints on the WordPress site are: spring.security.oauth2.client.provider.crm.authorization-uri=https://danwilliamsbooks.com/oauth/authorize spring.security.oauth2.client.provider.crm.token-uri=https://danwilliamsbooks.com/oauth/token spring.security.oauth2.client.provider.crm.user-info-uri=https://danwilliamsbooks.com/oauth/me

Outgoing URL to provider authorization: I have changed the client_id so it's the same length and format but not the same as my client_id.

https://danwilliamsbooks.com/oauth/authorize?response_type=code&client_id=0qmKz7oylbF9OYrkQpXy00X9BvnDDPnBLybN1YI8&state=L2kg7TErMCSgPyJ0qPj8SsgXVDgIayRE5RF4P-1XjMs%3D&redirect_uri=http://localhost:8080/login/oauth2/code/crm

Redirect URI (back to my app) - I think that's what you were asking is: http://localhost:8080/login/oauth2/code/crm The GET that comes back looks like this: /login/oauth2/code/crm?code=qj5vna7th3os0vofbjbnly856kkxc2rdpjbe71hq&state=L2kg7TErMCSgPyJ0qPj8SsgXVDgIayRE5RF4P-1XjMs&iframe=break

The code you provided The change alters the behaviour and may move the login a bit further along but still does not quite work. First, it gets when I follow the link to the secured page:

This page isn’t working

localhost redirected you too many times.

Try deleting your cookies. ERR_TOO_MANY_REDIRECTS

I think the correct URL is: localhost:8080/oauth2/authorize/crm. I tried typing that in manually and it logs me on but fails with a different error:

org.springframework.security.oauth2.core.OAuth2AuthenticationException: [missing_user_name_attribute] Missing required "user name" attribute name in UserInfoEndpoint for Client Registration: crm

I don't know if this is a good test because of my manual intervention which generated a mismatch in URLs so I won't look too far into that now.

In any case I had a few issues trying to integrate your code, I couldn't find HexEncodingStringKeyGenerator and my DefaultOAuth2AuthorizationRequestResolver constructor wants two parameters, the second being the authorizationBaseURI (a String). I'm guessing there's a version difference? I'm using spring security 6 and spring boot 3.2.0. In any case, I guessed the URL and wrote a HexEncodingStringKeyGenerator as follows (found it online):

package com.ddt.oauth.configuration;

import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.crypto.codec.Hex;

import java.security.SecureRandom;

public class HexStringKeyGenerator implements StringKeyGenerator {

    private final SecureRandom random = new SecureRandom();
    private final int keyLength;

    public HexStringKeyGenerator(int keyLength) {
        this.keyLength = keyLength;
    }

    @Override
    public String generateKey() {
        byte[] buffer = new byte[keyLength];
        random.nextBytes(buffer);
        return new String(Hex.encode(buffer));
    }
}

The security config now looks like this:

package com.ddt.oauth.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
    @Bean
    OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clients) {

        StringKeyGenerator stateGenerator = new HexStringKeyGenerator(32);
        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(clients,"/oauth2/authorize");
        authorizationRequestResolver.setAuthorizationRequestCustomizer((request) -> request
                .state(stateGenerator.generateKey())
        );
        return authorizationRequestResolver;
    }
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, OAuth2AuthorizationRequestResolver authorizationRequestResolver) throws Exception {
        http.authorizeHttpRequests((authsz) -> authsz
                        .requestMatchers("/", "/home", "/index.html","/error","/error.html","/webjars/**").permitAll()
                        .anyRequest().authenticated())
                .csrf(csrf -> csrf.disable())
                .sessionManagement(sessionConfigg -> sessionConfigg.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
                .oauth2Login(oauth2 -> oauth2
                        .authorizationEndpoint(authsz ->
                                authsz.authorizationRequestResolver(authorizationRequestResolver)));

        return http.build();
    }
}

Let me know if you need more info.

Comment From: jzheaux

Thanks, @dannz89, my mistake on the StringKeyGenerator interface. I answered your question too quickly.

First, given that the Spring Security-generated URI contains the escape character and the WordPress-generated URI does not, I believe you will either need to consult the WordPress plugin or use an alternative state generation strategy.

Second, regarding a custom OAuth2AuthorizationRequestResolver, let me try again here. Please try the following:

@Bean
OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clients) {
    StringKeyGenerator stateGenerator = () -> UUID.randomUUID().toString();
    DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
            new DefaultOAuth2AuthorizationRequestResolver(clients, "/oauth2/authorization");
    authorizationRequestResolver.setAuthorizationRequestCustomizer((request) -> request
            .state(stateGenerator.generateKey())
    );
    return authorizationRequestResolver;
}

Third, regarding your other errors, I think we are starting to leave the confines of your original bug report and we prefer to leave this channel open for those. That said, I believe if you set the user-name-attribute to the name of the claim that WordPress uses for the username, that will alleviate the username error. For the other, consider looking at the network logs (say in Google Chrome) to see which URI Spring Security is redirecting back to WordPress for. There may be a problem with configuration on either side. Either way, please feel free to post those questions to StackOverflow and share the link here, and I'd be happy to help out further over there.

At this point, I'll close this issue as I believe we've confirmed that WordPress is not correctly returning the state that was handed to it.

Comment From: dannz89

Thanks Josh. The amended workaround works fine so I'll contact the OAuth2 provider in any case but leave it at that for now. No need to continue on Stackoverflow and if they put in a fix at any point, I can always back out the workaround and use the default.