Skip to content

Use alternate (internal) URL for OIDC configuration during ISS claim validation #11515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
holgerstolzenberg opened this issue Jul 15, 2022 · 17 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue

Comments

@holgerstolzenberg
Copy link

holgerstolzenberg commented Jul 15, 2022

The problem

We currently facing a very special problem using Spring Security OAuth in conjunction with Keycloak in a container cluster (OKD) in a high regulated and networking constraint environment.

Essentially the problem boils down to the fact, that if an issuer-uri is set for a Spring Security oauth-client, the framework attempts to validate the ISS claim and therefore uses the specified URI as connection base for getting the /.well-known/openid-configuration stuff.

As this is hard to describe I have put together a simple schema to showcase the problem:

oauth-iss-claim-problem

Considering the following configuration:

spring:
  security:
     oauth2:
         client:
            provider:
                internal:
                  issuer-uri: https://the.platform.io/auth/realms/the-realm
                  authorization-uri: https://keycloak.cluster.local/auth/realms/the-realm/protocol/openid-connect/auth
                  token-uri: https://keycloak.cluster.local/auth/realms/the-realm/protocol/openid-connect/token
                  jwk-set-uri: https://keycloak.cluster.local/auth/realms/the-realm/protocol/openid-connect/certs
                  user-info-uri: https://keycloak.cluster.local/auth/realms/the-realm/protocol/openid-connect/userinfo

In the given example configuration the framework will attempt to get the OIDC configuration via https://the.platform.io/auth/realms/the-realm which is the outside URI of Keycloak that in our case is not reachable from inside the cluster due to network restrictions.

Yes I might be possible to setup some kind of split horizon DNS, but that makes network setup even more complicated and not well understandable.

The other issue is, that you do not want to do a full HTTP roundtrip to the outside world, if the targeted service (Keycloak) sits nearby in the same subnet.

A lot of tickets out there try to tackle this by having multiple issuer URIs, but all of the stuff I found regarding this solution has been denied or rejected as token validation would get too complicated.

Accidentally, a colleague of mine stumbled upon the following solution:
Instead of having multiple issuer URIs, some folks just use some kind of hash value (instead of a URI) to configure within the ISS claim of the oauth provider and the oauth clients, but unfortunately we cannot do this either with Spring Security nor Keycloak.

We also found the following in Quarkus:
https://quarkus.io/guides/security-openid-connect-multitenancy#quarkus-oidc_quarkus.oidc.token.issuer

Advice appreciated

At the moment we could work around this by leaving away the issuer-uri setting completely but are unsure about this and an advice from Spring Security team if that should be regarded as problematic would be very cool.

Possible solutions

  • A: Provide an separate config property to use for the connection for getting the OIDC configuration
  • B: Introduce a new property like iss-claim that is just a value to match, and still use issuer-uri for connection
  • C: You name it, I guess there might be better options 😎

Feedback very appreciated

✌️

@holgerstolzenberg holgerstolzenberg added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Jul 15, 2022
@holgerstolzenberg
Copy link
Author

holgerstolzenberg commented Jul 15, 2022

Add-On: I forgot to mention that we need to advertise the outside URI in the ISS claim in order for the frontend UI to work running in the client browser.

@olivierboudet
Copy link

olivierboudet commented Jul 15, 2022

Hello, it's funny because I had come here to open the exact same issue but you just did it only 3 hours before 😆

In my case, I am in the process of removing deprecated Keycloak Adapter by Spring Security OAuth and having same kind of issues.
I am using Keycloak with a Frontend URL configured.
My frontend uses a public Keycloak URI (https://mysite.com), the token issuer is my frontend URL.
But my backend should access to Keycloak with an internal service URI in k8s (http://keycloak:8080).

As @holgerstolzenberg described, token validation is failing on "invalid issuer" error.

My configuration :

  spring:
    security:
      oauth2:
        resourceserver:
          jwt:
            issuer-uri: http://keycloak:8080/auth/realms/myrealm
        client:
          registration:
            myclient:
              client-id: myclient
              client-secret: mysecret
              authorization-grant-type: client_credentials
              provider: myprovider
          provider:
            myprovider:
              issuer-uri: http://keycloak:8080/auth/realms/myrealm

This is the full exception I get :

2022-07-15 11:51:23.953 - - - TRACE 1 --- [nio-8080-exec-5] .o.s.r.w.BearerTokenAuthenticationFilter : Failed to process authentication request

org.springframework.security.oauth2.server.resource.InvalidBearerTokenException: Invalid issuer
	at org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver$ResolvingAuthenticationManager.authenticate(JwtIssuerAuthenticationManagerResolver.java:146)
	at org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:130)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:769)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)

As Keycloak returns URIs in openid-configuration with the Frontend URL, the desired behavior in my case might be to configure the token validation with the issuer-uri returned by keycloak openid-configuration endpoint instead of the one directly set in yaml configuration.

PS : I am using spring-security 5.6.1 with spring-boot 2.6.2.

@marcusdacoregio marcusdacoregio added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 15, 2022
@jgrandja
Copy link
Contributor

@holgerstolzenberg Can you provide more details on the specific issue you are having? For example, please provide the stacktrace so I can determine the point in code that is resulting in the error condition.

@jgrandja
Copy link
Contributor

jgrandja commented Jul 18, 2022

@olivierboudet The stacktrace you provided is coming from JwtIssuerAuthenticationManagerResolver so I suspect this might be a misconfiguration? Please review the reference documentation for configuring JwtIssuerAuthenticationManagerResolver and confirm that you have setup the correct issuers. Can you also provide a sample of your configuration?

@jgrandja jgrandja added the status: waiting-for-feedback We need additional information before we can continue label Jul 18, 2022
@olivierboudet
Copy link

Hello @jgrandja, indeed I am using JwtIssuerAuthenticationManagerResolver to manage multi-tenancy.
The issue is exactly as I can't set all issuers in configuration because the FrontendUrl of Keycloak is not reachable from inside the k8s cluster.

Perhaps it would be more clear if I show you the workaround I am using to resolve this issue.

I have a @Configuration class extending WebSecurityConfigurerAdapter with :


    protected final KeycloakProperties keycloakProperties;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();

        keycloakProperties.getIssuers().values().forEach(entry -> {
            JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(entry.get("internal-url"), entry.get("frontend-url")));
            authenticationProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
            authenticationManagers.put(entry.get("frontend-url"), authenticationProvider::authenticate);
        });

        JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
                authenticationManagers::get);

        http.authorizeRequests()
                .antMatchers("/v3/api-docs").permitAll()
                .antMatchers("**").hasAnyRole(role)
                .and()
                .csrf().disable()

                .exceptionHandling()
                .accessDeniedHandler(new KeycloakAccessDeniedHandler(objectMapper))
        ;

        http.oauth2ResourceServer(oauth2 ->
                oauth2.

                        authenticationManagerResolver(authenticationManagerResolver)
        );

    }

KeycloakProperties loads application.yaml properties as following :

    keycloak:
      issuers:
        mysite:
          frontend-url: https://localhost/auth/realms/myRealm
          internal-url: http://keycloak-service/auth/realms/myRealm

And as JwtDecorders and JwtDecoderProviderConfigurationUtils are private, I must duplicate them to allow to build a decoder which is not using the internal url of keycloak but its frontend url instead :

public final class JwtDecoders {

	private JwtDecoders() {
	}

	@SuppressWarnings("unchecked")
	public static <T extends JwtDecoder> T fromOidcIssuerLocation(String oidcIssuerLocation, String frontendUrl) {
		Assert.hasText(oidcIssuerLocation, "oidcIssuerLocation cannot be empty");
		Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
				.getConfigurationForOidcIssuerLocation(oidcIssuerLocation);
		return (T) withProviderConfiguration(configuration, oidcIssuerLocation, frontendUrl);
	}

	private static JwtDecoder withProviderConfiguration(Map<String, Object> configuration, String issuer, String frontendUrl) {
		//JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
		OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(frontendUrl);
		String jwkSetUri = configuration.get("jwks_uri").toString();
		NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
				.jwtProcessorCustomizer(JwtDecoderProviderConfigurationUtils::addJWSAlgorithms).build();
		jwtDecoder.setJwtValidator(jwtValidator);
		return jwtDecoder;
	}

}

I did not change anything in JwtDecoderProviderConfigurationUtils. In JwtDecoders I changed two things :

  • removed JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); because I know it will be different (issuer in configuration is ̀http://keycloak-service:8080/auth ̀ but issuer in ̀openid-configuration ̀ is ̀https://localhost/auth ̀ as set in the Keycloak's FrontendUrl)
  • I am creating the JwtDecoder with FrontendUrl explicitly in JwtDecoders.withProviderConfiguration : OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(frontendUrl);

These changes allow me to workaround the initial issue, but I think this would not be necessary if Spring Security allows to build the decoder with issuers found in the response of request to .well-known/openid-configuration in addition to those found in configuration.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 22, 2022
@xsreality
Copy link

We are facing the exact same problem in our setup and interesting to see the issue raised so recently :)

As @olivierboudet mentioned, the problem is with the fromIssuerLocation/fromOidcIssuerLocation method in JwtDecoders class. It takes the issuer input and uses it for both

  1. Calling the Discovery document (getConfigurationForIssuerLocation()) and
  2. Matching the same input with the response of the Discovery document (JwtDecoderProviderConfigurationUtils.validateIssuer()).

This tightly couples the two behaviours on the same input.

Note that the Discovery document returns the same Issuer irrespective of which URI it is called on.

