Skip to content
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

NimbusJwtDecoderJwkSupport should offer method to get OAuth2TokenValidator #6909

Open
TheFonz2017 opened this issue May 28, 2019 · 9 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: feedback-provided Feedback has been provided type: enhancement A general enhancement

Comments

@TheFonz2017
Copy link

Summary

NimbusJwtDecoderJwkSupport is the underlying implementation for Spring Security JwtDecoder.

NimbusJwtDecoderJwkSupport provides a method to setJwtValidator(OAuth2TokenValidator<Jwt>), but it does not have a method to retrieve the set validator(s).

NimbusJwtDecoderJwkSupport is used to auto-configure a JwtDecoder bean in OAuth2ResourceServerJwkConfiguration and its instantiation and especially the set JWT validators depend on the existence of either a JWKS URI or an Issuer URI from the spring.security.oauth2.resourceserver.jwt.issuer-uri.

If an application or library wants to add additional JWT validators, today's guidance in Spring Security documentation is to re-declare a JwtDecoder bean and set the validators on it.

Problem is, that now the application / library has no access to the configured defaults of Spring Security which (as described above) depend on the user's configurations.

Ideally we would like to do something like this:

public class WebSecurityConfigurations extends WebSecurityConfigurerAdapter {
     WebSecurityConfigurations(JwtDecoder standardSpringSecurityJwtDecoder) {
         NimbusJwtDecoderJwkSupport decoder = ((NimbusJwtDecoderJwkSupport) standardSpringSecurityJwtDecoder);
         OAuth2TokenValidator<Jwt> standardValidators = decoder.getValidator(); // does not work today.
         OAuth2TokenValidator<Jwt> myCustomValidor = new MyCustomValidator();
         OAuth2TokenValidator<Jwt> jwtValidators = new DelegatingOAuth2TokenValidator<Jwt>(standardValidators, myCustomValidator);
         decoder.setJwtValidator(jwtValidators);
    }
...
}

As an alternative, it might also be ok to add an addValidator(OAuth2TokenValidator<Jwt>) method to NimbusJwtDecoderJwkSupport, though presumably it's implementation would result in a lot of chained DelegatingOAuth2TokenValidator<Jwt>s.

Actual Behavior

No way for an application to get the OAuth2TokenValidators of the auto-configured standard Spring Security JwtDecoder.

Expected Behavior

A way for an application to get the OAuth2TokenValidators of the auto-configured standard Spring Security JwtDecoder or to add additional custom OAuth2TokenValidator.

References

OAuth2ResourceServerJwkConfiguration (Auto-Configuration):

/*
 * Copyright 2012-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;

/**
 * Configures a {@link JwtDecoder} when a JWK Set URI or OpenID Connect Issuer URI is
 * available.
 *
 * @author Madhura Bhave
 * @author Artsiom Yudovin
 */
@Configuration
class OAuth2ResourceServerJwkConfiguration {

	private final OAuth2ResourceServerProperties properties;

	OAuth2ResourceServerJwkConfiguration(OAuth2ResourceServerProperties properties) {
		this.properties = properties;
	}

	@Bean
	@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
	@ConditionalOnMissingBean
	public JwtDecoder jwtDecoderByJwkKeySetUri() {
		return new NimbusJwtDecoderJwkSupport(this.properties.getJwt().getJwkSetUri());
	}

	@Bean
	@Conditional(IssuerUriCondition.class)
	@ConditionalOnMissingBean
	public JwtDecoder jwtDecoderByIssuerUri() {
		return JwtDecoders
				.fromOidcIssuerLocation(this.properties.getJwt().getIssuerUri());
	}

}

Version

Spring Security Version 5.1.5.RELEASE

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 28, 2019
@jzheaux
Copy link
Contributor

jzheaux commented May 31, 2019

@TheFonz2017, thanks for the report.

Problem is, that now the application / library has no access to the configured defaults of Spring Security which (as described above) depend on the user's configurations.

The current guidance, which you pointed out, is still preferred:

