Create spring-boot-security-oauth2-authorization-server
This commit is contained in:
committed by
Phillip Webb
parent
0c0af48fea
commit
4d8b558b9b
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for OAuth2 authorization server
|
||||
* support.
|
||||
*
|
||||
* <p>
|
||||
* <strong>Note:</strong> This configuration and
|
||||
* {@link OAuth2AuthorizationServerJwtAutoConfiguration} work together to ensure that the
|
||||
* {@link org.springframework.security.config.ObjectPostProcessor} is defined
|
||||
* <strong>BEFORE</strong> {@link UserDetailsServiceAutoConfiguration} so that a
|
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService} can be created
|
||||
* if necessary.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 4.0.0
|
||||
* @see OAuth2AuthorizationServerJwtAutoConfiguration
|
||||
*/
|
||||
@AutoConfiguration(before = SecurityAutoConfiguration.class,
|
||||
beforeName = "org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration")
|
||||
@ConditionalOnClass(OAuth2Authorization.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@Import({ OAuth2AuthorizationServerConfiguration.class, OAuth2AuthorizationServerWebSecurityConfiguration.class })
|
||||
public class OAuth2AuthorizationServerAutoConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} used to map
|
||||
* {@link OAuth2AuthorizationServerProperties} to registered clients and settings.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(OAuth2AuthorizationServerProperties.class)
|
||||
class OAuth2AuthorizationServerConfiguration {
|
||||
|
||||
private final OAuth2AuthorizationServerPropertiesMapper propertiesMapper;
|
||||
|
||||
OAuth2AuthorizationServerConfiguration(OAuth2AuthorizationServerProperties properties) {
|
||||
this.propertiesMapper = new OAuth2AuthorizationServerPropertiesMapper(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(RegisteredClientsConfiguredCondition.class)
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
return new InMemoryRegisteredClientRepository(this.propertiesMapper.asRegisteredClients());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
AuthorizationServerSettings authorizationServerSettings() {
|
||||
return this.propertiesMapper.asAuthorizationServerSettings();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for JWT support for endpoints of the
|
||||
* OAuth2 authorization server that require it (e.g. User Info, Client Registration).
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = UserDetailsServiceAutoConfiguration.class)
|
||||
@ConditionalOnClass({ OAuth2Authorization.class, JWKSource.class })
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
public class OAuth2AuthorizationServerJwtAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
@ConditionalOnMissingBean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
RSAKey rsaKey = getRsaKey();
|
||||
JWKSet jwkSet = new JWKSet(rsaKey);
|
||||
return new ImmutableJWKSet<>(jwkSet);
|
||||
}
|
||||
|
||||
private static RSAKey getRsaKey() {
|
||||
KeyPair keyPair = generateRsaKey();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
return rsaKey;
|
||||
}
|
||||
|
||||
private static KeyPair generateRsaKey() {
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(JwtDecoder.class)
|
||||
static class JwtDecoderConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Authorization Server properties.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.security.oauth2.authorizationserver")
|
||||
public class OAuth2AuthorizationServerProperties implements InitializingBean {
|
||||
|
||||
/**
|
||||
* URL of the Authorization Server's Issuer Identifier.
|
||||
*/
|
||||
private String issuer;
|
||||
|
||||
/**
|
||||
* Whether multiple issuers are allowed per host. Using path components in the URL of
|
||||
* the issuer identifier enables supporting multiple issuers per host in a
|
||||
* multi-tenant hosting configuration.
|
||||
*/
|
||||
private boolean multipleIssuersAllowed = false;
|
||||
|
||||
/**
|
||||
* Registered clients of the Authorization Server.
|
||||
*/
|
||||
private final Map<String, Client> client = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Authorization Server endpoints.
|
||||
*/
|
||||
private final Endpoint endpoint = new Endpoint();
|
||||
|
||||
public boolean isMultipleIssuersAllowed() {
|
||||
return this.multipleIssuersAllowed;
|
||||
}
|
||||
|
||||
public void setMultipleIssuersAllowed(boolean multipleIssuersAllowed) {
|
||||
this.multipleIssuersAllowed = multipleIssuersAllowed;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return this.issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
public Map<String, Client> getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public Endpoint getEndpoint() {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
validate();
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
getClient().values().forEach(this::validateClient);
|
||||
}
|
||||
|
||||
private void validateClient(Client client) {
|
||||
if (!StringUtils.hasText(client.getRegistration().getClientId())) {
|
||||
throw new IllegalStateException("Client id must not be empty.");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(client.getRegistration().getClientAuthenticationMethods())) {
|
||||
throw new IllegalStateException("Client authentication methods must not be empty.");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(client.getRegistration().getAuthorizationGrantTypes())) {
|
||||
throw new IllegalStateException("Authorization grant types must not be empty.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Server endpoints.
|
||||
*/
|
||||
public static class Endpoint {
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Authorization Endpoint.
|
||||
*/
|
||||
private String authorizationUri = "/oauth2/authorize";
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Device Authorization Endpoint.
|
||||
*/
|
||||
private String deviceAuthorizationUri = "/oauth2/device_authorization";
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Device Verification Endpoint.
|
||||
*/
|
||||
private String deviceVerificationUri = "/oauth2/device_verification";
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Token Endpoint.
|
||||
*/
|
||||
private String tokenUri = "/oauth2/token";
|
||||
|
||||
/**
|
||||
* Authorization Server's JWK Set Endpoint.
|
||||
*/
|
||||
private String jwkSetUri = "/oauth2/jwks";
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Token Revocation Endpoint.
|
||||
*/
|
||||
private String tokenRevocationUri = "/oauth2/revoke";
|
||||
|
||||
/**
|
||||
* Authorization Server's OAuth 2.0 Token Introspection Endpoint.
|
||||
*/
|
||||
private String tokenIntrospectionUri = "/oauth2/introspect";
|
||||
|
||||
/**
|
||||
* OpenID Connect 1.0 endpoints.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final OidcEndpoint oidc = new OidcEndpoint();
|
||||
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
public void setAuthorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
}
|
||||
|
||||
public String getDeviceAuthorizationUri() {
|
||||
return this.deviceAuthorizationUri;
|
||||
}
|
||||
|
||||
public void setDeviceAuthorizationUri(String deviceAuthorizationUri) {
|
||||
this.deviceAuthorizationUri = deviceAuthorizationUri;
|
||||
}
|
||||
|
||||
public String getDeviceVerificationUri() {
|
||||
return this.deviceVerificationUri;
|
||||
}
|
||||
|
||||
public void setDeviceVerificationUri(String deviceVerificationUri) {
|
||||
this.deviceVerificationUri = deviceVerificationUri;
|
||||
}
|
||||
|
||||
public String getTokenUri() {
|
||||
return this.tokenUri;
|
||||
}
|
||||
|
||||
public void setTokenUri(String tokenUri) {
|
||||
this.tokenUri = tokenUri;
|
||||
}
|
||||
|
||||
public String getJwkSetUri() {
|
||||
return this.jwkSetUri;
|
||||
}
|
||||
|
||||
public void setJwkSetUri(String jwkSetUri) {
|
||||
this.jwkSetUri = jwkSetUri;
|
||||
}
|
||||
|
||||
public String getTokenRevocationUri() {
|
||||
return this.tokenRevocationUri;
|
||||
}
|
||||
|
||||
public void setTokenRevocationUri(String tokenRevocationUri) {
|
||||
this.tokenRevocationUri = tokenRevocationUri;
|
||||
}
|
||||
|
||||
public String getTokenIntrospectionUri() {
|
||||
return this.tokenIntrospectionUri;
|
||||
}
|
||||
|
||||
public void setTokenIntrospectionUri(String tokenIntrospectionUri) {
|
||||
this.tokenIntrospectionUri = tokenIntrospectionUri;
|
||||
}
|
||||
|
||||
public OidcEndpoint getOidc() {
|
||||
return this.oidc;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect 1.0 endpoints.
|
||||
*/
|
||||
public static class OidcEndpoint {
|
||||
|
||||
/**
|
||||
* Authorization Server's OpenID Connect 1.0 Logout Endpoint.
|
||||
*/
|
||||
private String logoutUri = "/connect/logout";
|
||||
|
||||
/**
|
||||
* Authorization Server's OpenID Connect 1.0 Client Registration Endpoint.
|
||||
*/
|
||||
private String clientRegistrationUri = "/connect/register";
|
||||
|
||||
/**
|
||||
* Authorization Server's OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*/
|
||||
private String userInfoUri = "/userinfo";
|
||||
|
||||
public String getLogoutUri() {
|
||||
return this.logoutUri;
|
||||
}
|
||||
|
||||
public void setLogoutUri(String logoutUri) {
|
||||
this.logoutUri = logoutUri;
|
||||
}
|
||||
|
||||
public String getClientRegistrationUri() {
|
||||
return this.clientRegistrationUri;
|
||||
}
|
||||
|
||||
public void setClientRegistrationUri(String clientRegistrationUri) {
|
||||
this.clientRegistrationUri = clientRegistrationUri;
|
||||
}
|
||||
|
||||
public String getUserInfoUri() {
|
||||
return this.userInfoUri;
|
||||
}
|
||||
|
||||
public void setUserInfoUri(String userInfoUri) {
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A registered client of the Authorization Server.
|
||||
*/
|
||||
public static class Client {
|
||||
|
||||
/**
|
||||
* Client registration information.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final Registration registration = new Registration();
|
||||
|
||||
/**
|
||||
* Whether the client is required to provide a proof key challenge and verifier
|
||||
* when performing the Authorization Code Grant flow.
|
||||
*/
|
||||
private boolean requireProofKey = false;
|
||||
|
||||
/**
|
||||
* Whether authorization consent is required when the client requests access.
|
||||
*/
|
||||
private boolean requireAuthorizationConsent = false;
|
||||
|
||||
/**
|
||||
* URL for the client's JSON Web Key Set.
|
||||
*/
|
||||
private String jwkSetUri;
|
||||
|
||||
/**
|
||||
* JWS algorithm that must be used for signing the JWT used to authenticate the
|
||||
* client at the Token Endpoint for the {@code private_key_jwt} and
|
||||
* {@code client_secret_jwt} authentication methods.
|
||||
*/
|
||||
private String tokenEndpointAuthenticationSigningAlgorithm;
|
||||
|
||||
/**
|
||||
* Token settings of the registered client.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final Token token = new Token();
|
||||
|
||||
public Registration getRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public boolean isRequireProofKey() {
|
||||
return this.requireProofKey;
|
||||
}
|
||||
|
||||
public void setRequireProofKey(boolean requireProofKey) {
|
||||
this.requireProofKey = requireProofKey;
|
||||
}
|
||||
|
||||
public boolean isRequireAuthorizationConsent() {
|
||||
return this.requireAuthorizationConsent;
|
||||
}
|
||||
|
||||
public void setRequireAuthorizationConsent(boolean requireAuthorizationConsent) {
|
||||
this.requireAuthorizationConsent = requireAuthorizationConsent;
|
||||
}
|
||||
|
||||
public String getJwkSetUri() {
|
||||
return this.jwkSetUri;
|
||||
}
|
||||
|
||||
public void setJwkSetUri(String jwkSetUri) {
|
||||
this.jwkSetUri = jwkSetUri;
|
||||
}
|
||||
|
||||
public String getTokenEndpointAuthenticationSigningAlgorithm() {
|
||||
return this.tokenEndpointAuthenticationSigningAlgorithm;
|
||||
}
|
||||
|
||||
public void setTokenEndpointAuthenticationSigningAlgorithm(String tokenEndpointAuthenticationSigningAlgorithm) {
|
||||
this.tokenEndpointAuthenticationSigningAlgorithm = tokenEndpointAuthenticationSigningAlgorithm;
|
||||
}
|
||||
|
||||
public Token getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Client registration information.
|
||||
*/
|
||||
public static class Registration {
|
||||
|
||||
/**
|
||||
* Client ID of the registration.
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* Client secret of the registration. May be left blank for a public client.
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
/**
|
||||
* Name of the client.
|
||||
*/
|
||||
private String clientName;
|
||||
|
||||
/**
|
||||
* Client authentication method(s) that the client may use.
|
||||
*/
|
||||
private Set<String> clientAuthenticationMethods = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Authorization grant type(s) that the client may use.
|
||||
*/
|
||||
private Set<String> authorizationGrantTypes = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Redirect URI(s) that the client may use in redirect-based flows.
|
||||
*/
|
||||
private Set<String> redirectUris = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Redirect URI(s) that the client may use for logout.
|
||||
*/
|
||||
private Set<String> postLogoutRedirectUris = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Scope(s) that the client may use.
|
||||
*/
|
||||
private Set<String> scopes = new HashSet<>();
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
public void setClientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
}
|
||||
|
||||
public Set<String> getClientAuthenticationMethods() {
|
||||
return this.clientAuthenticationMethods;
|
||||
}
|
||||
|
||||
public void setClientAuthenticationMethods(Set<String> clientAuthenticationMethods) {
|
||||
this.clientAuthenticationMethods = clientAuthenticationMethods;
|
||||
}
|
||||
|
||||
public Set<String> getAuthorizationGrantTypes() {
|
||||
return this.authorizationGrantTypes;
|
||||
}
|
||||
|
||||
public void setAuthorizationGrantTypes(Set<String> authorizationGrantTypes) {
|
||||
this.authorizationGrantTypes = authorizationGrantTypes;
|
||||
}
|
||||
|
||||
public Set<String> getRedirectUris() {
|
||||
return this.redirectUris;
|
||||
}
|
||||
|
||||
public void setRedirectUris(Set<String> redirectUris) {
|
||||
this.redirectUris = redirectUris;
|
||||
}
|
||||
|
||||
public Set<String> getPostLogoutRedirectUris() {
|
||||
return this.postLogoutRedirectUris;
|
||||
}
|
||||
|
||||
public void setPostLogoutRedirectUris(Set<String> postLogoutRedirectUris) {
|
||||
this.postLogoutRedirectUris = postLogoutRedirectUris;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public void setScopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Token settings of the registered client.
|
||||
*/
|
||||
public static class Token {
|
||||
|
||||
/**
|
||||
* Time-to-live for an authorization code.
|
||||
*/
|
||||
private Duration authorizationCodeTimeToLive = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* Time-to-live for an access token.
|
||||
*/
|
||||
private Duration accessTokenTimeToLive = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* Token format for an access token.
|
||||
*/
|
||||
private String accessTokenFormat = "self-contained";
|
||||
|
||||
/**
|
||||
* Time-to-live for a device code.
|
||||
*/
|
||||
private Duration deviceCodeTimeToLive = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* Whether refresh tokens are reused or a new refresh token is issued when
|
||||
* returning the access token response.
|
||||
*/
|
||||
private boolean reuseRefreshTokens = true;
|
||||
|
||||
/**
|
||||
* Time-to-live for a refresh token.
|
||||
*/
|
||||
private Duration refreshTokenTimeToLive = Duration.ofMinutes(60);
|
||||
|
||||
/**
|
||||
* JWS algorithm for signing the ID Token.
|
||||
*/
|
||||
private String idTokenSignatureAlgorithm = "RS256";
|
||||
|
||||
public Duration getAuthorizationCodeTimeToLive() {
|
||||
return this.authorizationCodeTimeToLive;
|
||||
}
|
||||
|
||||
public void setAuthorizationCodeTimeToLive(Duration authorizationCodeTimeToLive) {
|
||||
this.authorizationCodeTimeToLive = authorizationCodeTimeToLive;
|
||||
}
|
||||
|
||||
public Duration getAccessTokenTimeToLive() {
|
||||
return this.accessTokenTimeToLive;
|
||||
}
|
||||
|
||||
public void setAccessTokenTimeToLive(Duration accessTokenTimeToLive) {
|
||||
this.accessTokenTimeToLive = accessTokenTimeToLive;
|
||||
}
|
||||
|
||||
public String getAccessTokenFormat() {
|
||||
return this.accessTokenFormat;
|
||||
}
|
||||
|
||||
public void setAccessTokenFormat(String accessTokenFormat) {
|
||||
this.accessTokenFormat = accessTokenFormat;
|
||||
}
|
||||
|
||||
public Duration getDeviceCodeTimeToLive() {
|
||||
return this.deviceCodeTimeToLive;
|
||||
}
|
||||
|
||||
public void setDeviceCodeTimeToLive(Duration deviceCodeTimeToLive) {
|
||||
this.deviceCodeTimeToLive = deviceCodeTimeToLive;
|
||||
}
|
||||
|
||||
public boolean isReuseRefreshTokens() {
|
||||
return this.reuseRefreshTokens;
|
||||
}
|
||||
|
||||
public void setReuseRefreshTokens(boolean reuseRefreshTokens) {
|
||||
this.reuseRefreshTokens = reuseRefreshTokens;
|
||||
}
|
||||
|
||||
public Duration getRefreshTokenTimeToLive() {
|
||||
return this.refreshTokenTimeToLive;
|
||||
}
|
||||
|
||||
public void setRefreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
|
||||
this.refreshTokenTimeToLive = refreshTokenTimeToLive;
|
||||
}
|
||||
|
||||
public String getIdTokenSignatureAlgorithm() {
|
||||
return this.idTokenSignatureAlgorithm;
|
||||
}
|
||||
|
||||
public void setIdTokenSignatureAlgorithm(String idTokenSignatureAlgorithm) {
|
||||
this.idTokenSignatureAlgorithm = idTokenSignatureAlgorithm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerProperties.Client;
|
||||
import org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerProperties.Registration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
|
||||
/**
|
||||
* Maps {@link OAuth2AuthorizationServerProperties} to Authorization Server types.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
final class OAuth2AuthorizationServerPropertiesMapper {
|
||||
|
||||
private final OAuth2AuthorizationServerProperties properties;
|
||||
|
||||
OAuth2AuthorizationServerPropertiesMapper(OAuth2AuthorizationServerProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
AuthorizationServerSettings asAuthorizationServerSettings() {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
OAuth2AuthorizationServerProperties.Endpoint endpoint = this.properties.getEndpoint();
|
||||
OAuth2AuthorizationServerProperties.OidcEndpoint oidc = endpoint.getOidc();
|
||||
AuthorizationServerSettings.Builder builder = AuthorizationServerSettings.builder();
|
||||
map.from(this.properties::getIssuer).to(builder::issuer);
|
||||
map.from(this.properties::isMultipleIssuersAllowed).to(builder::multipleIssuersAllowed);
|
||||
map.from(endpoint::getAuthorizationUri).to(builder::authorizationEndpoint);
|
||||
map.from(endpoint::getDeviceAuthorizationUri).to(builder::deviceAuthorizationEndpoint);
|
||||
map.from(endpoint::getDeviceVerificationUri).to(builder::deviceVerificationEndpoint);
|
||||
map.from(endpoint::getTokenUri).to(builder::tokenEndpoint);
|
||||
map.from(endpoint::getJwkSetUri).to(builder::jwkSetEndpoint);
|
||||
map.from(endpoint::getTokenRevocationUri).to(builder::tokenRevocationEndpoint);
|
||||
map.from(endpoint::getTokenIntrospectionUri).to(builder::tokenIntrospectionEndpoint);
|
||||
map.from(oidc::getLogoutUri).to(builder::oidcLogoutEndpoint);
|
||||
map.from(oidc::getClientRegistrationUri).to(builder::oidcClientRegistrationEndpoint);
|
||||
map.from(oidc::getUserInfoUri).to(builder::oidcUserInfoEndpoint);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
List<RegisteredClient> asRegisteredClients() {
|
||||
List<RegisteredClient> registeredClients = new ArrayList<>();
|
||||
this.properties.getClient()
|
||||
.forEach((registrationId, client) -> registeredClients.add(getRegisteredClient(registrationId, client)));
|
||||
return registeredClients;
|
||||
}
|
||||
|
||||
private RegisteredClient getRegisteredClient(String registrationId, Client client) {
|
||||
Registration registration = client.getRegistration();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
RegisteredClient.Builder builder = RegisteredClient.withId(registrationId);
|
||||
map.from(registration::getClientId).to(builder::clientId);
|
||||
map.from(registration::getClientSecret).to(builder::clientSecret);
|
||||
map.from(registration::getClientName).to(builder::clientName);
|
||||
registration.getClientAuthenticationMethods()
|
||||
.forEach((clientAuthenticationMethod) -> map.from(clientAuthenticationMethod)
|
||||
.as(ClientAuthenticationMethod::new)
|
||||
.to(builder::clientAuthenticationMethod));
|
||||
registration.getAuthorizationGrantTypes()
|
||||
.forEach((authorizationGrantType) -> map.from(authorizationGrantType)
|
||||
.as(AuthorizationGrantType::new)
|
||||
.to(builder::authorizationGrantType));
|
||||
registration.getRedirectUris().forEach((redirectUri) -> map.from(redirectUri).to(builder::redirectUri));
|
||||
registration.getPostLogoutRedirectUris()
|
||||
.forEach((redirectUri) -> map.from(redirectUri).to(builder::postLogoutRedirectUri));
|
||||
registration.getScopes().forEach((scope) -> map.from(scope).to(builder::scope));
|
||||
builder.clientSettings(getClientSettings(client, map));
|
||||
builder.tokenSettings(getTokenSettings(client, map));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ClientSettings getClientSettings(Client client, PropertyMapper map) {
|
||||
ClientSettings.Builder builder = ClientSettings.builder();
|
||||
map.from(client::isRequireProofKey).to(builder::requireProofKey);
|
||||
map.from(client::isRequireAuthorizationConsent).to(builder::requireAuthorizationConsent);
|
||||
map.from(client::getJwkSetUri).to(builder::jwkSetUrl);
|
||||
map.from(client::getTokenEndpointAuthenticationSigningAlgorithm)
|
||||
.as(this::jwsAlgorithm)
|
||||
.to(builder::tokenEndpointAuthenticationSigningAlgorithm);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private TokenSettings getTokenSettings(Client client, PropertyMapper map) {
|
||||
OAuth2AuthorizationServerProperties.Token token = client.getToken();
|
||||
TokenSettings.Builder builder = TokenSettings.builder();
|
||||
map.from(token::getAuthorizationCodeTimeToLive).to(builder::authorizationCodeTimeToLive);
|
||||
map.from(token::getAccessTokenTimeToLive).to(builder::accessTokenTimeToLive);
|
||||
map.from(token::getAccessTokenFormat).as(OAuth2TokenFormat::new).to(builder::accessTokenFormat);
|
||||
map.from(token::getDeviceCodeTimeToLive).to(builder::deviceCodeTimeToLive);
|
||||
map.from(token::isReuseRefreshTokens).to(builder::reuseRefreshTokens);
|
||||
map.from(token::getRefreshTokenTimeToLive).to(builder::refreshTokenTimeToLive);
|
||||
map.from(token::getIdTokenSignatureAlgorithm)
|
||||
.as(this::signatureAlgorithm)
|
||||
.to(builder::idTokenSignatureAlgorithm);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private JwsAlgorithm jwsAlgorithm(String signingAlgorithm) {
|
||||
String name = signingAlgorithm.toUpperCase(Locale.ROOT);
|
||||
JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.from(name);
|
||||
if (jwsAlgorithm == null) {
|
||||
jwsAlgorithm = MacAlgorithm.from(name);
|
||||
}
|
||||
return jwsAlgorithm;
|
||||
}
|
||||
|
||||
private SignatureAlgorithm signatureAlgorithm(String signatureAlgorithm) {
|
||||
return SignatureAlgorithm.from(signatureAlgorithm.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.security.autoconfigure.ConditionalOnDefaultWebSecurity;
|
||||
import org.springframework.boot.security.autoconfigure.SecurityProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* {@link Configuration @Configuration} for OAuth2 authorization server support.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnDefaultWebSecurity
|
||||
@ConditionalOnBean({ RegisteredClientRepository.class, AuthorizationServerSettings.class })
|
||||
class OAuth2AuthorizationServerWebSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServer = OAuth2AuthorizationServerConfigurer
|
||||
.authorizationServer();
|
||||
http.securityMatcher(authorizationServer.getEndpointsMatcher());
|
||||
http.with(authorizationServer, withDefaults());
|
||||
http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(withDefaults());
|
||||
http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
|
||||
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
|
||||
new LoginUrlAuthenticationEntryPoint("/login"), createRequestMatcher()));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(SecurityProperties.BASIC_AUTH_ORDER)
|
||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()).formLogin(withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private static RequestMatcher createRequestMatcher() {
|
||||
MediaTypeRequestMatcher requestMatcher = new MediaTypeRequestMatcher(MediaType.TEXT_HTML);
|
||||
requestMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
|
||||
return requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* Condition that matches if any {@code spring.security.oauth2.authorizationserver.client}
|
||||
* properties are defined.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
class RegisteredClientsConfiguredCondition extends SpringBootCondition {
|
||||
|
||||
private static final Bindable<Map<String, OAuth2AuthorizationServerProperties.Client>> STRING_CLIENT_MAP = Bindable
|
||||
.mapOf(String.class, OAuth2AuthorizationServerProperties.Client.class);
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
ConditionMessage.Builder message = ConditionMessage
|
||||
.forCondition("OAuth2 Registered Clients Configured Condition");
|
||||
Map<String, OAuth2AuthorizationServerProperties.Client> registrations = getRegistrations(
|
||||
context.getEnvironment());
|
||||
if (!registrations.isEmpty()) {
|
||||
return ConditionOutcome.match(message.foundExactly("registered clients " + registrations.values()
|
||||
.stream()
|
||||
.map(OAuth2AuthorizationServerProperties.Client::getRegistration)
|
||||
.map(OAuth2AuthorizationServerProperties.Registration::getClientId)
|
||||
.collect(Collectors.joining(", "))));
|
||||
}
|
||||
return ConditionOutcome.noMatch(message.notAvailable("registered clients"));
|
||||
}
|
||||
|
||||
private Map<String, OAuth2AuthorizationServerProperties.Client> getRegistrations(Environment environment) {
|
||||
return Binder.get(environment)
|
||||
.bind("spring.security.oauth2.authorizationserver.client", STRING_CLIENT_MAP)
|
||||
.orElse(Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Spring Security's OAuth2 authorization server.
|
||||
*/
|
||||
package org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": []
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerAutoConfiguration
|
||||
org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerJwtAutoConfiguration
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link OAuth2AuthorizationServerAutoConfiguration}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class OAuth2AuthorizationServerAutoConfigurationTests {
|
||||
|
||||
private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver";
|
||||
|
||||
private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client";
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(OAuth2AuthorizationServerAutoConfiguration.class,
|
||||
OAuth2AuthorizationServerJwtAutoConfiguration.class, SecurityAutoConfiguration.class,
|
||||
UserDetailsServiceAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void autoConfigurationConditionalOnClassOauth2Authorization() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(OAuth2Authorization.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(OAuth2AuthorizationServerAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar",
|
||||
"spring-security-saml2-service-provider-*.jar" })
|
||||
void autoConfigurationDoesNotCauseUserDetailsServiceToBackOff() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(UserDetailsServiceAutoConfiguration.class)
|
||||
.hasBean("inMemoryUserDetailsManager"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredClientRepositoryBeanShouldNotBeCreatedWhenPropertiesAbsent() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(RegisteredClientRepository.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredClientRepositoryBeanShouldBeCreatedWhenPropertiesPresent() {
|
||||
this.contextRunner
|
||||
.withPropertyValues(CLIENT_PREFIX + ".foo.registration.client-id=abcd",
|
||||
CLIENT_PREFIX + ".foo.registration.client-secret=secret",
|
||||
CLIENT_PREFIX + ".foo.registration.client-authentication-methods=client_secret_basic",
|
||||
CLIENT_PREFIX + ".foo.registration.authorization-grant-types=client_credentials",
|
||||
CLIENT_PREFIX + ".foo.registration.scopes=test")
|
||||
.run((context) -> {
|
||||
RegisteredClientRepository registeredClientRepository = context
|
||||
.getBean(RegisteredClientRepository.class);
|
||||
RegisteredClient registeredClient = registeredClientRepository.findById("foo");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
assertThat(registeredClient.getClientId()).isEqualTo("abcd");
|
||||
assertThat(registeredClient.getClientSecret()).isEqualTo("secret");
|
||||
assertThat(registeredClient.getClientAuthenticationMethods())
|
||||
.containsOnly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
assertThat(registeredClient.getAuthorizationGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.CLIENT_CREDENTIALS);
|
||||
assertThat(registeredClient.getScopes()).containsOnly("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeredClientRepositoryBacksOffWhenRegisteredClientRepositoryBeanPresent() {
|
||||
this.contextRunner.withUserConfiguration(TestRegisteredClientRepositoryConfiguration.class)
|
||||
.withPropertyValues(CLIENT_PREFIX + ".foo.registration.client-id=abcd",
|
||||
CLIENT_PREFIX + ".foo.registration.client-secret=secret",
|
||||
CLIENT_PREFIX + ".foo.registration.client-authentication-methods=client_secret_basic",
|
||||
CLIENT_PREFIX + ".foo.registration.authorization-grant-types=client_credentials",
|
||||
CLIENT_PREFIX + ".foo.registration.scope=test")
|
||||
.run((context) -> {
|
||||
RegisteredClientRepository registeredClientRepository = context
|
||||
.getBean(RegisteredClientRepository.class);
|
||||
RegisteredClient registeredClient = registeredClientRepository.findById("test");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
assertThat(registeredClient.getClientId()).isEqualTo("abcd");
|
||||
assertThat(registeredClient.getClientSecret()).isEqualTo("secret");
|
||||
assertThat(registeredClient.getClientAuthenticationMethods())
|
||||
.containsOnly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
assertThat(registeredClient.getAuthorizationGrantTypes())
|
||||
.containsOnly(AuthorizationGrantType.CLIENT_CREDENTIALS);
|
||||
assertThat(registeredClient.getScopes()).containsOnly("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesAbsent() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(AuthorizationServerSettings.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesPresent() {
|
||||
this.contextRunner
|
||||
.withPropertyValues(PROPERTIES_PREFIX + ".issuer=https://example.com",
|
||||
PROPERTIES_PREFIX + ".endpoint.authorization-uri=/authorize",
|
||||
PROPERTIES_PREFIX + ".endpoint.device-authorization-uri=/device_authorization",
|
||||
PROPERTIES_PREFIX + ".endpoint.device-verification-uri=/device_verification",
|
||||
PROPERTIES_PREFIX + ".endpoint.token-uri=/token", PROPERTIES_PREFIX + ".endpoint.jwk-set-uri=/jwks",
|
||||
PROPERTIES_PREFIX + ".endpoint.token-revocation-uri=/revoke",
|
||||
PROPERTIES_PREFIX + ".endpoint.token-introspection-uri=/introspect",
|
||||
PROPERTIES_PREFIX + ".endpoint.oidc.logout-uri=/logout",
|
||||
PROPERTIES_PREFIX + ".endpoint.oidc.client-registration-uri=/register",
|
||||
PROPERTIES_PREFIX + ".endpoint.oidc.user-info-uri=/user")
|
||||
.run((context) -> {
|
||||
AuthorizationServerSettings settings = context.getBean(AuthorizationServerSettings.class);
|
||||
assertThat(settings.getIssuer()).isEqualTo("https://example.com");
|
||||
assertThat(settings.getAuthorizationEndpoint()).isEqualTo("/authorize");
|
||||
assertThat(settings.getDeviceAuthorizationEndpoint()).isEqualTo("/device_authorization");
|
||||
assertThat(settings.getDeviceVerificationEndpoint()).isEqualTo("/device_verification");
|
||||
assertThat(settings.getTokenEndpoint()).isEqualTo("/token");
|
||||
assertThat(settings.getJwkSetEndpoint()).isEqualTo("/jwks");
|
||||
assertThat(settings.getTokenRevocationEndpoint()).isEqualTo("/revoke");
|
||||
assertThat(settings.getTokenIntrospectionEndpoint()).isEqualTo("/introspect");
|
||||
assertThat(settings.getOidcLogoutEndpoint()).isEqualTo("/logout");
|
||||
assertThat(settings.getOidcClientRegistrationEndpoint()).isEqualTo("/register");
|
||||
assertThat(settings.getOidcUserInfoEndpoint()).isEqualTo("/user");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationServerSettingsBacksOffWhenAuthorizationServerSettingsBeanPresent() {
|
||||
this.contextRunner.withUserConfiguration(TestAuthorizationServerSettingsConfiguration.class)
|
||||
.withPropertyValues(PROPERTIES_PREFIX + ".issuer=https://test.com")
|
||||
.run((context) -> {
|
||||
AuthorizationServerSettings settings = context.getBean(AuthorizationServerSettings.class);
|
||||
assertThat(settings.getIssuer()).isEqualTo("https://example.com");
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestRegisteredClientRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("test")
|
||||
.clientId("abcd")
|
||||
.clientSecret("secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.scope("test")
|
||||
.build();
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestAuthorizationServerSettingsConfiguration {
|
||||
|
||||
@Bean
|
||||
AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().issuer("https://example.com").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerJwtAutoConfiguration}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
class OAuth2AuthorizationServerJwtAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(OAuth2AuthorizationServerJwtAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void autoConfigurationConditionalOnClassOAuth2Authorization() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(OAuth2Authorization.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(OAuth2AuthorizationServerJwtAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoConfigurationConditionalOnClassJWKSource() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(JWKSource.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(OAuth2AuthorizationServerJwtAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwtDecoderConditionalOnClassJwtDecoder() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(JwtDecoder.class))
|
||||
.run((context) -> assertThat(context).hasSingleBean(OAuth2AuthorizationServerJwtAutoConfiguration.class)
|
||||
.doesNotHaveBean("jwtDecoder"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwtConfigurationConfiguresJwtDecoderWithGeneratedKey() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasBean("jwtDecoder");
|
||||
assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class);
|
||||
assertThat(context).hasBean("jwkSource");
|
||||
assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwtDecoderBacksOffWhenBeanPresent() {
|
||||
this.contextRunner.withUserConfiguration(TestJwtDecoderConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasBean("jwtDecoder");
|
||||
assertThat(context.getBean("jwtDecoder")).isNotInstanceOf(NimbusJwtDecoder.class);
|
||||
assertThat(context).hasBean("jwkSource");
|
||||
assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwkSourceBacksOffWhenBeanPresent() {
|
||||
this.contextRunner.withUserConfiguration(TestJwkSourceConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasBean("jwtDecoder");
|
||||
assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class);
|
||||
assertThat(context).hasBean("jwkSource");
|
||||
assertThat(context.getBean("jwkSource")).isNotInstanceOf(ImmutableJWKSet.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestJwtDecoderConfiguration {
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return (token) -> null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestJwkSourceConfiguration {
|
||||
|
||||
@Bean
|
||||
JWKSource<SecurityContext> jwkSource() {
|
||||
return (jwkSelector, context) -> null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerPropertiesMapper}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
class OAuth2AuthorizationServerPropertiesMapperTests {
|
||||
|
||||
private final OAuth2AuthorizationServerProperties properties = new OAuth2AuthorizationServerProperties();
|
||||
|
||||
private final OAuth2AuthorizationServerPropertiesMapper mapper = new OAuth2AuthorizationServerPropertiesMapper(
|
||||
this.properties);
|
||||
|
||||
@Test
|
||||
void getRegisteredClientsWhenValidParametersShouldAdapt() {
|
||||
OAuth2AuthorizationServerProperties.Client client = createClient();
|
||||
this.properties.getClient().put("foo", client);
|
||||
List<RegisteredClient> registeredClients = this.mapper.asRegisteredClients();
|
||||
assertThat(registeredClients).hasSize(1);
|
||||
RegisteredClient registeredClient = registeredClients.get(0);
|
||||
assertThat(registeredClient.getClientId()).isEqualTo("foo");
|
||||
assertThat(registeredClient.getClientSecret()).isEqualTo("secret");
|
||||
assertThat(registeredClient.getClientAuthenticationMethods())
|
||||
.containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
assertThat(registeredClient.getAuthorizationGrantTypes())
|
||||
.containsExactly(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
assertThat(registeredClient.getRedirectUris()).containsExactly("https://example.com/redirect");
|
||||
assertThat(registeredClient.getPostLogoutRedirectUris()).containsExactly("https://example.com/logout");
|
||||
assertThat(registeredClient.getScopes()).containsExactly("user.read");
|
||||
assertThat(registeredClient.getClientSettings().isRequireProofKey()).isTrue();
|
||||
assertThat(registeredClient.getClientSettings().isRequireAuthorizationConsent()).isTrue();
|
||||
assertThat(registeredClient.getClientSettings().getJwkSetUrl()).isEqualTo("https://example.com/jwks");
|
||||
assertThat(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm())
|
||||
.isEqualTo(SignatureAlgorithm.RS256);
|
||||
assertThat(registeredClient.getTokenSettings().getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.REFERENCE);
|
||||
assertThat(registeredClient.getTokenSettings().getAccessTokenTimeToLive()).isEqualTo(Duration.ofSeconds(300));
|
||||
assertThat(registeredClient.getTokenSettings().getRefreshTokenTimeToLive()).isEqualTo(Duration.ofHours(24));
|
||||
assertThat(registeredClient.getTokenSettings().getDeviceCodeTimeToLive()).isEqualTo(Duration.ofMinutes(30));
|
||||
assertThat(registeredClient.getTokenSettings().isReuseRefreshTokens()).isEqualTo(true);
|
||||
assertThat(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm())
|
||||
.isEqualTo(SignatureAlgorithm.RS512);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationServerProperties.Client createClient() {
|
||||
OAuth2AuthorizationServerProperties.Client client = new OAuth2AuthorizationServerProperties.Client();
|
||||
client.setRequireProofKey(true);
|
||||
client.setRequireAuthorizationConsent(true);
|
||||
client.setJwkSetUri("https://example.com/jwks");
|
||||
client.setTokenEndpointAuthenticationSigningAlgorithm("rs256");
|
||||
OAuth2AuthorizationServerProperties.Registration registration = client.getRegistration();
|
||||
registration.setClientId("foo");
|
||||
registration.setClientSecret("secret");
|
||||
registration.getClientAuthenticationMethods().add("client_secret_basic");
|
||||
registration.getAuthorizationGrantTypes().add("authorization_code");
|
||||
registration.getRedirectUris().add("https://example.com/redirect");
|
||||
registration.getPostLogoutRedirectUris().add("https://example.com/logout");
|
||||
registration.getScopes().add("user.read");
|
||||
OAuth2AuthorizationServerProperties.Token token = client.getToken();
|
||||
token.setAccessTokenFormat("reference");
|
||||
token.setAccessTokenTimeToLive(Duration.ofSeconds(300));
|
||||
token.setRefreshTokenTimeToLive(Duration.ofHours(24));
|
||||
token.setDeviceCodeTimeToLive(Duration.ofMinutes(30));
|
||||
token.setReuseRefreshTokens(true);
|
||||
token.setIdTokenSignatureAlgorithm("rs512");
|
||||
return client;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAuthorizationServerSettingsWhenValidParametersShouldAdapt() {
|
||||
this.properties.setIssuer("https://example.com");
|
||||
OAuth2AuthorizationServerProperties.Endpoint endpoints = this.properties.getEndpoint();
|
||||
endpoints.setAuthorizationUri("/authorize");
|
||||
endpoints.setDeviceAuthorizationUri("/device_authorization");
|
||||
endpoints.setDeviceVerificationUri("/device_verification");
|
||||
endpoints.setTokenUri("/token");
|
||||
endpoints.setJwkSetUri("/jwks");
|
||||
endpoints.setTokenRevocationUri("/revoke");
|
||||
endpoints.setTokenIntrospectionUri("/introspect");
|
||||
OAuth2AuthorizationServerProperties.OidcEndpoint oidc = endpoints.getOidc();
|
||||
oidc.setLogoutUri("/logout");
|
||||
oidc.setClientRegistrationUri("/register");
|
||||
oidc.setUserInfoUri("/user");
|
||||
AuthorizationServerSettings settings = this.mapper.asAuthorizationServerSettings();
|
||||
assertThat(settings.getIssuer()).isEqualTo("https://example.com");
|
||||
assertThat(settings.isMultipleIssuersAllowed()).isFalse();
|
||||
assertThat(settings.getAuthorizationEndpoint()).isEqualTo("/authorize");
|
||||
assertThat(settings.getDeviceAuthorizationEndpoint()).isEqualTo("/device_authorization");
|
||||
assertThat(settings.getDeviceVerificationEndpoint()).isEqualTo("/device_verification");
|
||||
assertThat(settings.getTokenEndpoint()).isEqualTo("/token");
|
||||
assertThat(settings.getJwkSetEndpoint()).isEqualTo("/jwks");
|
||||
assertThat(settings.getTokenRevocationEndpoint()).isEqualTo("/revoke");
|
||||
assertThat(settings.getTokenIntrospectionEndpoint()).isEqualTo("/introspect");
|
||||
assertThat(settings.getOidcLogoutEndpoint()).isEqualTo("/logout");
|
||||
assertThat(settings.getOidcClientRegistrationEndpoint()).isEqualTo("/register");
|
||||
assertThat(settings.getOidcUserInfoEndpoint()).isEqualTo("/user");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAuthorizationServerSettingsWhenMultipleIssuersAllowedShouldAdapt() {
|
||||
this.properties.setMultipleIssuersAllowed(true);
|
||||
OAuth2AuthorizationServerProperties.Endpoint endpoints = this.properties.getEndpoint();
|
||||
endpoints.setAuthorizationUri("/authorize");
|
||||
endpoints.setDeviceAuthorizationUri("/device_authorization");
|
||||
endpoints.setDeviceVerificationUri("/device_verification");
|
||||
endpoints.setTokenUri("/token");
|
||||
endpoints.setJwkSetUri("/jwks");
|
||||
endpoints.setTokenRevocationUri("/revoke");
|
||||
endpoints.setTokenIntrospectionUri("/introspect");
|
||||
OAuth2AuthorizationServerProperties.OidcEndpoint oidc = endpoints.getOidc();
|
||||
oidc.setLogoutUri("/logout");
|
||||
oidc.setClientRegistrationUri("/register");
|
||||
oidc.setUserInfoUri("/user");
|
||||
AuthorizationServerSettings settings = this.mapper.asAuthorizationServerSettings();
|
||||
assertThat(settings.getIssuer()).isNull();
|
||||
assertThat(settings.isMultipleIssuersAllowed()).isTrue();
|
||||
assertThat(settings.getAuthorizationEndpoint()).isEqualTo("/authorize");
|
||||
assertThat(settings.getDeviceAuthorizationEndpoint()).isEqualTo("/device_authorization");
|
||||
assertThat(settings.getDeviceVerificationEndpoint()).isEqualTo("/device_verification");
|
||||
assertThat(settings.getTokenEndpoint()).isEqualTo("/token");
|
||||
assertThat(settings.getJwkSetEndpoint()).isEqualTo("/jwks");
|
||||
assertThat(settings.getTokenRevocationEndpoint()).isEqualTo("/revoke");
|
||||
assertThat(settings.getTokenIntrospectionEndpoint()).isEqualTo("/introspect");
|
||||
assertThat(settings.getOidcLogoutEndpoint()).isEqualTo("/logout");
|
||||
assertThat(settings.getOidcClientRegistrationEndpoint()).isEqualTo("/register");
|
||||
assertThat(settings.getOidcUserInfoEndpoint()).isEqualTo("/user");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerProperties}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
class OAuth2AuthorizationServerPropertiesTests {
|
||||
|
||||
private final OAuth2AuthorizationServerProperties properties = new OAuth2AuthorizationServerProperties();
|
||||
|
||||
@Test
|
||||
void clientIdAbsentThrowsException() {
|
||||
OAuth2AuthorizationServerProperties.Client client = new OAuth2AuthorizationServerProperties.Client();
|
||||
client.getRegistration().getClientAuthenticationMethods().add("client_secret_basic");
|
||||
client.getRegistration().getAuthorizationGrantTypes().add("authorization_code");
|
||||
this.properties.getClient().put("foo", client);
|
||||
assertThatIllegalStateException().isThrownBy(this.properties::validate)
|
||||
.withMessage("Client id must not be empty.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientSecretAbsentShouldNotThrowException() {
|
||||
OAuth2AuthorizationServerProperties.Client client = new OAuth2AuthorizationServerProperties.Client();
|
||||
client.getRegistration().setClientId("foo");
|
||||
client.getRegistration().getClientAuthenticationMethods().add("client_secret_basic");
|
||||
client.getRegistration().getAuthorizationGrantTypes().add("authorization_code");
|
||||
this.properties.getClient().put("foo", client);
|
||||
this.properties.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientAuthenticationMethodsEmptyThrowsException() {
|
||||
OAuth2AuthorizationServerProperties.Client client = new OAuth2AuthorizationServerProperties.Client();
|
||||
client.getRegistration().setClientId("foo");
|
||||
client.getRegistration().getAuthorizationGrantTypes().add("authorization_code");
|
||||
this.properties.getClient().put("foo", client);
|
||||
assertThatIllegalStateException().isThrownBy(this.properties::validate)
|
||||
.withMessage("Client authentication methods must not be empty.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationGrantTypesEmptyThrowsException() {
|
||||
OAuth2AuthorizationServerProperties.Client client = new OAuth2AuthorizationServerProperties.Client();
|
||||
client.getRegistration().setClientId("foo");
|
||||
client.getRegistration().getClientAuthenticationMethods().add("client_secret_basic");
|
||||
this.properties.getClient().put("foo", client);
|
||||
assertThatIllegalStateException().isThrownBy(this.properties::validate)
|
||||
.withMessage("Authorization grant types must not be empty.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultEndpointPropertiesMatchBuilderDefaults() {
|
||||
OAuth2AuthorizationServerProperties.Endpoint properties = new OAuth2AuthorizationServerProperties.Endpoint();
|
||||
AuthorizationServerSettings defaults = AuthorizationServerSettings.builder().build();
|
||||
assertThat(properties.getAuthorizationUri()).isEqualTo(defaults.getAuthorizationEndpoint());
|
||||
assertThat(properties.getDeviceAuthorizationUri()).isEqualTo(defaults.getDeviceAuthorizationEndpoint());
|
||||
assertThat(properties.getDeviceVerificationUri()).isEqualTo(defaults.getDeviceVerificationEndpoint());
|
||||
assertThat(properties.getTokenUri()).isEqualTo(defaults.getTokenEndpoint());
|
||||
assertThat(properties.getJwkSetUri()).isEqualTo(defaults.getJwkSetEndpoint());
|
||||
assertThat(properties.getTokenRevocationUri()).isEqualTo(defaults.getTokenRevocationEndpoint());
|
||||
assertThat(properties.getTokenIntrospectionUri()).isEqualTo(defaults.getTokenIntrospectionEndpoint());
|
||||
OAuth2AuthorizationServerProperties.OidcEndpoint oidc = properties.getOidc();
|
||||
assertThat(oidc.getLogoutUri()).isEqualTo(defaults.getOidcLogoutEndpoint());
|
||||
assertThat(oidc.getClientRegistrationUri()).isEqualTo(defaults.getOidcClientRegistrationEndpoint());
|
||||
assertThat(oidc.getUserInfoUri()).isEqualTo(defaults.getOidcUserInfoEndpoint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultClientPropertiesMatchBuilderDefaults() {
|
||||
OAuth2AuthorizationServerProperties.Client properties = new OAuth2AuthorizationServerProperties.Client();
|
||||
ClientSettings defaults = ClientSettings.builder().build();
|
||||
assertThat(properties.isRequireProofKey()).isEqualTo(defaults.isRequireProofKey());
|
||||
assertThat(properties.isRequireAuthorizationConsent()).isEqualTo(defaults.isRequireAuthorizationConsent());
|
||||
assertThat(properties.getJwkSetUri()).isEqualTo(defaults.getJwkSetUrl());
|
||||
assertThat(properties.getTokenEndpointAuthenticationSigningAlgorithm())
|
||||
.isEqualTo((defaults.getTokenEndpointAuthenticationSigningAlgorithm() != null)
|
||||
? defaults.getTokenEndpointAuthenticationSigningAlgorithm().getName() : null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultTokenPropertiesMatchBuilderDefaults() {
|
||||
OAuth2AuthorizationServerProperties.Token properties = new OAuth2AuthorizationServerProperties.Token();
|
||||
TokenSettings defaults = TokenSettings.builder().build();
|
||||
assertThat(properties.getAuthorizationCodeTimeToLive()).isEqualTo(defaults.getAuthorizationCodeTimeToLive());
|
||||
assertThat(properties.getAccessTokenTimeToLive()).isEqualTo(defaults.getAccessTokenTimeToLive());
|
||||
assertThat(properties.getAccessTokenFormat()).isEqualTo(defaults.getAccessTokenFormat().getValue());
|
||||
assertThat(properties.getDeviceCodeTimeToLive()).isEqualTo(defaults.getDeviceCodeTimeToLive());
|
||||
assertThat(properties.isReuseRefreshTokens()).isEqualTo(defaults.isReuseRefreshTokens());
|
||||
assertThat(properties.getRefreshTokenTimeToLive()).isEqualTo(defaults.getRefreshTokenTimeToLive());
|
||||
assertThat(properties.getIdTokenSignatureAlgorithm())
|
||||
.isEqualTo(defaults.getIdTokenSignatureAlgorithm().getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.authorization.autoconfigure.servlet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.BeanIds;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationServerWebSecurityConfiguration}.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
class OAuth2AuthorizationServerWebSecurityConfigurationTests {
|
||||
|
||||
private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver";
|
||||
|
||||
private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client";
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner();
|
||||
|
||||
@Test
|
||||
void webSecurityConfigurationConfiguresAuthorizationServerWithFormLogin() {
|
||||
this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class)
|
||||
.withPropertyValues(CLIENT_PREFIX + ".foo.registration.client-id=abcd",
|
||||
CLIENT_PREFIX + ".foo.registration.client-secret=secret",
|
||||
CLIENT_PREFIX + ".foo.registration.client-authentication-methods=client_secret_basic",
|
||||
CLIENT_PREFIX + ".foo.registration.authorization-grant-types=client_credentials",
|
||||
CLIENT_PREFIX + ".foo.registration.scopes=test")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasBean("authorizationServerSecurityFilterChain");
|
||||
assertThat(context).hasBean("defaultSecurityFilterChain");
|
||||
assertThat(context).hasBean("registeredClientRepository");
|
||||
assertThat(context).hasBean("authorizationServerSettings");
|
||||
assertThat(findFilter(context, OAuth2AuthorizationEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OAuth2TokenEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OAuth2TokenIntrospectionEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OAuth2TokenRevocationEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OAuth2AuthorizationServerMetadataEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OidcProviderConfigurationEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OidcUserInfoEndpointFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, BearerTokenAuthenticationFilter.class, 0)).isNotNull();
|
||||
assertThat(findFilter(context, OidcClientRegistrationEndpointFilter.class, 0)).isNull();
|
||||
assertThat(findFilter(context, UsernamePasswordAuthenticationFilter.class, 0)).isNull();
|
||||
assertThat(findFilter(context, DefaultLoginPageGeneratingFilter.class, 1)).isNotNull();
|
||||
assertThat(findFilter(context, UsernamePasswordAuthenticationFilter.class, 1)).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void securityFilterChainsBackOffWhenSecurityFilterChainBeanPresent() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(TestSecurityFilterChainConfiguration.class,
|
||||
TestOAuth2AuthorizationServerConfiguration.class)
|
||||
.withPropertyValues(CLIENT_PREFIX + ".foo.registration.client-id=abcd",
|
||||
CLIENT_PREFIX + ".foo.registration.client-secret=secret",
|
||||
CLIENT_PREFIX + ".foo.registration.client-authentication-methods=client_secret_basic",
|
||||
CLIENT_PREFIX + ".foo.registration.authorization-grant-types=client_credentials",
|
||||
CLIENT_PREFIX + ".foo.registration.scopes=test")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasBean("authServerSecurityFilterChain");
|
||||
assertThat(context).doesNotHaveBean("authorizationServerSecurityFilterChain");
|
||||
assertThat(context).hasBean("securityFilterChain");
|
||||
assertThat(context).doesNotHaveBean("defaultSecurityFilterChain");
|
||||
assertThat(context).hasBean("registeredClientRepository");
|
||||
assertThat(context).hasBean("authorizationServerSettings");
|
||||
assertThat(findFilter(context, BearerTokenAuthenticationFilter.class, 0)).isNull();
|
||||
assertThat(findFilter(context, UsernamePasswordAuthenticationFilter.class, 1)).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
private Filter findFilter(AssertableWebApplicationContext context, Class<? extends Filter> filter,
|
||||
int filterChainIndex) {
|
||||
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
|
||||
List<Filter> filters = filterChains.get(filterChainIndex).getFilters();
|
||||
return filters.stream().filter(filter::isInstance).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import({ TestRegisteredClientRepositoryConfiguration.class,
|
||||
OAuth2AuthorizationServerWebSecurityConfiguration.class,
|
||||
OAuth2AuthorizationServerJwtAutoConfiguration.class })
|
||||
static class TestOAuth2AuthorizationServerConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestRegisteredClientRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
RegisteredClient registeredClient = RegisteredClient.withId("test")
|
||||
.clientId("abcd")
|
||||
.clientSecret("secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.scope("test")
|
||||
.build();
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().issuer("https://example.com").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class TestSecurityFilterChainConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServer = OAuth2AuthorizationServerConfigurer
|
||||
.authorizationServer();
|
||||
http.securityMatcher(authorizationServer.getEndpointsMatcher())
|
||||
.with(authorizationServer, Customizer.withDefaults());
|
||||
http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.httpBasic(withDefaults()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user