diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 7420c840a2..9cd8994da6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -180,6 +180,7 @@ dependencies { exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.springframework.security:spring-security-messaging") + optional("org.springframework.security:spring-security-oauth2-authorization-server") optional("org.springframework.security:spring-security-oauth2-client") optional("org.springframework.security:spring-security-oauth2-jose") optional("org.springframework.security:spring-security-oauth2-resource-server") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerProperties.java new file mode 100644 index 0000000000..bd0779e7e3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerProperties.java @@ -0,0 +1,499 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +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 3.1.0 + */ +@ConfigurationProperties(prefix = "spring.security.oauth2.authorizationserver") +public class OAuth2AuthorizationServerProperties implements InitializingBean { + + /** + * URL of the Authorization Server's Issuer Identifier. + */ + private String issuer; + + /** + * Registered clients of the Authorization Server. + */ + private final Map client = new HashMap<>(); + + /** + * Authorization Server endpoints. + */ + private final Endpoint endpoint = new Endpoint(); + + public String getIssuer() { + return this.issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public Map 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; + + /** + * Authorization Server's OAuth 2.0 Token Endpoint. + */ + private String tokenUri; + + /** + * Authorization Server's JWK Set Endpoint. + */ + private String jwkSetUri; + + /** + * Authorization Server's OAuth 2.0 Token Revocation Endpoint. + */ + private String tokenRevocationUri; + + /** + * Authorization Server's OAuth 2.0 Token Introspection Endpoint. + */ + private String tokenIntrospectionUri; + + /** + * 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 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; + + /** + * Authorization Server's OpenID Connect 1.0 Client Registration Endpoint. + */ + private String clientRegistrationUri; + + /** + * Authorization Server's OpenID Connect 1.0 UserInfo Endpoint. + */ + private String userInfoUri; + + 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; + + /** + * Whether authorization consent is required when the client requests access. + */ + private boolean requireAuthorizationConsent; + + /** + * 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 clientAuthenticationMethods = new HashSet<>(); + + /** + * Authorization grant type(s) that the client may use. + */ + private Set authorizationGrantTypes = new HashSet<>(); + + /** + * Redirect URI(s) that the client may use in redirect-based flows. + */ + private Set redirectUris = new HashSet<>(); + + /** + * Redirect URI(s) that the client may use for logout. + */ + private Set postLogoutRedirectUris = new HashSet<>(); + + /** + * Scope(s) that the client may use. + */ + private Set 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 getClientAuthenticationMethods() { + return this.clientAuthenticationMethods; + } + + public void setClientAuthenticationMethods(Set clientAuthenticationMethods) { + this.clientAuthenticationMethods = clientAuthenticationMethods; + } + + public Set getAuthorizationGrantTypes() { + return this.authorizationGrantTypes; + } + + public void setAuthorizationGrantTypes(Set authorizationGrantTypes) { + this.authorizationGrantTypes = authorizationGrantTypes; + } + + public Set getRedirectUris() { + return this.redirectUris; + } + + public void setRedirectUris(Set redirectUris) { + this.redirectUris = redirectUris; + } + + public Set getPostLogoutRedirectUris() { + return this.postLogoutRedirectUris; + } + + public void setPostLogoutRedirectUris(Set postLogoutRedirectUris) { + this.postLogoutRedirectUris = postLogoutRedirectUris; + } + + public Set getScopes() { + return this.scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + } + + /** + * Token settings of the registered client. + */ + public static class Token { + + /** + * Time-to-live for an authorization code. + */ + private Duration authorizationCodeTimeToLive; + + /** + * Time-to-live for an access token. + */ + private Duration accessTokenTimeToLive; + + /** + * Token format for an access token. + */ + private String accessTokenFormat; + + /** + * Whether refresh tokens are reused or a new refresh token is issued when + * returning the access token response. + */ + private boolean reuseRefreshTokens; + + /** + * Time-to-live for a refresh token. + */ + private Duration refreshTokenTimeToLive; + + /** + * JWS algorithm for signing the ID Token. + */ + private String idTokenSignatureAlgorithm; + + 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 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapter.java new file mode 100644 index 0000000000..edebfe8939 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapter.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.autoconfigure.security.oauth2.server.OAuth2AuthorizationServerProperties.Client; +import org.springframework.boot.autoconfigure.security.oauth2.server.OAuth2AuthorizationServerProperties.Registration; +import org.springframework.boot.context.properties.PropertyMapper; +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.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +/** + * Adapter class to convert {@link Client} to a {@link RegisteredClient}. + * + * @author Steve Riesenberg + * @since 3.1.0 + */ +public final class OAuth2AuthorizationServerPropertiesRegistrationAdapter { + + private OAuth2AuthorizationServerPropertiesRegistrationAdapter() { + } + + public static List getRegisteredClients(OAuth2AuthorizationServerProperties properties) { + List registeredClients = new ArrayList<>(); + properties.getClient() + .forEach((registrationId, client) -> registeredClients.add(getRegisteredClient(registrationId, client))); + return registeredClients; + } + + private static 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 static 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(OAuth2AuthorizationServerPropertiesRegistrationAdapter::jwsAlgorithm) + .to(builder::tokenEndpointAuthenticationSigningAlgorithm); + return builder.build(); + } + + private static 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::isReuseRefreshTokens).to(builder::reuseRefreshTokens); + map.from(token::getRefreshTokenTimeToLive).to(builder::refreshTokenTimeToLive); + map.from(token::getIdTokenSignatureAlgorithm) + .as(OAuth2AuthorizationServerPropertiesRegistrationAdapter::signatureAlgorithm) + .to(builder::idTokenSignatureAlgorithm); + return builder.build(); + } + + private static JwsAlgorithm jwsAlgorithm(String signingAlgorithm) { + String name = signingAlgorithm.toUpperCase(); + JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.from(name); + if (jwsAlgorithm == null) { + jwsAlgorithm = MacAlgorithm.from(name); + } + return jwsAlgorithm; + } + + private static SignatureAlgorithm signatureAlgorithm(String signatureAlgorithm) { + return SignatureAlgorithm.from(signatureAlgorithm.toUpperCase()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapter.java new file mode 100644 index 0000000000..d99a9b088b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; + +/** + * Adapter class to convert {@link OAuth2AuthorizationServerProperties.Endpoint} to a + * {@link AuthorizationServerSettings}. + * + * @author Steve Riesenberg + * @since 3.1.0 + */ +public final class OAuth2AuthorizationServerPropertiesSettingsAdapter { + + private OAuth2AuthorizationServerPropertiesSettingsAdapter() { + } + + public static AuthorizationServerSettings getAuthorizationServerSettings( + OAuth2AuthorizationServerProperties properties) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + OAuth2AuthorizationServerProperties.Endpoint endpoint = properties.getEndpoint(); + OAuth2AuthorizationServerProperties.OidcEndpoint oidc = endpoint.getOidc(); + AuthorizationServerSettings.Builder builder = AuthorizationServerSettings.builder(); + map.from(properties::getIssuer).to(builder::issuer); + map.from(endpoint::getAuthorizationUri).to(builder::authorizationEndpoint); + 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(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/RegisteredClientsConfiguredCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/RegisteredClientsConfiguredCondition.java new file mode 100644 index 0000000000..f7bc2c8084 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/RegisteredClientsConfiguredCondition.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +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 + * @since 3.1.0 + */ +public class RegisteredClientsConfiguredCondition extends SpringBootCondition { + + private static final Bindable> 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 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 getRegistrations(Environment environment) { + return Binder.get(environment) + .bind("spring.security.oauth2.authorizationserver.client", STRING_CLIENT_MAP) + .orElse(Collections.emptyMap()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/package-info.java new file mode 100644 index 0000000000..e67ca7e3c9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for Spring Security's OAuth2 authorization server. + */ +package org.springframework.boot.autoconfigure.security.oauth2.server; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfiguration.java new file mode 100644 index 0000000000..1f011a9d8e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.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.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.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. + * + *

+ * Note: This configuration and + * {@link OAuth2AuthorizationServerJwtAutoConfiguration} work together to ensure that the + * {@link org.springframework.security.config.annotation.ObjectPostProcessor} is defined + * BEFORE {@link UserDetailsServiceAutoConfiguration} so that a + * {@link org.springframework.security.core.userdetails.UserDetailsService} can be created + * if necessary. + * + * @author Steve Riesenberg + * @since 3.1.0 + * @see OAuth2AuthorizationServerJwtAutoConfiguration + */ +@AutoConfiguration(before = { OAuth2ResourceServerAutoConfiguration.class, SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class }) +@ConditionalOnClass(OAuth2Authorization.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@Import({ OAuth2AuthorizationServerConfiguration.class, OAuth2AuthorizationServerWebSecurityConfiguration.class }) +public class OAuth2AuthorizationServerAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java new file mode 100644 index 0000000000..691303122f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.servlet; + +import java.util.List; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.oauth2.server.OAuth2AuthorizationServerProperties; +import org.springframework.boot.autoconfigure.security.oauth2.server.OAuth2AuthorizationServerPropertiesRegistrationAdapter; +import org.springframework.boot.autoconfigure.security.oauth2.server.OAuth2AuthorizationServerPropertiesSettingsAdapter; +import org.springframework.boot.autoconfigure.security.oauth2.server.RegisteredClientsConfiguredCondition; +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.RegisteredClient; +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 { + + @Bean + @ConditionalOnMissingBean + @Conditional(RegisteredClientsConfiguredCondition.class) + RegisteredClientRepository registeredClientRepository(OAuth2AuthorizationServerProperties properties) { + List registeredClients = OAuth2AuthorizationServerPropertiesRegistrationAdapter + .getRegisteredClients(properties); + return new InMemoryRegisteredClientRepository(registeredClients); + } + + @Bean + @ConditionalOnMissingBean + AuthorizationServerSettings authorizationServerSettings(OAuth2AuthorizationServerProperties properties) { + return OAuth2AuthorizationServerPropertiesSettingsAdapter.getAuthorizationServerSettings(properties); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfiguration.java new file mode 100644 index 0000000000..71251ed0aa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.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.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.context.annotation.Bean; +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 3.1.0 + */ +@AutoConfiguration(after = UserDetailsServiceAutoConfiguration.class) +@ConditionalOnClass(OAuth2Authorization.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class OAuth2AuthorizationServerJwtAutoConfiguration { + + @Bean + @ConditionalOnClass(JwtDecoder.class) + @ConditionalOnMissingBean + JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnMissingBean + JWKSource 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(); + // @formatter:off + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + 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; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java new file mode 100644 index 0000000000..37a496a938 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.servlet; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; +import org.springframework.boot.autoconfigure.security.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.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +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; + +/** + * {@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 { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults()); + // @formatter:off + http + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) + ); + // @formatter:on + return http.build(); + } + + @Bean + @Order(SecurityProperties.BASIC_AUTH_ORDER) + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .formLogin(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/package-info.java new file mode 100644 index 0000000000..fac8806edd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Spring Security's OAuth2 authorization server. + */ +package org.springframework.boot.autoconfigure.security.oauth2.server.servlet; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.java index d5490ebeb1..fcfb70fcca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.java @@ -37,7 +37,7 @@ import org.springframework.security.authentication.DefaultAuthenticationEventPub * @author Madhura Bhave * @since 1.0.0 */ -@AutoConfiguration +@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class) @ConditionalOnClass(DefaultAuthenticationEventPublisher.class) @EnableConfigurationProperties(SecurityProperties.class) @Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b9719cf4f4..c3ebd99315 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -112,6 +112,8 @@ org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2Clie org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration +org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerAutoConfiguration +org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerJwtAutoConfiguration org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapterTests.java new file mode 100644 index 0000000000..6b3bd4bbf0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapterTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +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.OAuth2TokenFormat; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2AuthorizationServerPropertiesRegistrationAdapter}. + * + * @author Steve Riesenberg + */ +class OAuth2AuthorizationServerPropertiesRegistrationAdapterTests { + + @Test + void getRegisteredClientsWhenValidParametersShouldAdapt() { + OAuth2AuthorizationServerProperties properties = new OAuth2AuthorizationServerProperties(); + OAuth2AuthorizationServerProperties.Client client = createClient(); + properties.getClient().put("foo", client); + List registeredClients = OAuth2AuthorizationServerPropertiesRegistrationAdapter + .getRegisteredClients(properties); + 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().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.setReuseRefreshTokens(true); + token.setIdTokenSignatureAlgorithm("rs512"); + return client; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapterTests.java new file mode 100644 index 0000000000..0ccde3864f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapterTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2AuthorizationServerPropertiesRegistrationAdapter}. + * + * @author Steve Riesenberg + */ +class OAuth2AuthorizationServerPropertiesSettingsAdapterTests { + + @Test + void getAuthorizationServerSettingsWhenValidParametersShouldAdapt() { + OAuth2AuthorizationServerProperties properties = createAuthorizationServerProperties(); + AuthorizationServerSettings settings = OAuth2AuthorizationServerPropertiesSettingsAdapter + .getAuthorizationServerSettings(properties); + assertThat(settings.getIssuer()).isEqualTo("https://example.com"); + assertThat(settings.getAuthorizationEndpoint()).isEqualTo("/authorize"); + 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"); + } + + private OAuth2AuthorizationServerProperties createAuthorizationServerProperties() { + OAuth2AuthorizationServerProperties properties = new OAuth2AuthorizationServerProperties(); + properties.setIssuer("https://example.com"); + OAuth2AuthorizationServerProperties.Endpoint endpoints = properties.getEndpoint(); + endpoints.setAuthorizationUri("/authorize"); + 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"); + return properties; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesTests.java new file mode 100644 index 0000000000..5009a11217 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server; + +import org.junit.jupiter.api.Test; + +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."); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java new file mode 100644 index 0000000000..947962e625 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.servlet; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +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.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 + void autoConfigurationDoesNotCauseUserDetailsServiceToBackOff() { + this.contextRunner.run((context) -> assertThat(context).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.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.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(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java new file mode 100644 index 0000000000..feecee8570 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.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 jwtDecoderConditionalOnClassJwtDecoder() { + this.contextRunner.withClassLoader(new FilteredClassLoader(JwtDecoder.class)) + .run((context) -> assertThat(context).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 jwkSource() { + return (jwkSelector, context) -> null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfigurationTests.java new file mode 100644 index 0000000000..dbf32229a9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfigurationTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.server.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.configuration.OAuth2AuthorizationServerConfiguration; +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; + +/** + * 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 filter, + int filterChainIndex) { + FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); + List filterChains = filterChain.getFilterChains(); + List 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 { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + return http.build(); + } + + @Bean + @Order(2) + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.httpBasic(Customizer.withDefaults()).build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7d483e78ff..e03296ac6c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1336,6 +1336,13 @@ bom { ] } } + library("Spring Authorization Server", "1.1.0-M2") { + group("org.springframework.security") { + modules = [ + "spring-security-oauth2-authorization-server" + ] + } + } library("Spring Batch", "5.0.1") { group("org.springframework.batch") { imports = [ diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index e16493ae20..0627bb0b80 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -320,6 +320,7 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], "spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], "spring-security-version": securityVersion, + "spring-authorization-server-version": versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"], "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"], "tomcat-version": tomcatVersion.split("\\.").take(2).join('.'), "remote-spring-application-output": runRemoteSpringApplicationExample.outputs.files.singleFile, diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index eefbbe0f01..6393f3bed2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -94,6 +94,7 @@ :spring-security: https://spring.io/projects/spring-security :spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version} :spring-authorization-server: https://spring.io/projects/spring-authorization-server +:spring-authorization-server-docs: https://docs.spring.io/spring-authorization-server/docs/{spring-authorization-server-version}/reference/html :spring-session: https://spring.io/projects/spring-session :spring-webservices-docs: https://docs.spring.io/spring-ws/docs/{spring-webservices-version}/reference/html/ :ant-docs: https://ant.apache.org/manual diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc index ac868a860c..8303dcea5e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc @@ -227,7 +227,72 @@ Alternatively, you can define your own `OpaqueTokenIntrospector` bean for servle [[web.security.oauth2.authorization-server]] ==== Authorization Server -You can use the {spring-authorization-server}[Spring Authorization Server] project to implement an OAuth 2.0 Authorization Server. +If you have `spring-security-oauth2-authorization-server` on your classpath, you can take advantage of some auto-configuration to set up an OAuth2 Authorization Server. +This configuration makes use of the properties under `OAuth2AuthorizationServerProperties`. +The properties are only applicable for servlet applications. + +You can register multiple OAuth2 clients under the `spring.security.oauth2.authorizationserver.client` prefix, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + authorizationserver: + client: + my-client-1: + registration: + client-id: "abcd" + client-secret: "{noop}secret1" + client-authentication-methods: + - "client_secret_basic" + authorization-grant-types: + - "authorization_code" + - "refresh_token" + redirect-uris: + - "https://my-client-1.com/login/oauth2/code/abcd" + - "https://my-client-1.com/authorized" + scopes: + - "openid" + - "profile" + - "email" + - "phone" + - "address" + require-authorization-consent: true + my-client-2: + registration: + client-id: "efgh" + client-secret: "{noop}secret2" + client-authentication-methods: + - "client_secret_jwt" + authorization-grant-types: + - "client_credentials" + scopes: + - "user.read" + - "user.write" + jwk-set-uri: "https://my-client-2.com/jwks" + token-endpoint-authentication-signing-algorithm: "RS256" +---- + +NOTE: The `client-secret` property must be in a format that can be matched by the configured `PasswordEncoder`. +The default instance of `PasswordEncoder` is created via `PasswordEncoderFactories.createDelegatingPasswordEncoder()`. + +The auto-configuration Spring Boot provides for Spring Authorization Server is designed for getting started quickly. +Most applications will require customization and will want to define several beans to override auto-configuration. + +The following components can be defined as beans to override auto-configuration specific to Spring Authorization Server: + +* `RegisteredClientRepository` +* `AuthorizationServerSettings` +* `SecurityFilterChain` +* `com.nimbusds.jose.jwk.source.JWKSource` +* `JwtDecoder` + +TIP: Spring Boot auto-configures an `InMemoryRegisteredClientRepository` which is used by Spring Authorization Server for the management of registered clients. +The `InMemoryRegisteredClientRepository` has limited capabilities and we recommend using it only for development environments. +For production environments, consider using a `JdbcRegisteredClientRepository` or creating your own implementation of `RegisteredClientRepository`. + +Additional information can be found in the {spring-authorization-server-docs}/getting-started.html[Getting Started] chapter of the {spring-authorization-server-docs}/index.html[Spring Authorization Server Reference Guide]. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-authorization-server/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-authorization-server/build.gradle new file mode 100644 index 0000000000..15caf0d0c4 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-authorization-server/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Authorization Server features" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.security:spring-security-oauth2-authorization-server") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/build.gradle new file mode 100644 index 0000000000..a61db53d14 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot OAuth2 Authorization Server smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-oauth2-authorization-server")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation("org.apache.httpcomponents.client5:httpclient5") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplication.java new file mode 100644 index 0000000000..7d12c3b2f7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.oauth2.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleOAuth2AuthorizationServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleOAuth2AuthorizationServerApplication.class); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/resources/application.yml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/resources/application.yml new file mode 100644 index 0000000000..f2ccb0d9b9 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + security: + oauth2: + authorizationserver: + issuer: https://provider.com + endpoint: + authorization-uri: /authorize + token-uri: /token + jwk-set-uri: /jwks + token-revocation-uri: /revoke + token-introspection-uri: /introspect + oidc: + logout-uri: /logout + client-registration-uri: /register + user-info-uri: /user + client: + messaging-client: + registration: + client-id: messaging-client + client-secret: "{noop}secret" + client-authentication-methods: + - client_secret_basic + authorization-grant-types: + - client_credentials + scopes: + - message.read + - message.write diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java new file mode 100644 index 0000000000..ce2d7b75f0 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.oauth2.server; + +import java.net.URI; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; +import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class SampleOAuth2AuthorizationServerApplicationTests { + + private static final ParameterizedTypeReference> MAP_TYPE_REFERENCE = new ParameterizedTypeReference<>() { + }; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void openidConfigurationShouldAllowAccess() { + ResponseEntity> entity = this.restTemplate.exchange("/.well-known/openid-configuration", + HttpMethod.GET, null, MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + + OidcProviderConfiguration config = OidcProviderConfiguration.withClaims(entity.getBody()).build(); + assertThat(config.getIssuer().toString()).isEqualTo("https://provider.com"); + assertThat(config.getAuthorizationEndpoint().toString()).isEqualTo("https://provider.com/authorize"); + assertThat(config.getTokenEndpoint().toString()).isEqualTo("https://provider.com/token"); + assertThat(config.getJwkSetUrl().toString()).isEqualTo("https://provider.com/jwks"); + assertThat(config.getTokenRevocationEndpoint().toString()).isEqualTo("https://provider.com/revoke"); + assertThat(config.getEndSessionEndpoint().toString()).isEqualTo("https://provider.com/logout"); + assertThat(config.getTokenIntrospectionEndpoint().toString()).isEqualTo("https://provider.com/introspect"); + assertThat(config.getUserInfoEndpoint().toString()).isEqualTo("https://provider.com/user"); + // OIDC Client Registration is disabled by default + assertThat(config.getClientRegistrationEndpoint()).isNull(); + } + + @Test + void authServerMetadataShouldAllowAccess() { + ResponseEntity> entity = this.restTemplate + .exchange("/.well-known/oauth-authorization-server", HttpMethod.GET, null, MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + + OAuth2AuthorizationServerMetadata config = OAuth2AuthorizationServerMetadata.withClaims(entity.getBody()) + .build(); + assertThat(config.getIssuer().toString()).isEqualTo("https://provider.com"); + assertThat(config.getAuthorizationEndpoint().toString()).isEqualTo("https://provider.com/authorize"); + assertThat(config.getTokenEndpoint().toString()).isEqualTo("https://provider.com/token"); + assertThat(config.getJwkSetUrl().toString()).isEqualTo("https://provider.com/jwks"); + assertThat(config.getTokenRevocationEndpoint().toString()).isEqualTo("https://provider.com/revoke"); + assertThat(config.getTokenIntrospectionEndpoint().toString()).isEqualTo("https://provider.com/introspect"); + // OIDC Client Registration is disabled by default + assertThat(config.getClientRegistrationEndpoint()).isNull(); + } + + @Test + void anonymousShouldRedirectToLogin() { + ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); + assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); + } + + @Test + void validTokenRequestShouldReturnTokenResponse() { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("messaging-client", "secret"); + HttpEntity request = new HttpEntity<>(headers); + String requestUri = UriComponentsBuilder.fromUriString("/token") + .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") + .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") + .toUriString(); + ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + Map tokenResponse = Objects.requireNonNull(entity.getBody()); + assertThat(tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN)).isNotNull(); + assertThat(tokenResponse.get(OAuth2ParameterNames.EXPIRES_IN)).isNotNull(); + assertThat(tokenResponse.get(OAuth2ParameterNames.SCOPE)).isEqualTo("message.read message.write"); + assertThat(tokenResponse.get(OAuth2ParameterNames.TOKEN_TYPE)) + .isEqualTo(OAuth2AccessToken.TokenType.BEARER.getValue()); + } + +}