Skip to content

Enable Client Registration (OAuth2 or OIDC) with Opaque token or authenticationManagerResolver configured will catch error #19186

@herodotus-ecosystem

Description

@herodotus-ecosystem

Environment

My environment is:Spring Security 7.0.5 和 Spring Authorization Server 7.0.5

Reproduce

Spring Authorization Server is used as the authentication server, with Opaque Tokens being used by default. Currently, since we need to support dynamic client registration, I enabled this feature by modifying the code configuration. After enabling dynamic client registration, the system throws an error when started: Spring Security only supports JWTs or Opaque Tokens, not both at the same time..

I thought it was because my code configuration wasn’t compatible with the new version of Spring Authorization Server. So I tried using a custom authenticationManagerResolver to resolve the issue, but I still encountered errors. The error message was as follows:

Caused by: java.lang.IllegalStateException: If an authenticationManagerResolver() is configured, then it takes precedence over any jwt() or opaqueToken() configuration.
	at org.springframework.util.Assert.state(Assert.java:80) ~[spring-core-7.0.7.jar:7.0.7]
	at org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer.validateConfiguration(OAuth2ResourceServerConfigurer.java:324) ~[spring-security-config-7.0.5.jar:7.0.5]
	at org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer.init(OAuth2ResourceServerConfigurer.java:273) ~[spring-security-config-7.0.5.jar:7.0.5]
	at org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer.init(OAuth2ResourceServerConfigurer.java:155) ~[spring-security-config-7.0.5.jar:7.0.5]
	at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.init(AbstractConfiguredSecurityBuilder.java:371) ~[spring-security-config-7.0.5.jar:7.0.5]
	at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:333) ~[spring-security-config-7.0.5.jar:7.0.5]
	at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:38) ~[spring-security-config-7.0.5.jar:7.0.5]
	at cn.herodotus.cloud.authentication.autoconfigure.AuthorizationAutoConfiguration.authorizationServerSecurityFilterChain(AuthorizationAutoConfiguration.java:103) ~[authentication-spring-boot-starter-4.0.6.2.jar:4.0.6.2]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:155) ~[spring-beans-7.0.7.jar:7.0.7]
	... 24 common frames omitted

This issue occurs whether it’s OAuth2 client dynamic registration or OIDC client dynamic registration.

Analyze

Because my own code includes module encapsulation and extensions, making it difficult to understand, I’ll use the following example code to illustrate:

@Bean
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .oauth2AuthorizationServer((authorizationServer) ->
                        authorizationServer
                                .clientRegistrationEndpoint(Customizer.withDefaults())
                )
                .authorizeHttpRequests((authorize) ->
                        authorize.anyRequest().authenticated()
                )
                .oauth2ResourceServer(configurer -> configurer.opaqueToken(Customizer.withDefaults()))
                //.oauth2ResourceServer(configurer -> configurer.authenticationManagerResolver(new CustomAuthenticationManagerResolver))
        return http.build();
    }

After debugging the code, I found that if we follow the previous example approach—first configuring .oauth2AuthorizationServer(), followed by .oauth2ResourceServer(configurer -> configurer.opaqueToken(Customizer.withDefaults()))—then during the execution of http.build(), OAuth2AuthorizationServerConfigurer.init() will execute before OAuth2ResourceServerConfigurer.init().

Since client dynamic registration is enabled, the default JWT configuration will be set in the .oauth2AuthorizationServer() method within the OAuth2AuthorizationServerConfigurer.init() code, as shown below:

if (getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class) != null) {
	httpSecurity
		// Accept access tokens for Client Registration
		.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
}

OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
if (oidcConfigurer != null) {
	if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
			|| oidcConfigurer.getConfigurer(OidcClientRegistrationEndpointConfigurer.class) != null) {
		httpSecurity
			// Accept access tokens for User Info and/or Client Registration
			.oauth2ResourceServer(
					(oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));

	}
}

The process will then enter the OAuth2ResourceServerConfigurer.init() method, where the first step is validateConfiguration(). Since client dynamic registration is already enabled by setting .oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));, configuring .oauth2ResourceServer(configurer -> configurer.opaqueToken(Customizer.withDefaults())) as shown in the previous example or setting .oauth2ResourceServer(configurer -> configurer.authenticationManagerResolver(new CustomAuthenticationManagerResolver)) will both cause the validateConfiguration() method to fail validation.

Resolve

I found ISSUE #16406 and, following the bypass method mentioned in it, placed the configuration of .oauth2Resourserver() before .oauth2AuthorizationServer(). The example code is as follows:

@Bean
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
		        .oauth2ResourceServer(configurer -> configurer.opaqueToken(Customizer.withDefaults()))
                //.oauth2ResourceServer(configurer -> configurer.authenticationManagerResolver(new CustomAuthenticationManagerResolver))
                .oauth2AuthorizationServer((authorizationServer) ->
                        authorizationServer
                                .clientRegistrationEndpoint(Customizer.withDefaults())
                )
                .authorizeHttpRequests((authorize) ->
                        authorize.anyRequest().authenticated()
                )
        return http.build();
    }

This approach can indeed provide temporary solutions to problems.
Debug code found that configuring .oauth2Resourserver() before .oauth2AuthorizationServer() and executing http. build(); At this time, it will become OAuth2ResourceServerConfigurer.init() to be executed before OAuth2AuthorizationServerConfigurer.init().

Whether using OpaqueToken or a custom authenticationManagerResolver, executing OAuth2ResourceServerConfigurer.init() first will pass the validation of the validateConfiguration() method. Execute the code OAuth2AuthorizationServerConfigurer.init() again, even if JWT is configured, it has already bypassed the validation of validateConfiguration()

While this approach allows the code to run successfully, it leads to situations where either “opaque” and JWT are configured simultaneously, or JWT and a custom AuthenticationManagerResolver are configured together. In reality, this violates the constraints specified by validateConfiguration().

Thought process

OAuth2ResourceServerConfigurer.init()validateConfiguration() 代码如下:

private void validateConfiguration() {
	if (this.authenticationManagerResolver == null) {
		Assert.state(this.jwtConfigurer != null || this.opaqueTokenConfigurer != null,
				"Jwt and Opaque Token are the only supported formats for bearer tokens "
						+ "in Spring Security and neither was found. Make sure to configure JWT "
						+ "via http.oauth2ResourceServer().jwt() or Opaque Tokens via "
						+ "http.oauth2ResourceServer().opaqueToken().");
		Assert.state(this.jwtConfigurer == null || this.opaqueTokenConfigurer == null,
				"Spring Security only supports JWTs or Opaque Tokens, not both at the " + "same time.");
	}
	else {
		Assert.state(this.jwtConfigurer == null && this.opaqueTokenConfigurer == null,
				"If an authenticationManagerResolver() is configured, then it takes "
						+ "precedence over any jwt() or opaqueToken() configuration.");
	}
}

Since it is not allowed to set both jwtConfigurator and opaqueTokenConfigurator at the same time, can we consider modifying it as follows: if opaqueTokenConfigurator is configured, jwtConfigurator will be assigned a null value; if jwtConfigurator is configured, opaqueTokenConfigurator will be assigned a null value; if authenticationManagerResolver is configured, opaqueTokenConfigurator and jwtConfigurator will be assigned a null value at the same time

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions