Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ spring:
oauth2:
resourceserver:
jwt:
jwk-set-uri: "https://example.com/oauth2/default/v1/keys"
jwkset:
uri: "https://example.com/oauth2/default/v1/keys"
----

[configprops,yaml]
Expand Down Expand Up @@ -195,6 +196,8 @@ spring:
The same properties are applicable for both servlet and reactive applications.
Alternatively, you can define your own javadoc:org.springframework.security.oauth2.jwt.JwtDecoder[] bean for servlet applications or a javadoc:org.springframework.security.oauth2.jwt.ReactiveJwtDecoder[] for reactive applications.

If the JWK Set URI uses SSL configuration that differs from the JVM defaults, the SSL bundle to use for JWK Set requests can be configured using the configprop:spring.security.oauth2.resourceserver.jwt.jwkset.ssl.bundle[] property.

In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens through introspection:

[configprops,yaml]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
if (!StringUtils.hasText(issuerUri)) {
return ConditionOutcome.noMatch(message.didNotFind("issuer-uri property").atAll());
}
String jwkSetUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri");
String jwkSetUri = JwkSetUriProperty.get(environment);
if (StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.found("jwk-set-uri property").items(jwkSetUri));
return ConditionOutcome.noMatch(message.found("JWK Set URI property").items(jwkSetUri));
}
return ConditionOutcome.match(message.foundExactly("issuer-uri property"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2012-present 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.boot.security.oauth2.server.resource.autoconfigure;

import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;

/**
* Condition for creating {@link org.springframework.security.oauth2.jwt.JwtDecoder} by
* JWK Set URI.
*
* @author Hyeonseok Lee
*/
class JwkSetUriCondition extends SpringBootCondition {

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("JWK Set URI Condition");
Environment environment = context.getEnvironment();
String jwkSetUri = JwkSetUriProperty.get(environment);
if (!StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.didNotFind("JWK Set URI property").atAll());
}
return ConditionOutcome.match(message.foundExactly("JWK Set URI property"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2012-present 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.boot.security.oauth2.server.resource.autoconfigure;

import org.jspecify.annotations.Nullable;

import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;

/**
* Property names for the configured JWK Set URI.
*
* @author Hyeonseok Lee
*/
final class JwkSetUriProperty {

static final String NAME = "spring.security.oauth2.resourceserver.jwt.jwkset.uri";

static final String DEPRECATED_NAME = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri";

private JwkSetUriProperty() {
}

static @Nullable String get(Environment environment) {
String jwkSetUri = environment.getProperty(NAME);
if (StringUtils.hasText(jwkSetUri)) {
return jwkSetUri;
}
return environment.getProperty(DEPRECATED_NAME);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,33 @@

package org.springframework.boot.security.oauth2.server.resource.autoconfigure;

import java.net.http.HttpClient;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.net.ssl.SSLParameters;

import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import org.jspecify.annotations.Nullable;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
Expand All @@ -49,6 +58,8 @@
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

/**
* {@link Configuration @Configuration} for JWT decoder beans.
Expand All @@ -66,18 +77,27 @@
@ConditionalOnMissingBean(JwtDecoder.class)
class JwtDecoderConfiguration {

private static final Duration JWK_SET_CONNECT_TIMEOUT = Duration
.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT);

private static final Duration JWK_SET_READ_TIMEOUT = Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);

private final OAuth2ResourceServerProperties.Jwt properties;

private final List<OAuth2TokenValidator<Jwt>> additionalValidators;

private final ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> jwkSetUriJwtDecoderBuilderCustomizers;

private final @Nullable SslBundles sslBundles;

JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
ObjectProvider<OAuth2TokenValidator<Jwt>> additionalValidators,
ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> jwkSetUriJwtDecoderBuilderCustomizers) {
ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> jwkSetUriJwtDecoderBuilderCustomizers,
ObjectProvider<SslBundles> sslBundles) {
this.properties = properties.getJwt();
this.additionalValidators = additionalValidators.orderedStream().toList();
this.jwkSetUriJwtDecoderBuilderCustomizers = jwkSetUriJwtDecoderBuilderCustomizers;
this.sslBundles = sslBundles.getIfAvailable();
}

@Bean
Expand Down Expand Up @@ -115,12 +135,13 @@ private SignatureAlgorithm exactlyOneAlgorithm() {
}

@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
@Conditional(JwkSetUriCondition.class)
JwtDecoder jwtDecoderByJwkKeySetUri() {
String jwkSetUri = this.properties.getJwkSetUri();
Assert.state(jwkSetUri != null, "No JWK Set URI property specified");
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
builder.jwsAlgorithms(this::jwsAlgorithms);
configureSsl(builder);
return buildJwkSetUriJwtDecoder(builder);
}

Expand Down Expand Up @@ -156,6 +177,48 @@ private JwtDecoder buildJwkSetUriJwtDecoder(JwkSetUriJwtDecoderBuilder builder)
return decoder;
}

private void configureSsl(JwkSetUriJwtDecoderBuilder builder) {
SslBundle sslBundle = getSslBundle();
if (sslBundle != null) {
builder.restOperations(restOperations(sslBundle));
}
}

private @Nullable SslBundle getSslBundle() {
OAuth2ResourceServerProperties.Jwkset.Ssl ssl = this.properties.getJwkset().getSsl();
if (!ssl.isEnabled()) {
return null;
}
String bundleName = ssl.getBundle();
if (StringUtils.hasLength(bundleName)) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(bundleName);
}
return SslBundle.systemDefault();
}

private RestOperations restOperations(SslBundle sslBundle) {
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(createHttpClient(sslBundle));
requestFactory.setReadTimeout(JWK_SET_READ_TIMEOUT);
return new RestTemplate(requestFactory);
}

private HttpClient createHttpClient(SslBundle sslBundle) {
return HttpClient.newBuilder()
.connectTimeout(JWK_SET_CONNECT_TIMEOUT)
.sslContext(sslBundle.createSslContext())
.sslParameters(asSslParameters(sslBundle))
.build();
}

private SSLParameters asSslParameters(SslBundle sslBundle) {
SslOptions options = sslBundle.getOptions();
SSLParameters parameters = new SSLParameters();
parameters.setCipherSuites(options.getCiphers());
parameters.setProtocols(options.getEnabledProtocols());
return parameters;
}

private OAuth2TokenValidator<Jwt> getValidator() {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
if (this.properties.getIssuerUri() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
if (!StringUtils.hasText(publicKeyLocation)) {
return ConditionOutcome.noMatch(message.didNotFind("public-key-location property").atAll());
}
String jwkSetUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri");
String jwkSetUri = JwkSetUriProperty.get(environment);
if (StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.found("jwk-set-uri property").items(jwkSetUri));
return ConditionOutcome.noMatch(message.found("JWK Set URI property").items(jwkSetUri));
}
String issuerUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.issuer-uri");
if (StringUtils.hasText(issuerUri)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
import org.jspecify.annotations.Nullable;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

/**
* OAuth 2.0 resource server properties.
Expand Down Expand Up @@ -62,6 +64,8 @@ public static class Jwt {
*/
private @Nullable String jwkSetUri;

private final Jwkset jwkset = new Jwkset();

/**
* JSON Web Algorithms used for verifying the digital signatures.
*/
Expand Down Expand Up @@ -111,14 +115,21 @@ public static class Jwt {
*/
private @Nullable String principalClaimName;

@DeprecatedConfigurationProperty(replacement = "spring.security.oauth2.resourceserver.jwt.jwkset.uri",
since = "4.1.0")
public @Nullable String getJwkSetUri() {
return this.jwkSetUri;
String jwksetUri = this.jwkset.getUri();
return StringUtils.hasText(jwksetUri) ? jwksetUri : this.jwkSetUri;
}

public void setJwkSetUri(@Nullable String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}

public Jwkset getJwkset() {
return this.jwkset;
}

public List<String> getJwsAlgorithms() {
return this.jwsAlgorithms;
}
Expand Down Expand Up @@ -208,6 +219,60 @@ public String readPublicKey() throws IOException {

}

public static class Jwkset {

/**
* JSON Web Key URI to use to verify the JWT token.
*/
private @Nullable String uri;

private final Ssl ssl = new Ssl();

public @Nullable String getUri() {
return this.uri;
}

public void setUri(@Nullable String uri) {
this.uri = uri;
}

public Ssl getSsl() {
return this.ssl;
}

public static class Ssl {

/**
* Whether to enable SSL support. Enabled automatically if "bundle" is
* provided unless specified otherwise.
*/
private @Nullable Boolean enabled;

/**
* SSL bundle name.
*/
private @Nullable String bundle;

public boolean isEnabled() {
return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle);
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public @Nullable String getBundle() {
return this.bundle;
}

public void setBundle(@Nullable String bundle) {
this.bundle = bundle;
}

}

}

public static class Opaquetoken {

/**
Expand Down
Loading
Loading