spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.78k stars 5.89k forks source link

NimbusJwtDecoderJwkSupport should offer method to get OAuth2TokenValidator #6909

Open TheFonz2017 opened 5 years ago

TheFonz2017 commented 5 years ago

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

jzheaux commented 5 years ago

@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?

TheFonz2017 commented 5 years ago

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!

jzheaux commented 5 years ago

Related conversation @ https://github.com/spring-projects/spring-security/pull/6978#issuecomment-502380146

@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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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.

mrodal commented 1 year ago

are there any plans to provide access to the set of validators? now validators are even more scattered, heres another one: https://github.com/spring-projects/spring-boot/blob/2.7.x/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java#L105

jzheaux commented 4 months ago

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.

mrodal commented 4 months ago

it does, thank you. I believe issue https://github.com/spring-projects/spring-security/issues/13249 can also be closed