@jgrandja
Copy link
Contributor

@holgerstolzenberg, @olivierboudet, @xsreality

In case you are not aware, I'd like to refer you to 4.3. OpenID Provider Configuration Validation:

The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.

Hence, the reason for JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer) in JwtDecoders as this is implemented to spec.

There are limitations to customizing the flow when using JwtDecoders so we're looking at adding gh-10309, which will allow customizing the underlying RestOperations used when requesting the provider configuration, as well, a custom OAuth2TokenValidator can be provided to overcome the issuer validation issues you all are having.

I'm leaning towards closing this issue as a duplicate as I feel gh-10309 will provide the flexibility you all are looking for.

@holgerstolzenberg, @olivierboudet, @xsreality Can you confirm if my assumptions are correct? And if so, can you please provide further comments in the other issue so we capture all the requirements you need to make things work?

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jul 25, 2022
@holgerstolzenberg
Copy link
Author

@jgrandja If I understand the mechanics of the referenced issue correctly (without seeing code changes) you are going to introduce a new configuration property that enables us to configure/overwrite the issuer location to be used for ISS claim validation. I guess that might fix our problem.

To be clear, what we need to able to do is to configure a oauth provider whose configured issuer-uri is not necessarily the value provided by the ISS claim.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Aug 1, 2022
@ruckc
Copy link

ruckc commented Aug 1, 2022

We are running into this same issue. Our main issue is while we can do the insane loopback through cloudflare/routing/ingress controllers to a pod sitting in the same local subnet... our development staff, use "localhost" and our spring-boot service can't access keycloak via "localhost"... so we give the issuer-uri as "https://keycloak:8443/auth/realms/whatever", which then means we get "the iss claim is invalid".

I don't really care how we can solve it, but i'm about to have to replace whatever @bean configures the JWT validation, and copy+paste into our code base whatever all the way down the stacktrace... just so I can set a Host header on the request to pull the ${issuer-uri}/.well-known/openid-configuration.

I get being truthful to the OpenID Provider spec, which is why i'm about to go file an issue wherever that is maintained.

@jgrandja
Copy link
Contributor

jgrandja commented Aug 3, 2022

@holgerstolzenberg

you are going to introduce a new configuration property that enables us to configure/overwrite the issuer location to be used for ISS claim validation

No, we will not be introducing a new configuration property.

But before I explain further, I re-read your original issue and my previous comment does not apply to your situation. Sorry for the confusion. My previous comment refers to issuer validation during the initialization of a JwtDecoder component. However, your issue occurs when a ClientRegistration is auto-configured via provider configuration lookup.

Regarding your work around:

At the moment we could work around this by leaving away the issuer-uri setting completely but are unsure about this and an advice from Spring Security team if that should be regarded as problematic would be very cool.

Yes, this is exactly what you would do to bypass issuer validation. This is documented in ClientRegistrations. FYI, the ClientRegistrations utility class has some limitations and we're considering adding a component that provides better flexibility.

Does this answer your question?

My previous comment was referring to issuer validation during the initialization of a JwtDecoder component.

Specifically the configuration:

spring:
    security:
      oauth2:
        resourceserver:
          jwt:
            issuer-uri: http://keycloak:8080/auth/realms/myrealm

After gh-10309 is implemented, you could provide the following configuration:

@Bean
public JwtDecoder jwtDecoder() {
	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
			.withIssuerLocation("https://the.platform.io/auth/realms/the-realm")
			.build();
	jwtDecoder.setJwtValidator(
			JwtValidators.createDefaultWithIssuer("https://keycloak.cluster.local/auth/realms/the-realm")
	);
	return jwtDecoder;
}

With the above configuration, the issuer validation is overridden using JwtValidators.createDefaultWithIssuer("https://keycloak.cluster.local/auth/realms/the-realm"), which will give you totally control on how the issuer is validated.

@jgrandja jgrandja removed the status: feedback-provided Feedback has been provided label Aug 3, 2022
@holgerstolzenberg
Copy link
Author

Yes - that looks good to me. The suggested overridden JwtDecoder looks promising.
We can give that a try.

@olivierboudet
Copy link

It looks good also for me 👍

@sutr90
Copy link

sutr90 commented Sep 16, 2022

@olivierboudet I have come up with a bit simpler workaround. We are in the same boat, backend and frontend URLs, not possible to go around the proxies, etc.

Our solution is following:

public class JwtMultiIssuerDecoder implements JwtDecoder {
    private final NimbusJwtDecoder internalDecoder;
    private final NimbusJwtDecoder publicDecoder;

    public JwtMultiIssuerDecoder(String internalUri, String publicUri){
        internalDecoder = JwtDecoders.fromIssuerLocation(internalUri);
       // This is not an error!!! We need to init the public decoder from the internal URI - the publicUri might not be reachable by this server!
        publicDecoder = JwtDecoders.fromIssuerLocation(internalUri);
        publicDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(publicUri));
    }

    @Override
    public Jwt decode(String token) throws JwtException {
        try {
            return internalDecoder.decode(token);
        } catch (JwtValidationException e){
            return publicDecoder.decode(token);
        }
    }
}

And then just use it like this in your WebSecurityConfig:

@Bean
public JwtDecoder jwtDecoder() {
    return new JwtMultiIssuerDecoder("internal URI", "public URI");
}

This way you do not have to sacrifice the issuer validation, with the cost of validating twice, for the "public" tokens.

@jgrandja
Copy link
Contributor

@holgerstolzenberg, @olivierboudet, @xsreality Closing this in favour of gh-10309

@jgrandja jgrandja added status: duplicate A duplicate of another issue and removed type: enhancement A general enhancement labels Dec 19, 2022
glmanhtu added a commit to chat-socket/identity-authorization-server that referenced this issue Mar 21, 2023
@xsreality
Copy link

xsreality commented May 18, 2023

@jgrandja It is still not clear to me how #10309 solves the issue.

@Bean
public JwtDecoder jwtDecoder() {
	NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
			.withIssuerLocation("https://the.platform.io/auth/realms/the-realm")
			.build();
	jwtDecoder.setJwtValidator(
			JwtValidators.createDefaultWithIssuer("https://keycloak.cluster.local/auth/realms/the-realm")
	);
	return jwtDecoder;
}

The NimbusJwtDecoder.withIssuerLocation() still uses a function that validates the issuer with this line JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); So above code will end up calling the Issuer on the public URL to fetch the configuration and try to validate the issuer which will succeed since the public URL is being used. Later the validation will happen again with the internal URL which will fail as it doesn't match the issuer claim.

I tried above code with 6.1.0 and can confirm above described behaviour. Here's my code:

SecurityConfigurer:

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
                .authorizeExchange().anyExchange().authenticated()
                .and()
                .oauth2ResourceServer(spec ->
                        spec.authenticationManagerResolver(authenticationManagerResolver))
                .build();
    }

Custom AuthenticationManagerResolver:

    @Override
    public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
        return Mono.just(globalAuthenticationManager());
    }

    ReactiveAuthenticationManager globalAuthenticationManager() {
        return new JwtReactiveAuthenticationManager(globalAuthJwtDecoder());
    }

    ReactiveJwtDecoder globalAuthJwtDecoder() {
        List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();

        validators.add(JwtValidators.createDefaultWithIssuer(globalAuthConfig.getIssuerUrlForValidation()));
        validators.add(new JwtClaimValidator<List<String>>(AUD, s -> s.contains(globalAuthConfig.getAudience())));
        validators.add(new JwtClaimValidator<String>(EMAIL, StringUtils::hasText));
        validators.add(new JwtClaimValidator<String>("tenant_short_name", StringUtils::hasText));

        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(validators);

        // we use separate issuerUrl to discover configuration to allow the possibility of
        // keeping the traffic from API Gw -> Authorization server in the internal network
        NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder
                .withIssuerLocation(globalAuthConfig.getIssuerUrlForDiscovery())
                .build();
        jwtDecoder.setJwtValidator(validator);
        return new SupplierReactiveJwtDecoder(() -> jwtDecoder);
    }

@Cherrywoood
Copy link

@ruckc Hello, I have such a problem. Do you know how to solve it? I have a spring app and keycloak running in docker. It is logical that the spring cannot be formed through localhost to keycloak in order to validate the token, however keycloak returns iss with localhost and an external port. I don't know how to solve this problem.

@heruan
Copy link
Contributor

heruan commented Feb 18, 2024

Sorry to bump this closed issue, but it's not clear to me which is the solution. Same situation:

  • client-app and Keycloak running in the same cluster, browser outside
  • client-app uses internal hostname as issuer, but validation fails since Keycloak returns the external hostname for the issuer in metadata, see:

Assert.state(issuer.equals(metadataIssuer),
() -> "The Issuer \"" + metadataIssuer + "\" provided in the configuration metadata did "
+ "not match the requested issuer \"" + issuer + "\"");

I need to fetch metadata from the issuer since some endpoints are read only from metadata, e.g the end_session_endpoint:

ProviderDetails providerDetails = clientRegistration.getProviderDetails();
Object endSessionEndpoint = providerDetails.getConfigurationMetadata().get("end_session_endpoint");

But also other back-channel endpoints without configuration properties in application.yaml.

Since I know both internal and external hostnames, how can I make Spring Security fetch metadata from the internal issuer hostname and accept the external hostname in metadata?

Update I realized this is a different scenario, so I opened a new issue: #14633

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue
Projects
None yet
Development

No branches or pull requests

10 participants