spring-projects / spring-projects/spring-security

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

Open
#19,186 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage type: bug
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading OAuth2AuthorizationServerConfigurer.init() and OAuth2ResourceServerConfigurer.validateConfiguration(), then reproduce the issue with the clientRegistrationEndpoint and opaqueToken configurations shown. Compare initialization in both configuration orders, including authenticationManagerResolver; done means the reported dynamic registration setup has consistent, valid resource-server configuration without relying on ordering.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.