@Bean 
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
    String issuer = properties.getJwt().getIssuerUri();
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuer);
    OAuth2TokenValidator<Jwt> defaults = JwtValidators.createDefaultWithIssuer(issuer);
    DelegatingOAuth2TokenValidator<Jwt> validators = 
            new DelegatingOAuth2TokenValidator<>(defaults, myValidator);
    jwtDecoder.setJwtValidator(validators);
    return jwtDecoder;
}

There's also JwtValidators.createDefault() which leaves out the issuer validation.

If those don't address your issue, can you explain some more detail about what you are trying to do that isn't simple?

@jzheaux jzheaux added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels May 31, 2019
@TheFonz2017
Copy link
Author

Hi Josh,

thanks for your response, and sorry for not being clear enough.

The actual problem is: - NimbusJwtDecoderJwkSupport has a method to setJwtValidator(OAuth2TokenValidator<Jwt>), but it does not have a method to get the validator(s), e.g. OAuth2TokenValidator<Jwt> getJwtValidator().

Why is this a problem? - The current guidance (which you said is still preferred) is fine, if an application wants to expose an entirely new JwtDecoder. However, this is not always (usually?) intended. In my case, I would prefer to get access to the JwtDecoder (an instance of NimbusJwtDecoderJwkSupport) that is auto-configured by Spring Security OAuth2 and add additional TokenValidators to it. For that, I would need a mechanism to first get (not possible today) all the TokenValidators the standard JwtDecoder has configured. Then I would create a DelegatingOAuth2TokenValidator to add my own validators to the standard ones.
Finally I would pass the DelegatingOAuth2TokenValidator to the (existing) set method on NimbusJwtDecoderJwkSupport.

Why not just create a new JwtDecoder as by the preferred guidance? - The answer to this lies in the way Spring Security OAuth2 creates the standard (auto-configured) JwtDecoder instance.
If you dig a little deeper, you will find that Spring Security OAuth2 configures the JwtDecoder - especially its TokenValidators - dependending on the application's / user's configuration (see class OAuth2ResourceServerJwkConfiguration):

  1. If an application provides the spring.security.oauth2.resourceserver.jwt.issuer-uri in its application.yml, then the auto-configured JwtDecoder contains an instance of
    1. JwtTimestampValidator and
    2. JwtIssuerValidator
  2. Whereas, if an application provides the spring.security.oauth2.resourceserver.jwt.jwk-set-uri property in its application.yml instead, the auto-configured JwtDecoder just constains an instance of
    1. JwtTimestampValidator

This behavior makes sense, and differs based on the configuration of the application.

In my case, where I need to add additional custom TokenValidators (not possible today) this now means that in order to have that standard behavior, I need to copy the code of OAuth2ResourceServerJwkConfiguration and with it re-create that behavior.
Of course, copying framework source-code is not the preferred way to deal with this problem.

And this is only necessary, since there is no proper OAuth2TokenValidator<Jwt> getJwtValidator() method available on NimbusJwtDecoderJwkSupport.

Finally, it needs to be said that I am not just writing an application. I am writing a reuse library that's supposed to auto-configure additional token validation logic on top of the standard Spring Security OAuth2 one. That's why enhancing the standard Spring Security behavior is preferred over simply recreating it.

I hope that clarifies it.

Cheers!

@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 Jun 3, 2019
@jzheaux
Copy link
Contributor

jzheaux commented Jul 2, 2019

Related conversation @ #6978 (comment)

@TheFonz2017 I understand your concern about copy-pasting code. I don't think I'm seeing that as a concern in this case, though.

If you want to do exactly what the auto-configuration does, just adding a custom validator, you could do:

@Autowired
public void addValidators(JwtDecoder jwtDecoder) {
    OAuth2TokenValidator<Jwt> defaults = JwtValidators.createDefaultWithIssuer(issuer);
    ((NimbusJwtDecoder) jwtDecoder).setJwtValidator(
        new DelegatingOAuth2TokenValidator<Jwt>(defaults, myCustomValidator));
}

Since JwtDecoders#fromOidcIssuerLocation simply calls JwtValidators, this will give you the same setup as, say:

@Autowired
public void addValidators(JwtDecoder jwtDecoder) {
    OAuth2TokenValidator<Jwt> defaults = jwtDecoder.getJwtValidator();
    ((NimbusJwtDecoder) jwtDecoder).setJwtValidator(
        new DelegatingOAuth2TokenValidator<Jwt>(defaults, myCustomValidator));
}

So, really, I'm not seeing why not having getJwtValidator is creating such heartburn. Can you provide an example of something you are trying to do that is not simple with the existing setup?

@TheFonz2017
Copy link
Author

@jzheaux, thanks for your response.

Note that I do not want to "do exactly as the auto-configuration does", but I want to let the auto-configuration do its thing and afterwards add my custom validators to the ones added by the auto-configuration.

Since the auto-configuration creates two different JwtDecoders (with differing sets of JwtValidators) and these cannot be accessed using a getter, I have no other choice than to duplicate auto-configuration coding.

I will explain, why what you are proposing still leads to duplicating framework coding:

Note: in the meantime OAuth2ResourceServerJwkConfiguration was renamed to OAuth2ResourceServerJwtConfiguration.

Let's look at the class:

@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
public JwtDecoder jwtDecoderByJwkKeySetUri() {
	return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
		.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
}
//...
@Bean
@Conditional(IssuerUriCondition.class)
public JwtDecoder jwtDecoderByIssuerUri() {
	return JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri());
}

These two conditional bean declaration create two differently configured JwtDecoders.

The first one, NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) does the following (see NimbusJwtDecoder:

public final class NimbusJwtDecoder implements JwtDecoder {
        //...
        // !! createDefault() ≠ createDefaultWithIssuer(issuer) (see below) !!
	private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault(); 
        //... 
        private Jwt validateJwt(Jwt jwt){
		OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
                 //...
        }

Where JwtValidators.createDefault() creates a simple JwtTimestampValidator.

The second one JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()) does the following (see JwtDecoders):

public static JwtDecoder fromIssuerLocation(String issuer) {
        //...
	return withProviderConfiguration(configuration, issuer);
}

... doing ...

private static JwtDecoder withProviderConfiguration(Map<String, Object> configuration, String issuer) {
        //...
        // !! createDefaultWithIssuer(issuer) ≠ createDefault() (see above) !!
	OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
	NimbusJwtDecoder jwtDecoder = withJwkSetUri(configuration.get("jwks_uri").toString()).build();
	jwtDecoder.setJwtValidator(jwtValidator);
	return jwtDecoder;
}

... where JwtValidators.createDefaultWithIssuer(issuer) does:

public static OAuth2TokenValidator<Jwt> createDefaultWithIssuer(String issuer) {
	List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
	validators.add(new JwtTimestampValidator());  // same validator as in "default" case
	validators.add(new JwtIssuerValidator(issuer)); // additional validator!!!
	return new DelegatingOAuth2TokenValidator<>(validators);
}

... meaning that there is not just a JwtTimestampValidator but an additional JwtIssuerValidator added to the JwtDecoder.

Thus, the JwtDecoders have different validators depending on whether the framework auto-configuration created them with an issuer URI (as a result of configuring one in application.yml) or not.

So, even if I used your approach (which I tried before), I would have to duplicate the logic of

  1. checking if an issuer URI is configured or not
  2. depending on whether it is configured either
    1. add JwtTimestampValidator and my custom ones or
    2. add JwtTimestampValidator and JwtIssuerValidator and my custom ones.

That is an exact duplication of the auto-configuration coding. And it could / should be avoided by providing a simple getter for the validators.

@jzheaux
Copy link
Contributor

jzheaux commented Jul 4, 2019

@TheFonz2017 I appreciate your analysis, thank you.

Can you show me what code you are trying to write that is cumbersome? It appears to me that that only thing you are replacing is getJwtValidator with JwtValidators.createDefaultWithIssuer, which I don't believe can be argued as being an inconvenience. I believe sample code from your application will go further than analyzing Spring Security's code.

@TheFonz2017
Copy link
Author

Sure Josh,

I am providing a security reuse library that an application shall be able to drop into its classpath.
There it will auto-configure itself under the hood of the application and as part of doing so, will add an additional JwtValidator to the JwtDecoder that the auto-configuration of Spring Security has created. As outlined above, that JwtDecoder configuration depends on the application's configuration (i.e. is an issuer-Id maintained in application.yml or is it just a jwk set URI).

So my code looks as follows:

// Copied from org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwkConfiguration
// Unfortunately there is (today) no better way to get the default configurations for Validators of Spring Security.

@Configuration
@AutoConfigureBefore(OAuth2ResourceServerAutoConfiguration.class)
@ConditionalOnClass(OAuth2ResourceServerProperties.class)
public class XsuaaResourceServerJwkConfiguration {

    private final OAuth2ResourceServerProperties properties;

    public XsuaaResourceServerJwkConfiguration(OAuth2ResourceServerProperties properties) {
        Assert.notNull(properties, "Properties must not be null.");
        this.properties = properties;
    }

    @Bean
    @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
    @ConditionalOnMissingBean
    public JwtDecoder jwtDecoderByJwkKeySetUri(XsuaaServiceBindings xsuaaServiceBindings) {
        String jwkSetUri = this.properties.getJwt().getJwkSetUri();
        OAuth2TokenValidator<Jwt> defaultValidators = JwtValidators.createDefault();
        
        // My custom JwtValidator to be added additional to the ones that are standard in 
        // Spring Security auto-configuration.
        OAuth2TokenValidator<Jwt> xsuaaAudienceValidator = new XsuaaAudienceValidator(xsuaaServiceBindings);

        OAuth2TokenValidator<Jwt> combinedValidators = new DelegatingOAuth2TokenValidator<>(defaultValidators, xsuaaAudienceValidator);
        NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
        jwtDecoder.setJwtValidator(combinedValidators);
        return jwtDecoder;
    }

    @Bean
    @Conditional(IssuerUriCondition.class)
    @ConditionalOnMissingBean
    public JwtDecoder jwtDecoderByIssuerUri(XsuaaServiceBindings xsuaaServiceBindings) {
        String oidcIssuerLocation = this.properties.getJwt().getIssuerUri();
        OAuth2TokenValidator<Jwt> defaultValidators = JwtValidators.createDefaultWithIssuer(oidcIssuerLocation);

        // My custom JwtValidator to be added additional to the ones that are standard in 
        // Spring Security auto-configuration.
        OAuth2TokenValidator<Jwt> xsuaaAudienceValidator = new XsuaaAudienceValidator(xsuaaServiceBindings);

        OAuth2TokenValidator<Jwt> combinedValidators = new DelegatingOAuth2TokenValidator<>(defaultValidators, xsuaaAudienceValidator);
        NimbusJwtDecoderJwkSupport jwtDecoder = nimbusJwtDecoderFromOidcIssuerLocation(oidcIssuerLocation);
        jwtDecoder.setJwtValidator(combinedValidators);
        return jwtDecoder;
    }
    
    protected NimbusJwtDecoderJwkSupport nimbusJwtDecoderFromOidcIssuerLocation(String oidcIssuerLocation) {
        return (NimbusJwtDecoderJwkSupport) JwtDecoders.fromOidcIssuerLocation(oidcIssuerLocation);
    }
}

Note: this code uses the Spring Security APIs of version 5.1.5 - not the current master, which was referenced in the discussion above - but the issue is still exactly the same.

As you can see, this is copying the coding of the original Spring Security org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwkConfiguration simply to add the custom validator.
I would like to avoid that.

@rwinch rwinch changed the title NimbusJwtDecoderJwkSupport shoudl offer method to get OAuth2TokenValidator NimbusJwtDecoderJwkSupport should offer method to get OAuth2TokenValidator Jul 10, 2019
@mrodal
Copy link

mrodal commented Apr 13, 2023

@jzheaux
Copy link
Contributor

jzheaux commented Jun 17, 2024

Thanks for reaching out, @mrodal. Does this feature in Spring Boot address your concern? If so, I think we can close this issue in favor of it.

@jzheaux jzheaux self-assigned this Jun 17, 2024
@jzheaux jzheaux added status: waiting-for-feedback We need additional information before we can continue type: enhancement A general enhancement in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: feedback-provided Feedback has been provided labels Jun 17, 2024
@mrodal
Copy link

mrodal commented Jun 17, 2024

it does, thank you. I believe issue #13249 can also be closed

@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 Jun 17, 2024
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: feedback-provided Feedback has been provided type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants