Make jwks_uri optional for RFC 8414 and Required for OpenID Connect
OpenID Connect Discovery 1.0 expects the OpenId Provider Metadata response is expected to return a valid jwks_uri, however, this field is optional in the Authorization Server Metadata response as per RFC 8414 specification. Fixes gh-7512
This commit is contained in:
parent
e1fad001d9
commit
58ca81d500
|
@ -21,6 +21,7 @@ import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import com.nimbusds.oauth2.sdk.GrantType;
|
import com.nimbusds.oauth2.sdk.GrantType;
|
||||||
import com.nimbusds.oauth2.sdk.ParseException;
|
import com.nimbusds.oauth2.sdk.ParseException;
|
||||||
|
@ -86,10 +87,7 @@ public final class ClientRegistrations {
|
||||||
*/
|
*/
|
||||||
public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
|
public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) {
|
||||||
Assert.hasText(issuer, "issuer cannot be empty");
|
Assert.hasText(issuer, "issuer cannot be empty");
|
||||||
Map<String, Object> configuration = getConfiguration(issuer, oidc(URI.create(issuer)));
|
return getBuilder(issuer, oidc(URI.create(issuer)));
|
||||||
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
|
|
||||||
return withProviderConfiguration(metadata, issuer)
|
|
||||||
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,42 +135,68 @@ public final class ClientRegistrations {
|
||||||
public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
|
public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
|
||||||
Assert.hasText(issuer, "issuer cannot be empty");
|
Assert.hasText(issuer, "issuer cannot be empty");
|
||||||
URI uri = URI.create(issuer);
|
URI uri = URI.create(issuer);
|
||||||
Map<String, Object> configuration = getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
|
return getBuilder(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
|
||||||
AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse);
|
|
||||||
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer);
|
|
||||||
String userinfoEndpoint = (String) configuration.get("userinfo_endpoint");
|
|
||||||
if (userinfoEndpoint != null) {
|
|
||||||
builder.userInfoUri(userinfoEndpoint);
|
|
||||||
}
|
|
||||||
return builder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URI oidc(URI issuer) {
|
private static Supplier<ClientRegistration.Builder> oidc(URI issuer) {
|
||||||
return UriComponentsBuilder.fromUri(issuer)
|
URI uri = UriComponentsBuilder.fromUri(issuer)
|
||||||
.replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap());
|
.replacePath(issuer.getPath() + OIDC_METADATA_PATH).build(Collections.emptyMap());
|
||||||
|
|
||||||
|
return () -> {
|
||||||
|
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
||||||
|
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
|
||||||
|
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
|
||||||
|
return withProviderConfiguration(metadata, issuer.toASCIIString())
|
||||||
|
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
||||||
|
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URI oidcRfc8414(URI issuer) {
|
private static Supplier<ClientRegistration.Builder> oidcRfc8414(URI issuer) {
|
||||||
return UriComponentsBuilder.fromUri(issuer)
|
URI uri = UriComponentsBuilder.fromUri(issuer)
|
||||||
.replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
|
.replacePath(OIDC_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
|
||||||
|
return getRfc8414Builder(issuer, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URI oauth(URI issuer) {
|
private static Supplier<ClientRegistration.Builder> oauth(URI issuer) {
|
||||||
return UriComponentsBuilder.fromUri(issuer)
|
URI uri = UriComponentsBuilder.fromUri(issuer)
|
||||||
.replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
|
.replacePath(OAUTH_METADATA_PATH + issuer.getPath()).build(Collections.emptyMap());
|
||||||
|
return getRfc8414Builder(issuer, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, Object> getConfiguration(String issuer, URI... uris) {
|
private static Supplier<ClientRegistration.Builder> getRfc8414Builder(URI issuer, URI uri) {
|
||||||
|
return () -> {
|
||||||
|
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
||||||
|
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
|
||||||
|
AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse);
|
||||||
|
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString());
|
||||||
|
|
||||||
|
URI jwkSetUri = metadata.getJWKSetURI();
|
||||||
|
if (jwkSetUri != null) {
|
||||||
|
builder.jwkSetUri(jwkSetUri.toASCIIString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String userinfoEndpoint = (String) configuration.get("userinfo_endpoint");
|
||||||
|
if (userinfoEndpoint != null) {
|
||||||
|
builder.userInfoUri(userinfoEndpoint);
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
private static ClientRegistration.Builder getBuilder(String issuer, Supplier<ClientRegistration.Builder>... suppliers) {
|
||||||
String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"";
|
String errorMessage = "Unable to resolve Configuration with the provided Issuer of \"" + issuer + "\"";
|
||||||
for (URI uri : uris) {
|
for (Supplier<ClientRegistration.Builder> supplier : suppliers) {
|
||||||
try {
|
try {
|
||||||
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
return supplier.get();
|
||||||
return rest.exchange(request, typeReference).getBody();
|
|
||||||
} catch (HttpClientErrorException e) {
|
} catch (HttpClientErrorException e) {
|
||||||
if (!e.getStatusCode().is4xxClientError()) {
|
if (!e.getStatusCode().is4xxClientError()) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// else try another endpoint
|
// else try another endpoint
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
|
throw e;
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
throw new IllegalArgumentException(errorMessage, e);
|
throw new IllegalArgumentException(errorMessage, e);
|
||||||
}
|
}
|
||||||
|
@ -219,7 +243,6 @@ public final class ClientRegistrations {
|
||||||
.clientAuthenticationMethod(method)
|
.clientAuthenticationMethod(method)
|
||||||
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
|
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
|
||||||
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
|
.authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString())
|
||||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
|
||||||
.providerConfigurationMetadata(configurationMetadata)
|
.providerConfigurationMetadata(configurationMetadata)
|
||||||
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
|
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
|
||||||
.clientName(issuer);
|
.clientName(issuer);
|
||||||
|
|
|
@ -168,6 +168,33 @@ public class ClientRegistrationsTest {
|
||||||
"grant_types_supported", "token_endpoint", "token_endpoint_auth_methods_supported", "userinfo_endpoint");
|
"grant_types_supported", "token_endpoint", "token_endpoint_auth_methods_supported", "userinfo_endpoint");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenResponseMissingJwksUriThenThrowsIllegalArgumentException() throws Exception {
|
||||||
|
this.response.remove("jwks_uri");
|
||||||
|
assertThatThrownBy(() -> registration("").build())
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOidcFallbackResponseMissingJwksUriThenThrowsIllegalArgumentException() throws Exception {
|
||||||
|
this.response.remove("jwks_uri");
|
||||||
|
assertThatThrownBy(() -> registrationOidcFallback("issuer1", null).build())
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOAuth2ResponseMissingJwksUriThenThenSuccess() throws Exception {
|
||||||
|
this.response.remove("jwks_uri");
|
||||||
|
ClientRegistration registration = registrationOAuth2("", null).build();
|
||||||
|
ClientRegistration.ProviderDetails provider = registration.getProviderDetails();
|
||||||
|
assertThat(provider.getJwkSetUri()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
|
public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
|
||||||
assertThat(registration("")).isNotNull();
|
assertThat(registration("")).isNotNull();
|
||||||
|
|
|
@ -33,6 +33,7 @@ import java.util.Map;
|
||||||
* issuer and method invoked.
|
* issuer and method invoked.
|
||||||
*
|
*
|
||||||
* @author Thomas Vitale
|
* @author Thomas Vitale
|
||||||
|
* @author Rafiullah Hamedy
|
||||||
* @since 5.2
|
* @since 5.2
|
||||||
*/
|
*/
|
||||||
class JwtDecoderProviderConfigurationUtils {
|
class JwtDecoderProviderConfigurationUtils {
|
||||||
|
@ -69,7 +70,15 @@ class JwtDecoderProviderConfigurationUtils {
|
||||||
try {
|
try {
|
||||||
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
RequestEntity<Void> request = RequestEntity.get(uri).build();
|
||||||
ResponseEntity<Map<String, Object>> response = rest.exchange(request, typeReference);
|
ResponseEntity<Map<String, Object>> response = rest.exchange(request, typeReference);
|
||||||
return response.getBody();
|
Map<String, Object> configuration = response.getBody();
|
||||||
|
|
||||||
|
if (configuration.get("jwks_uri") == null) {
|
||||||
|
throw new IllegalArgumentException("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw e;
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
if (!(e instanceof HttpClientErrorException &&
|
if (!(e instanceof HttpClientErrorException &&
|
||||||
((HttpClientErrorException) e).getStatusCode().is4xxClientError())) {
|
((HttpClientErrorException) e).getStatusCode().is4xxClientError())) {
|
||||||
|
|
|
@ -33,6 +33,11 @@ import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
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 static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
|
||||||
|
@ -163,6 +168,36 @@ public class JwtDecodersTests {
|
||||||
.isInstanceOf(RuntimeException.class);
|
.isInstanceOf(RuntimeException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponse(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> JwtDecoders.fromOidcIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> JwtDecoders.fromIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() {
|
public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() {
|
||||||
prepareConfigurationResponse("malformed");
|
prepareConfigurationResponse("malformed");
|
||||||
|
@ -294,4 +329,12 @@ public class JwtDecodersTests {
|
||||||
.setBody(body)
|
.setBody(body)
|
||||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2019 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -33,12 +33,18 @@ import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
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 static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ReactiveJwtDecoders}
|
* Tests for {@link ReactiveJwtDecoders}
|
||||||
*
|
*
|
||||||
* @author Josh Cummings
|
* @author Josh Cummings
|
||||||
|
* @author Rafiullah Hamedy
|
||||||
*/
|
*/
|
||||||
public class ReactiveJwtDecodersTests {
|
public class ReactiveJwtDecodersTests {
|
||||||
/**
|
/**
|
||||||
|
@ -76,14 +82,12 @@ public class ReactiveJwtDecodersTests {
|
||||||
|
|
||||||
private MockWebServer server;
|
private MockWebServer server;
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private String jwkSetUri;
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
this.server.start();
|
this.server.start();
|
||||||
this.issuer = createIssuerFromServer();
|
this.issuer = createIssuerFromServer();
|
||||||
this.jwkSetUri = this.issuer + ".well-known/jwks.json";
|
|
||||||
this.issuer += "path";
|
this.issuer += "path";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +151,36 @@ public class ReactiveJwtDecodersTests {
|
||||||
.isInstanceOf(RuntimeException.class);
|
.isInstanceOf(RuntimeException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponse(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> ReactiveJwtDecoders.fromOidcIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-7512
|
||||||
|
@Test
|
||||||
|
public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException()
|
||||||
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri());
|
||||||
|
assertThatCode(() -> ReactiveJwtDecoders.fromIssuerLocation(this.issuer))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("The public JWK set URI must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() {
|
public void issuerWhenResponseIsMalformedThenThrowsRuntimeException() {
|
||||||
prepareConfigurationResponse("malformed");
|
prepareConfigurationResponse("malformed");
|
||||||
|
@ -281,4 +315,12 @@ public class ReactiveJwtDecodersTests {
|
||||||
.setBody(body)
|
.setBody(body)
|
||||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue