Open TheFonz2017 opened 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?
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 TokenValidator
s to it. For that, I would need a mechanism to first get (not possible today) all the TokenValidator
s 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 TokenValidator
s - dependending on the application's / user's configuration (see class OAuth2ResourceServerJwkConfiguration
):
spring.security.oauth2.resourceserver.jwt.issuer-uri
in its application.yml
, then the auto-configured JwtDecoder
contains an instance of
JwtTimestampValidator
andJwtIssuerValidator
spring.security.oauth2.resourceserver.jwt.jwk-set-uri
property in its application.yml
instead, the auto-configured JwtDecoder
just constains an instance of
JwtTimestampValidator
This behavior makes sense, and differs based on the configuration of the application.
In my case, where I need to add additional custom TokenValidator
s (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!
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?
@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 JwtDecoder
s.
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
JwtTimestampValidator
and my custom ones orJwtTimestampValidator
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.
@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.
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.
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
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.
it does, thank you. I believe issue https://github.com/spring-projects/spring-security/issues/13249 can also be closed
Summary
NimbusJwtDecoderJwkSupport
is the underlying implementation for Spring SecurityJwtDecoder
.NimbusJwtDecoderJwkSupport
provides a method tosetJwtValidator(OAuth2TokenValidator<Jwt>)
, but it does not have a method to retrieve the set validator(s).NimbusJwtDecoderJwkSupport
is used to auto-configure aJwtDecoder
bean inOAuth2ResourceServerJwkConfiguration
and its instantiation and especially the set JWT validators depend on the existence of either a JWKS URI or an Issuer URI from thespring.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:
As an alternative, it might also be ok to add an
addValidator(OAuth2TokenValidator<Jwt>)
method toNimbusJwtDecoderJwkSupport
, though presumably it's implementation would result in a lot of chainedDelegatingOAuth2TokenValidator<Jwt>
s.Actual Behavior
No way for an application to get the
OAuth2TokenValidator
s of the auto-configured standard Spring SecurityJwtDecoder
.Expected Behavior
A way for an application to get the
OAuth2TokenValidator
s of the auto-configured standard Spring SecurityJwtDecoder
or to add additional customOAuth2TokenValidator
.References
OAuth2ResourceServerJwkConfiguration (Auto-Configuration):
Version
Spring Security Version 5.1.5.RELEASE