Add withIssuerLocation

Closes gh-10309
This commit is contained in:
Josh Cummings 2023-04-12 15:47:06 -06:00
parent 9ee8202625
commit 76eba9bd0c
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
11 changed files with 667 additions and 67 deletions

View File

@ -327,6 +327,7 @@ This is handy when you need deeper configuration, such as <<webflux-oauth2resour
==== Exposing a `ReactiveJwtDecoder` `@Bean`
Alternately, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
You can construct one with a `jwkSetUri` like so:
====
.Java
@ -343,7 +344,51 @@ public ReactiveJwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
----
====
or you can use the issuer and have `NimbusReactiveJwtDecoder` look up the `jwkSetUri` when `build()` is invoked, like the following:
====
.Java
[source,java,role="primary"]
----
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
}
----
====
Or, if the defaults work for you, you can also use `JwtDecoders`, which does the above in addition to configuring the decoder's validator:
====
.Java
[source,java,role="primary"]
----
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuer);
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuer)
}
----
====
@ -384,7 +429,7 @@ For greater power, though, we can use a builder that ships with `NimbusReactiveJ
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
----
@ -394,7 +439,7 @@ ReactiveJwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
----
@ -408,7 +453,7 @@ Calling `jwsAlgorithm` more than once configures `NimbusReactiveJwtDecoder` to t
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
----
@ -418,7 +463,7 @@ ReactiveJwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
----
@ -432,7 +477,7 @@ Alternately, you can call `jwsAlgorithms`:
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
@ -445,7 +490,7 @@ ReactiveJwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)

View File

@ -430,7 +430,8 @@ This is handy when deeper configuration, like <<oauth2resourceserver-jwt-validat
[[oauth2resourceserver-jwt-decoder-bean]]
=== Exposing a `JwtDecoder` `@Bean`
Or, exposing a <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> `@Bean` has the same effect as `decoder()`:
Or, exposing a <<oauth2resourceserver-jwt-architecture-jwtdecoder,`JwtDecoder`>> `@Bean` has the same effect as `decoder()`.
You can construct one with a `jwkSetUri` like so:
====
.Java
@ -452,6 +453,50 @@ fun jwtDecoder(): JwtDecoder {
----
====
or you can use the issuer and have `NimbusJwtDecoder` look up the `jwkSetUri` when `build()` is invoked, like the following:
====
.Java
[source,java,role="primary"]
----
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withIssuerLocation(issuer).build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withIssuerLocation(issuer).build()
}
----
====
Or, if the defaults work for you, you can also use `JwtDecoders`, which does the above in addition to configuring the decoder's validator:
====
.Java
[source,java,role="primary"]
----
@Bean
public JwtDecoders jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuer);
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun jwtDecoder(): JwtDecoders {
return JwtDecoders.fromIssuerLocation(issuer)
}
----
====
[[oauth2resourceserver-jwt-decoder-algorithm]]
== Configuring Trusted Algorithms
@ -486,7 +531,7 @@ For greater power, though, we can use a builder that ships with `NimbusJwtDecode
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
----
@ -496,7 +541,7 @@ JwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
----
@ -510,7 +555,7 @@ Calling `jwsAlgorithm` more than once will configure `NimbusJwtDecoder` to trust
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
----
@ -520,7 +565,7 @@ JwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
----
@ -534,7 +579,7 @@ Or, you can call `jwsAlgorithms`:
----
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
@ -547,7 +592,7 @@ JwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
@ -1207,7 +1252,7 @@ An individual claim's conversion strategy can be configured using `MappedJwtClai
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
@ -1222,7 +1267,7 @@ JwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
val converter = MappedJwtClaimSetConverter
.withDefaults(mapOf("sub" to this::lookupUserIdBySub))
@ -1319,7 +1364,7 @@ And then, the instance can be supplied like normal:
----
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
@ -1330,7 +1375,7 @@ JwtDecoder jwtDecoder() {
----
@Bean
fun jwtDecoder(): JwtDecoder {
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
return jwtDecoder
}
@ -1358,7 +1403,7 @@ public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
.setReadTimeout(Duration.ofSeconds(60))
.build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
return jwtDecoder;
}
----
@ -1372,7 +1417,7 @@ fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
.setConnectTimeout(Duration.ofSeconds(60))
.setReadTimeout(Duration.ofSeconds(60))
.build()
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build()
return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
}
----
====
@ -1388,7 +1433,7 @@ To adjust the way in which Resource Server caches the JWK set, `NimbusJwtDecoder
----
@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(issuer)
.cache(cacheManager.getCache("jwks"))
.build();
}
@ -1399,7 +1444,7 @@ public JwtDecoder jwtDecoder(CacheManager cacheManager) {
----
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
return NimbusJwtDecoder.withIssuerLocation(issuer)
.cache(cacheManager.getCache("jwks"))
.build()
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@ -42,6 +42,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@ -71,12 +72,16 @@ final class JwtDecoderProviderConfigurationUtils {
}
static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
return getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation)));
return getConfiguration(oidcIssuerLocation, rest, oidc(URI.create(oidcIssuerLocation)));
}
static Map<String, Object> getConfigurationForIssuerLocation(String issuer, RestOperations rest) {
URI uri = URI.create(issuer);
return getConfiguration(issuer, rest, oidc(uri), oidcRfc8414(uri), oauth(uri));
}
static Map<String, Object> getConfigurationForIssuerLocation(String issuer) {
URI uri = URI.create(issuer);
return getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
return getConfigurationForIssuerLocation(issuer, rest);
}
static void validateIssuer(Map<String, Object> configuration, String issuer) {
@ -142,7 +147,7 @@ final class JwtDecoderProviderConfigurationUtils {
return "(unavailable)";
}
private static Map<String, Object> getConfiguration(String issuer, URI... uris) {
private static Map<String, Object> getConfiguration(String issuer, RestOperations rest, URI... uris) {
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
for (URI uri : uris) {
try {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@ -89,9 +89,10 @@ public final class JwtDecoders {
@SuppressWarnings("unchecked")
public static <T extends JwtDecoder> T fromIssuerLocation(String issuer) {
Assert.hasText(issuer, "issuer cannot be empty");
Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(issuer);
return (T) withProviderConfiguration(configuration, issuer);
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
jwtDecoder.setJwtValidator(jwtValidator);
return (T) jwtDecoder;
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -29,6 +29,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.crypto.SecretKey;
@ -200,6 +201,31 @@ public final class NimbusJwtDecoder implements JwtDecoder {
return "Unable to validate Jwt";
}
/**
* Use the given <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
* by making an <a href=
* "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID
* Provider Configuration Request</a> and using the values in the <a href=
* "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
* Provider Configuration Response</a> to derive the needed
* <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
* @param issuer the <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
* @return a {@link JwkSetUriJwtDecoderBuilder} that will derive the JWK Set uri when
* {@link JwkSetUriJwtDecoderBuilder#build} is called
* @since 6.1
* @see JwtDecoders
*/
public static JwkSetUriJwtDecoderBuilder withIssuerLocation(String issuer) {
return new JwkSetUriJwtDecoderBuilder((rest) -> {
Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(issuer, rest);
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
return configuration.get("jwks_uri").toString();
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}
/**
* Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
* uri.
@ -235,7 +261,10 @@ public final class NimbusJwtDecoder implements JwtDecoder {
*/
public static final class JwkSetUriJwtDecoderBuilder {
private String jwkSetUri;
private Function<RestOperations, String> jwkSetUri;
private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
.of(JWSAlgorithm.RS256);
private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@ -247,7 +276,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (rest) -> jwkSetUri;
this.jwtProcessorCustomizer = (processor) -> {
};
}
private JwkSetUriJwtDecoderBuilder(Function<RestOperations, String> jwkSetUri,
Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms) {
Assert.notNull(jwkSetUri, "jwkSetUri function cannot be null");
Assert.notNull(defaultAlgorithms, "defaultAlgorithms function cannot be null");
this.jwkSetUri = jwkSetUri;
this.defaultAlgorithms = defaultAlgorithms;
this.jwtProcessorCustomizer = (processor) -> {
};
}
@ -324,7 +363,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
if (this.signatureAlgorithms.isEmpty()) {
return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
}
Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
@ -334,17 +373,18 @@ public final class NimbusJwtDecoder implements JwtDecoder {
return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
}
JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever) {
JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever, String jwkSetUri) {
if (this.cache == null) {
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);
return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever);
}
JWKSetCache jwkSetCache = new SpringJWKSetCache(this.jwkSetUri, this.cache);
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever, jwkSetCache);
JWKSetCache jwkSetCache = new SpringJWKSetCache(jwkSetUri, this.cache);
return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever, jwkSetCache);
}
JWTProcessor<SecurityContext> processor() {
ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations);
JWKSource<SecurityContext> jwkSource = jwkSource(jwkSetRetriever);
String jwkSetUri = this.jwkSetUri.apply(this.restOperations);
JWKSource<SecurityContext> jwkSource = jwkSource(jwkSetRetriever, jwkSetUri);
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
// Spring Security validates the claim set independent from Nimbus

View File

@ -38,7 +38,6 @@ import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWKSecurityContext;
import com.nimbusds.jose.proc.JWSKeySelector;
@ -211,6 +210,36 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
return "Unable to validate Jwt";
}
/**
* Use the given <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
* by making an <a href=
* "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">OpenID
* Provider Configuration Request</a> and using the values in the <a href=
* "https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">OpenID
* Provider Configuration Response</a> to derive the needed
* <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
* @param issuer the <a href=
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a>
* @return a {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder} that will derive the
* JWK Set uri when {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder#build} is
* called
* @since 6.1
* @see JwtDecoders
*/
public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issuer) {
return new JwkSetUriReactiveJwtDecoderBuilder((web) -> ReactiveJwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(issuer, web).flatMap((configuration) -> {
try {
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
}
catch (IllegalStateException ex) {
return Mono.error(ex);
}
return Mono.just(configuration.get("jwks_uri").toString());
}), ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}
/**
* Use the given <a href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
* uri to validate JWTs.
@ -280,7 +309,10 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE);
private final String jwkSetUri;
private Function<WebClient, Mono<String>> jwkSetUri;
private Function<ReactiveRemoteJWKSource, Mono<Set<JWSAlgorithm>>> defaultAlgorithms = (source) -> Mono
.just(Set.of(JWSAlgorithm.RS256));
private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@ -290,7 +322,16 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor);
}
private JwkSetUriReactiveJwtDecoderBuilder(Function<WebClient, Mono<String>> jwkSetUri,
Function<ReactiveRemoteJWKSource, Mono<Set<JWSAlgorithm>>> defaultAlgorithms) {
Assert.notNull(jwkSetUri, "jwkSetUri cannot be null");
Assert.notNull(defaultAlgorithms, "defaultAlgorithms cannot be null");
this.jwkSetUri = jwkSetUri;
this.defaultAlgorithms = defaultAlgorithms;
this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor);
}
@ -369,30 +410,32 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
return new NimbusReactiveJwtDecoder(processor());
}
JWSKeySelector<JWKSecurityContext> jwsKeySelector(JWKSource<JWKSecurityContext> jwkSource) {
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
if (this.signatureAlgorithms.isEmpty()) {
return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
return this.defaultAlgorithms.apply(source)
.map((algorithms) -> new JWSVerificationKeySelector<>(algorithms, jwkSource));
}
Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
jwsAlgorithms.add(jwsAlgorithm);
}
return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
return Mono.just(new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource));
}
Converter<JWT, Mono<JWTClaimsSet>> processor() {
JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
JWSKeySelector<JWKSecurityContext> jwsKeySelector = jwsKeySelector(jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri.apply(this.webClient));
source.setWebClient(this.webClient);
Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = this.jwtProcessorCustomizer
.apply(source, jwtProcessor)
.map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector = jwsKeySelector(source);
Mono<Tuple2<ConfigurableJWTProcessor<JWKSecurityContext>, Function<JWSAlgorithm, Boolean>>> jwtProcessorMono = jwsKeySelector
.flatMap((selector) -> {
jwtProcessor.setJWSKeySelector(selector);
return this.jwtProcessorCustomizer.apply(source, jwtProcessor);
}).map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector())))
.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
return (jwt) -> {
return jwtProcessorMono.flatMap((tuple) -> {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@ -16,7 +16,10 @@
package org.springframework.security.oauth2.jwt;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.nimbusds.jose.JWSAlgorithm;
@ -31,12 +34,24 @@ import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder;
final class ReactiveJwtDecoderProviderConfigurationUtils {
private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
};
static <C extends SecurityContext> Mono<ConfigurableJWTProcessor<C>> addJWSAlgorithms(
ReactiveRemoteJWKSource jwkSource, ConfigurableJWTProcessor<C> jwtProcessor) {
JWSKeySelector<C> selector = jwtProcessor.getJWSKeySelector();
@ -75,6 +90,56 @@ final class ReactiveJwtDecoderProviderConfigurationUtils {
}).onErrorMap(KeySourceException.class, (ex) -> new IllegalStateException(ex));
}
static Mono<Map<String, Object>> getConfigurationForIssuerLocation(String issuer, WebClient web) {
URI uri = URI.create(issuer);
return getConfiguration(issuer, web, oidc(uri), oidcRfc8414(uri), oauth(uri));
}
private static URI oidc(URI issuer) {
// @formatter:off
return UriComponentsBuilder.fromUri(issuer)
.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
.build(Collections.emptyMap());
// @formatter:on
}
private static URI oidcRfc8414(URI issuer) {
// @formatter:off
return UriComponentsBuilder.fromUri(issuer)
.replacePath(OIDC_METADATA_PATH + issuer.getPath())
.build(Collections.emptyMap());
// @formatter:on
}
private static URI oauth(URI issuer) {
// @formatter:off
return UriComponentsBuilder.fromUri(issuer)
.replacePath(OAUTH_METADATA_PATH + issuer.getPath())
.build(Collections.emptyMap());
// @formatter:on
}
private static Mono<Map<String, Object>> getConfiguration(String issuer, WebClient web, URI... uris) {
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
return Flux.just(uris).concatMap((uri) -> web.get().uri(uri).retrieve().bodyToMono(STRING_OBJECT_MAP))
.flatMap((configuration) -> {
if (configuration.get("jwks_uri") == null) {
return Mono
.error(() -> new IllegalArgumentException("The public JWK set URI must not be null"));
}
return Mono.just(configuration);
})
.onErrorContinue(
(ex) -> ex instanceof WebClientResponseException
&& ((WebClientResponseException) ex).getStatusCode().is4xxClientError(),
(ex, object) -> {
})
.onErrorMap(RuntimeException.class,
(ex) -> (ex instanceof IllegalArgumentException) ? ex
: new IllegalArgumentException(errorMessage, ex))
.next().switchIfEmpty(Mono.error(() -> new IllegalArgumentException(errorMessage)));
}
private ReactiveJwtDecoderProviderConfigurationUtils() {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
@ -45,11 +45,16 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
private WebClient webClient = WebClient.create();
private final String jwkSetURL;
private final Mono<String> jwkSetURL;
ReactiveRemoteJWKSource(String jwkSetURL) {
Assert.hasText(jwkSetURL, "jwkSetURL cannot be empty");
this.jwkSetURL = jwkSetURL;
this.jwkSetURL = Mono.just(jwkSetURL);
}
ReactiveRemoteJWKSource(Mono<String> jwkSetURL) {
Assert.notNull(jwkSetURL, "jwkSetURL cannot be null");
this.jwkSetURL = jwkSetURL.cache();
}
@Override
@ -95,10 +100,10 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
*/
private Mono<JWKSet> getJWKSet() {
// @formatter:off
return this.webClient.get()
.uri(this.jwkSetURL)
return this.jwkSetURL.flatMap((jwkSetURL) -> this.webClient.get()
.uri(jwkSetURL)
.retrieve()
.bodyToMono(String.class)
.bodyToMono(String.class))
.map(this::parse)
.doOnNext((jwkSet) -> this.cachedJWKSet
.set(Mono.just(jwkSet))

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -61,6 +61,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -97,7 +98,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/
public class NimbusJwtDecoderTests {
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
@ -312,6 +313,19 @@ public class NimbusJwtDecoderTests {
}
}
@Test
public void decodeWhenIssuerLocationThenOk() {
String issuer = "https://example.org/issuer";
RestOperations restOperations = mock(RestOperations.class);
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class))).willReturn(
new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
// @formatter:off

View File

@ -41,7 +41,6 @@ import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
import com.nimbusds.jose.proc.JWKSecurityContext;
import com.nimbusds.jose.proc.JWSKeySelector;
@ -58,6 +57,7 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@ -597,11 +597,29 @@ public class NimbusReactiveJwtDecoderTests {
// @formatter:on
}
@Test
public void decodeWhenIssuerLocationThenOk() {
String issuer = "https://example.org/issuer";
WebClient real = WebClient.builder().build();
WebClient.RequestHeadersUriSpec spec = spy(real.get());
WebClient webClient = spy(WebClient.class);
given(webClient.get()).willReturn(spec);
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
given(spec.retrieve()).willReturn(responseSpec);
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer).webClient(webClient)
.build();
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
}
@Test
public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() {
JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsKeySelector(jwkSource);
.jwsKeySelector(jwkSource).block();
assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue();
@ -609,9 +627,9 @@ public class NimbusReactiveJwtDecoderTests {
@Test
public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() {
JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS512).jwsKeySelector(jwkSource);
.jwsAlgorithm(SignatureAlgorithm.RS512).jwsKeySelector(jwkSource).block();
assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
JWSVerificationKeySelector<JWKSecurityContext> jwsVerificationKeySelector = (JWSVerificationKeySelector<JWKSecurityContext>) jwsKeySelector;
assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue();
@ -619,12 +637,12 @@ public class NimbusReactiveJwtDecoderTests {
@Test
public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() {
JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);
ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
// @formatter:off
JWSKeySelector<JWKSecurityContext> jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS256)
.jwsAlgorithm(SignatureAlgorithm.RS512)
.jwsKeySelector(jwkSource);
.jwsKeySelector(jwkSource).block();
// @formatter:on
assertThat(jwsKeySelector instanceof JWSVerificationKeySelector);
JWSVerificationKeySelector<?> jwsAlgorithmMapKeySelector = (JWSVerificationKeySelector<?>) jwsKeySelector;

View File

@ -0,0 +1,319 @@
/*
* Copyright 2002-2023 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
*
* https://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.security.oauth2.jwt;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ReactiveJwtDecoderProviderConfigurationUtils}
*
* @author Josh Cummings
*/
public class ReactiveJwtDecoderProviderConfigurationUtilsTests {
/**
* Contains those parameters required to construct a ReactiveJwtDecoder as well as any
* required parameters
*/
// @formatter:off
private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n"
+ " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n"
+ " \"id_token_signing_alg_values_supported\": [\n"
+ " \"RS256\"\n"
+ " ], \n"
+ " \"issuer\": \"%s\", \n"
+ " \"jwks_uri\": \"%s/.well-known/jwks.json\", \n"
+ " \"response_types_supported\": [\n"
+ " \"code\", \n"
+ " \"token\", \n"
+ " \"id_token\", \n"
+ " \"code token\", \n"
+ " \"code id_token\", \n"
+ " \"token id_token\", \n"
+ " \"code token id_token\", \n"
+ " \"none\"\n"
+ " ], \n"
+ " \"subject_types_supported\": [\n"
+ " \"public\"\n"
+ " ], \n"
+ " \"token_endpoint\": \"https://example.com/oauth2/v4/token\"\n"
+ "}";
// @formatter:on
private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
private final WebClient web = WebClient.builder().build();
private MockWebServer server;
private String issuer;
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.issuer = createIssuerFromServer();
this.issuer += "path";
}
@AfterEach
public void cleanup() throws Exception {
this.server.shutdown();
}
@Test
public void issuerWhenResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
prepareConfigurationResponse();
Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(this.issuer, this.web).block();
assertThat(configuration).containsKey("jwks_uri");
}
@Test
public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
prepareConfigurationResponseOidc();
Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(this.issuer, this.web).block();
assertThat(configuration).containsKey("jwks_uri");
}
@Test
public void issuerWhenOAuth2ResponseIsTypicalThenReturnedConfigurationContainsJwksUri() {
prepareConfigurationResponseOAuth2();
Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(this.issuer, this.web).block();
assertThat(configuration).containsKey("jwks_uri");
}
@Test
public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() {
prepareConfigurationResponseOidc("{ \"missing_required_keys\" : \"and_values\" }");
// @formatter:off
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
// @formatter:on
}
@Test
public void issuerWhenOAuth2ResponseIsNonCompliantThenThrowsRuntimeException() {
prepareConfigurationResponseOAuth2("{ \"missing_required_keys\" : \"and_values\" }");
// @formatter:off
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
// @formatter:on
}
// gh-7512
@Test
public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
throws JsonMappingException, JsonProcessingException {
prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri());
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block())
.withMessage("The public JWK set URI must not be null");
// @formatter:on
}
// gh-7512
@Test
public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
throws JsonMappingException, JsonProcessingException {
prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri());
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block())
.withMessage("The public JWK set URI must not be null");
// @formatter:on
}
@Test
public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() {
prepareConfigurationResponseOidc("malformed");
// @formatter:off
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
// @formatter:on
}
@Test
public void issuerWhenOAuth2ResponseIsMalformedThenThrowsRuntimeException() {
prepareConfigurationResponseOAuth2("malformed");
// @formatter:off
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block());
// @formatter:on
}
@Test
public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareConfigurationResponseOidc(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer));
// @formatter:off
assertThatIllegalStateException()
.isThrownBy(() -> {
Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block();
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer);
});
// @formatter:on
}
@Test
public void issuerWhenOAuth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() {
prepareConfigurationResponseOAuth2(
String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer));
// @formatter:off
assertThatIllegalStateException()
.isThrownBy(() -> {
Map<String, Object> configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block();
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer);
});
// @formatter:on
}
@Test
public void issuerWhenOidcFallbackRequestedIssuerIsUnresponsiveThenThrowsIllegalArgumentException()
throws Exception {
this.server.shutdown();
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation("https://issuer", this.web).block());
// @formatter:on
}
private void prepareConfigurationResponse() {
String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
prepareConfigurationResponse(body);
}
private void prepareConfigurationResponse(String body) {
this.server.enqueue(response(body));
this.server.enqueue(response(JWK_SET));
}
private void prepareConfigurationResponseOidc() {
String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
prepareConfigurationResponseOidc(body);
}
private void prepareConfigurationResponseOidc(String body) {
Map<String, MockResponse> responses = new HashMap<>();
responses.put(oidc(), response(body));
responses.put(jwks(), response(JWK_SET));
prepareConfigurationResponses(responses);
}
private void prepareConfigurationResponseOAuth2() {
String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer);
prepareConfigurationResponseOAuth2(body);
}
private void prepareConfigurationResponseOAuth2(String body) {
Map<String, MockResponse> responses = new HashMap<>();
responses.put(oauth(), response(body));
responses.put(jwks(), response(JWK_SET));
prepareConfigurationResponses(responses);
}
private void prepareConfigurationResponses(Map<String, MockResponse> responses) {
Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
// @formatter:off
return Optional.of(request)
.map(RecordedRequest::getRequestUrl)
.map(HttpUrl::toString)
.map(responses::get)
.orElse(new MockResponse().setResponseCode(404));
// @formatter:on
}
};
this.server.setDispatcher(dispatcher);
}
private String createIssuerFromServer() {
return this.server.url("").toString();
}
private String oidc() {
URI uri = URI.create(this.issuer);
// @formatter:off
return UriComponentsBuilder.fromUri(uri)
.replacePath(uri.getPath() + OIDC_METADATA_PATH)
.toUriString();
// @formatter:on
}
private String oauth() {
URI uri = URI.create(this.issuer);
// @formatter:off
return UriComponentsBuilder.fromUri(uri)
.replacePath(OAUTH_METADATA_PATH + uri.getPath())
.toUriString();
// @formatter:on
}
private String jwks() {
return this.issuer + "/.well-known/jwks.json";
}
private MockResponse response(String body) {
// @formatter:off
return new MockResponse().setBody(body)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// @formatter:on
}
public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE,
new TypeReference<Map<String, Object>>() {
});
response.remove("jwks_uri");
return mapper.writeValueAsString(response);
}
}