From 25d77ee70bab86402f2310c6671faa424fd94d99 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 27 Jan 2023 16:03:25 -0600 Subject: [PATCH 1/2] Add Spring Authorization Server support See gh-34003 --- .../spring-boot-autoconfigure/build.gradle | 1 + .../OAuth2AuthorizationServerProperties.java | 499 ++++++++++++++++++ ...onServerPropertiesRegistrationAdapter.java | 159 ++++++ ...zationServerPropertiesSettingsAdapter.java | 52 ++ .../RegisteredClientsConfiguredCondition.java | 66 +++ .../security/oauth2/server/package-info.java | 20 + ...2AuthorizationServerAutoConfiguration.java | 53 ++ ...thorizationServerJwtAutoConfiguration.java | 41 ++ ...h2AuthorizationServerJwtConfiguration.java | 87 +++ ...rizationServerPropertiesConfiguration.java | 60 +++ ...izationServerWebSecurityConfiguration.java | 70 +++ .../oauth2/server/servlet/package-info.java | 20 + ...ot.autoconfigure.AutoConfiguration.imports | 2 + ...verPropertiesRegistrationAdapterTests.java | 100 ++++ ...nServerPropertiesSettingsAdapterTests.java | 67 +++ ...th2AuthorizationServerPropertiesTests.java | 72 +++ ...horizationServerJwtConfigurationTests.java | 110 ++++ ...ionServerPropertiesConfigurationTests.java | 189 +++++++ ...onServerWebSecurityConfigurationTests.java | 154 ++++++ .../spring-boot-dependencies/build.gradle | 7 + .../spring-boot-docs/build.gradle | 1 + .../src/docs/asciidoc/attributes.adoc | 1 + .../docs/asciidoc/web/spring-security.adoc | 67 ++- .../build.gradle | 10 + .../build.gradle | 14 + ...eOAuth2AuthorizationServerApplication.java | 29 + .../src/main/resources/application.yml | 27 + ...h2AuthorizationServerApplicationTests.java | 125 +++++ 28 files changed, 2102 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapter.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapter.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/RegisteredClientsConfiguredCondition.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapterTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapterTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-authorization-server/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/main/resources/application.yml create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java 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..7cb9187f80 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapter.java @@ -0,0 +1,159 @@ +/* + * 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; +import org.springframework.util.CollectionUtils; + +/** + * 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); + if (!CollectionUtils.isEmpty(registration.getClientAuthenticationMethods())) { + registration.getClientAuthenticationMethods() + .forEach((clientAuthenticationMethod) -> map.from(clientAuthenticationMethod) + .as(OAuth2AuthorizationServerPropertiesRegistrationAdapter::clientAuthenticationMethod) + .to(builder::clientAuthenticationMethod)); + } + if (!CollectionUtils.isEmpty(registration.getAuthorizationGrantTypes())) { + registration.getAuthorizationGrantTypes() + .forEach((authorizationGrantType) -> map.from(authorizationGrantType) + .as(OAuth2AuthorizationServerPropertiesRegistrationAdapter::authorizationGrantType) + .to(builder::authorizationGrantType)); + } + if (!CollectionUtils.isEmpty(registration.getRedirectUris())) { + registration.getRedirectUris().forEach((redirectUri) -> map.from(redirectUri).to(builder::redirectUri)); + } + if (!CollectionUtils.isEmpty(registration.getPostLogoutRedirectUris())) { + registration.getPostLogoutRedirectUris() + .forEach((redirectUri) -> map.from(redirectUri).to(builder::postLogoutRedirectUri)); + } + if (!CollectionUtils.isEmpty(registration.getScopes())) { + 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 ClientAuthenticationMethod clientAuthenticationMethod(String clientAuthenticationMethod) { + if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) { + return ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + } + else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) { + return ClientAuthenticationMethod.CLIENT_SECRET_POST; + } + else if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientAuthenticationMethod)) { + return ClientAuthenticationMethod.CLIENT_SECRET_JWT; + } + else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientAuthenticationMethod)) { + return ClientAuthenticationMethod.PRIVATE_KEY_JWT; + } + else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) { + return ClientAuthenticationMethod.NONE; + } + else { + return new ClientAuthenticationMethod(clientAuthenticationMethod); + } + } + + private static AuthorizationGrantType authorizationGrantType(String authorizationGrantType) { + if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) { + return AuthorizationGrantType.AUTHORIZATION_CODE; + } + else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) { + return AuthorizationGrantType.CLIENT_CREDENTIALS; + } + else { + return new AuthorizationGrantType(authorizationGrantType); + } + } + + 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..6720564014 --- /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,53 @@ +/* + * 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.SecurityFilterAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; +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, SecurityFilterAutoConfiguration.class }) +@ConditionalOnClass(OAuth2Authorization.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@Import({ OAuth2AuthorizationServerPropertiesConfiguration.class, + OAuth2AuthorizationServerWebSecurityConfiguration.class, ObjectPostProcessorConfiguration.class }) +public class OAuth2AuthorizationServerAutoConfiguration { + +} 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..2b4e95bb56 --- /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,41 @@ +/* + * 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.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; + +/** + * {@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 + * @see OAuth2AuthorizationServerAutoConfiguration + */ +@AutoConfiguration(after = { UserDetailsServiceAutoConfiguration.class }) +@ConditionalOnClass(OAuth2Authorization.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@Import(OAuth2AuthorizationServerJwtConfiguration.class) +public class OAuth2AuthorizationServerJwtAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java new file mode 100644 index 0000000000..8809ea029c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java @@ -0,0 +1,87 @@ +/* + * 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.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; + +/** + * {@link Configuration @Configuration} for JWT support for endpoints of the OAuth2 + * authorization server that require it (e.g. User Info, Client Registration). + * + * @author Steve Riesenberg + */ +@Configuration(proxyBeanMethods = false) +class OAuth2AuthorizationServerJwtConfiguration { + + @Bean + @ConditionalOnClass(JwtDecoder.class) + @ConditionalOnMissingBean + JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnClass(JWKSource.class) + @ConditionalOnMissingBean + JWKSource jwkSource() { + 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 + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + 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/OAuth2AuthorizationServerPropertiesConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfiguration.java new file mode 100644 index 0000000000..b3495cbcc9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfiguration.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 OAuth2AuthorizationServerPropertiesConfiguration { + + @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/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..6800b61c51 --- /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,70 @@ +/* + * 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.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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +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 +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/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..04ff163ca5 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesRegistrationAdapterTests.java @@ -0,0 +1,100 @@ +/* + * 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 + */ +public 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..476c47001b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/OAuth2AuthorizationServerPropertiesSettingsAdapterTests.java @@ -0,0 +1,67 @@ +/* + * 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 + */ +public 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..27b18c2eb1 --- /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 + */ +public 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/OAuth2AuthorizationServerJwtConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java new file mode 100644 index 0000000000..dd2d562932 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java @@ -0,0 +1,110 @@ +/* + * 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.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2AuthorizationServerJwtConfiguration}. + * + * @author Steve Riesenberg + */ +public class OAuth2AuthorizationServerJwtConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void jwtConfigurationConfiguresJwtDecoderWithGeneratedKey() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestJwtConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("jwtDecoder"); + assertThat(context).hasBean("jwkSource"); + + assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class); + assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class); + }); + // @formatter:on + } + + @Test + void jwtDecoderBacksOffWhenBeanPresent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestJwtDecoderConfiguration.class, TestJwtConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("jwtDecoder"); + assertThat(context).hasBean("jwkSource"); + + assertThat(context.getBean("jwtDecoder")).isNotInstanceOf(NimbusJwtDecoder.class); + assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class); + }); + // @formatter:on + } + + @Test + void jwkSourceBacksOffWhenBeanPresent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestJwkSourceConfiguration.class, TestJwtConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("jwtDecoder"); + assertThat(context).hasBean("jwkSource"); + + assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class); + assertThat(context.getBean("jwkSource")).isNotInstanceOf(ImmutableJWKSet.class); + }); + // @formatter:on + } + + @Configuration + @Import(OAuth2AuthorizationServerJwtConfiguration.class) + static class TestJwtConfiguration { + + } + + @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/OAuth2AuthorizationServerPropertiesConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java new file mode 100644 index 0000000000..ee552cc571 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java @@ -0,0 +1,189 @@ +/* + * 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.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +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.settings.AuthorizationServerSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Riesenberg + */ +public class OAuth2AuthorizationServerPropertiesConfigurationTests { + + private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver"; + + private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void registeredClientRepositoryBeanShouldNotBeCreatedWhenPropertiesAbsent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(RegisteredClientRepository.class)); + // @formatter:on + } + + @Test + void registeredClientRepositoryBeanShouldBeCreatedWhenPropertiesPresent() { + // @formatter:off + 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) -> { + 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"); + }); + // @formatter:on + } + + @Test + void registeredClientRepositoryBacksOffWhenRegisteredClientRepositoryBeanPresent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestRegisteredClientRepositoryConfiguration.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.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"); + }); + // @formatter:on + } + + @Test + void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesAbsent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(AuthorizationServerSettings.class)); + // @formatter:on + } + + @Test + void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesPresent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) + .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"); + }); + // @formatter:on + } + + @Test + void authorizationServerSettingsBacksOffWhenAuthorizationServerSettingsBeanPresent() { + // @formatter:off + this.contextRunner.withUserConfiguration(TestAuthorizationServerSettingsConfiguration.class, + TestOAuth2AuthorizationServerConfiguration.class) + .withPropertyValues(PROPERTIES_PREFIX + ".issuer=https://test.com") + .run((context) -> { + AuthorizationServerSettings settings = context.getBean(AuthorizationServerSettings.class); + assertThat(settings.getIssuer()).isEqualTo("https://example.com"); + }); + // @formatter:on + } + + @Configuration + @EnableWebSecurity + @Import({ OAuth2AuthorizationServerPropertiesConfiguration.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); + } + + } + + @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/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..3276fa42d7 --- /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,154 @@ +/* + * 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.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +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.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.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; + +/** + * @author Steve Riesenberg + */ +public class OAuth2AuthorizationServerWebSecurityConfigurationTests { + + private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver"; + + private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void webSecurityConfigurationConfiguresAuthorizationServerWithFormLogin() { + // @formatter:off + 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(); + }); + // @formatter:on + } + + @Test + void securityFilterChainsBackOffWhenSecurityFilterChainBeanPresent() { + // @formatter:off + 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(); + }); + // @formatter:on + } + + private Filter findFilter(AssertableApplicationContext 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({ OAuth2AuthorizationServerPropertiesConfiguration.class, + OAuth2AuthorizationServerWebSecurityConfiguration.class, OAuth2AuthorizationServerJwtConfiguration.class }) + static class TestOAuth2AuthorizationServerConfiguration { + + } + + @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..8568aef17d 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-M1") { + 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..d1616700f5 --- /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,125 @@ +/* + * 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); + // @formatter:off + 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(); + // @formatter:on + + 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()); + } + +} From e6f602cec0b8ec0da30d97099189448698111cad Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 21 Mar 2023 16:34:49 -0700 Subject: [PATCH 2/2] Polish "Add Spring Authorization Server support" See gh-34003 --- ...onServerPropertiesRegistrationAdapter.java | 68 ++----- ...2AuthorizationServerAutoConfiguration.java | 9 +- ...uth2AuthorizationServerConfiguration.java} | 2 +- ...thorizationServerJwtAutoConfiguration.java | 65 +++++- ...h2AuthorizationServerJwtConfiguration.java | 87 -------- ...izationServerWebSecurityConfiguration.java | 4 + .../servlet/SecurityAutoConfiguration.java | 2 +- ...verPropertiesRegistrationAdapterTests.java | 9 +- ...nServerPropertiesSettingsAdapterTests.java | 5 +- ...th2AuthorizationServerPropertiesTests.java | 2 +- ...orizationServerAutoConfigurationTests.java | 184 +++++++++++++++++ ...zationServerJwtAutoConfigurationTests.java | 107 ++++++++++ ...horizationServerJwtConfigurationTests.java | 110 ---------- ...ionServerPropertiesConfigurationTests.java | 189 ------------------ ...onServerWebSecurityConfigurationTests.java | 134 ++++++++----- .../spring-boot-dependencies/build.gradle | 2 +- ...h2AuthorizationServerApplicationTests.java | 12 +- 17 files changed, 461 insertions(+), 530 deletions(-) rename spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/{OAuth2AuthorizationServerPropertiesConfiguration.java => OAuth2AuthorizationServerConfiguration.java} (98%) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtAutoConfigurationTests.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java 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 index 7cb9187f80..edebfe8939 100644 --- 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 @@ -31,7 +31,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere 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; -import org.springframework.util.CollectionUtils; /** * Adapter class to convert {@link Client} to a {@link RegisteredClient}. @@ -58,66 +57,23 @@ public final class OAuth2AuthorizationServerPropertiesRegistrationAdapter { map.from(registration::getClientId).to(builder::clientId); map.from(registration::getClientSecret).to(builder::clientSecret); map.from(registration::getClientName).to(builder::clientName); - if (!CollectionUtils.isEmpty(registration.getClientAuthenticationMethods())) { - registration.getClientAuthenticationMethods() - .forEach((clientAuthenticationMethod) -> map.from(clientAuthenticationMethod) - .as(OAuth2AuthorizationServerPropertiesRegistrationAdapter::clientAuthenticationMethod) - .to(builder::clientAuthenticationMethod)); - } - if (!CollectionUtils.isEmpty(registration.getAuthorizationGrantTypes())) { - registration.getAuthorizationGrantTypes() - .forEach((authorizationGrantType) -> map.from(authorizationGrantType) - .as(OAuth2AuthorizationServerPropertiesRegistrationAdapter::authorizationGrantType) - .to(builder::authorizationGrantType)); - } - if (!CollectionUtils.isEmpty(registration.getRedirectUris())) { - registration.getRedirectUris().forEach((redirectUri) -> map.from(redirectUri).to(builder::redirectUri)); - } - if (!CollectionUtils.isEmpty(registration.getPostLogoutRedirectUris())) { - registration.getPostLogoutRedirectUris() - .forEach((redirectUri) -> map.from(redirectUri).to(builder::postLogoutRedirectUri)); - } - if (!CollectionUtils.isEmpty(registration.getScopes())) { - registration.getScopes().forEach((scope) -> map.from(scope).to(builder::scope)); - } + 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 ClientAuthenticationMethod clientAuthenticationMethod(String clientAuthenticationMethod) { - if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) { - return ClientAuthenticationMethod.CLIENT_SECRET_BASIC; - } - else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) { - return ClientAuthenticationMethod.CLIENT_SECRET_POST; - } - else if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientAuthenticationMethod)) { - return ClientAuthenticationMethod.CLIENT_SECRET_JWT; - } - else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientAuthenticationMethod)) { - return ClientAuthenticationMethod.PRIVATE_KEY_JWT; - } - else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) { - return ClientAuthenticationMethod.NONE; - } - else { - return new ClientAuthenticationMethod(clientAuthenticationMethod); - } - } - - private static AuthorizationGrantType authorizationGrantType(String authorizationGrantType) { - if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) { - return AuthorizationGrantType.AUTHORIZATION_CODE; - } - else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) { - return AuthorizationGrantType.CLIENT_CREDENTIALS; - } - else { - return new AuthorizationGrantType(authorizationGrantType); - } - } - private static ClientSettings getClientSettings(Client client, PropertyMapper map) { ClientSettings.Builder builder = ClientSettings.builder(); map.from(client::isRequireProofKey).to(builder::requireProofKey); 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 index 6720564014..1f011a9d8e 100644 --- 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 @@ -21,10 +21,9 @@ 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.SecurityFilterAutoConfiguration; +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.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; /** @@ -43,11 +42,11 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat * @since 3.1.0 * @see OAuth2AuthorizationServerJwtAutoConfiguration */ -@AutoConfiguration(before = { OAuth2ResourceServerAutoConfiguration.class, SecurityFilterAutoConfiguration.class }) +@AutoConfiguration(before = { OAuth2ResourceServerAutoConfiguration.class, SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class }) @ConditionalOnClass(OAuth2Authorization.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@Import({ OAuth2AuthorizationServerPropertiesConfiguration.class, - OAuth2AuthorizationServerWebSecurityConfiguration.class, ObjectPostProcessorConfiguration.class }) +@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/OAuth2AuthorizationServerPropertiesConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java similarity index 98% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfiguration.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java index b3495cbcc9..691303122f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerConfiguration.java @@ -40,7 +40,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(OAuth2AuthorizationServerProperties.class) -class OAuth2AuthorizationServerPropertiesConfiguration { +class OAuth2AuthorizationServerConfiguration { @Bean @ConditionalOnMissingBean 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 index 2b4e95bb56..71251ed0aa 100644 --- 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 @@ -16,13 +16,30 @@ 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.Import; +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 @@ -30,12 +47,52 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat * * @author Steve Riesenberg * @since 3.1.0 - * @see OAuth2AuthorizationServerAutoConfiguration */ -@AutoConfiguration(after = { UserDetailsServiceAutoConfiguration.class }) +@AutoConfiguration(after = UserDetailsServiceAutoConfiguration.class) @ConditionalOnClass(OAuth2Authorization.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@Import(OAuth2AuthorizationServerJwtConfiguration.class) 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/OAuth2AuthorizationServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java deleted file mode 100644 index 8809ea029c..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; - -/** - * {@link Configuration @Configuration} for JWT support for endpoints of the OAuth2 - * authorization server that require it (e.g. User Info, Client Registration). - * - * @author Steve Riesenberg - */ -@Configuration(proxyBeanMethods = false) -class OAuth2AuthorizationServerJwtConfiguration { - - @Bean - @ConditionalOnClass(JwtDecoder.class) - @ConditionalOnMissingBean - JwtDecoder jwtDecoder(JWKSource jwkSource) { - return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - @ConditionalOnClass(JWKSource.class) - @ConditionalOnMissingBean - JWKSource jwkSource() { - 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 - JWKSet jwkSet = new JWKSet(rsaKey); - return new ImmutableJWKSet<>(jwkSet); - } - - 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 index 6800b61c51..37a496a938 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -25,8 +26,10 @@ 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; @@ -37,6 +40,7 @@ import org.springframework.security.web.authentication.LoginUrlAuthenticationEnt */ @Configuration(proxyBeanMethods = false) @ConditionalOnDefaultWebSecurity +@ConditionalOnBean({ RegisteredClientRepository.class, AuthorizationServerSettings.class }) class OAuth2AuthorizationServerWebSecurityConfiguration { @Bean 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/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 index 04ff163ca5..6b3bd4bbf0 100644 --- 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 @@ -34,18 +34,16 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Steve Riesenberg */ -public class OAuth2AuthorizationServerPropertiesRegistrationAdapterTests { +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"); @@ -56,13 +54,11 @@ public class OAuth2AuthorizationServerPropertiesRegistrationAdapterTests { 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)); @@ -77,7 +73,6 @@ public class OAuth2AuthorizationServerPropertiesRegistrationAdapterTests { client.setRequireAuthorizationConsent(true); client.setJwkSetUri("https://example.com/jwks"); client.setTokenEndpointAuthenticationSigningAlgorithm("rs256"); - OAuth2AuthorizationServerProperties.Registration registration = client.getRegistration(); registration.setClientId("foo"); registration.setClientSecret("secret"); @@ -86,14 +81,12 @@ public class OAuth2AuthorizationServerPropertiesRegistrationAdapterTests { 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 index 476c47001b..0ccde3864f 100644 --- 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 @@ -27,12 +27,11 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Steve Riesenberg */ -public class OAuth2AuthorizationServerPropertiesSettingsAdapterTests { +class OAuth2AuthorizationServerPropertiesSettingsAdapterTests { @Test void getAuthorizationServerSettingsWhenValidParametersShouldAdapt() { OAuth2AuthorizationServerProperties properties = createAuthorizationServerProperties(); - AuthorizationServerSettings settings = OAuth2AuthorizationServerPropertiesSettingsAdapter .getAuthorizationServerSettings(properties); assertThat(settings.getIssuer()).isEqualTo("https://example.com"); @@ -49,14 +48,12 @@ public class OAuth2AuthorizationServerPropertiesSettingsAdapterTests { 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"); 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 index 27b18c2eb1..5009a11217 100644 --- 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 @@ -25,7 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Steve Riesenberg */ -public class OAuth2AuthorizationServerPropertiesTests { +class OAuth2AuthorizationServerPropertiesTests { private final OAuth2AuthorizationServerProperties properties = new OAuth2AuthorizationServerProperties(); 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/OAuth2AuthorizationServerJwtConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java deleted file mode 100644 index dd2d562932..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerJwtConfigurationTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OAuth2AuthorizationServerJwtConfiguration}. - * - * @author Steve Riesenberg - */ -public class OAuth2AuthorizationServerJwtConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - - @Test - void jwtConfigurationConfiguresJwtDecoderWithGeneratedKey() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestJwtConfiguration.class) - .run((context) -> { - assertThat(context).hasBean("jwtDecoder"); - assertThat(context).hasBean("jwkSource"); - - assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class); - assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class); - }); - // @formatter:on - } - - @Test - void jwtDecoderBacksOffWhenBeanPresent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestJwtDecoderConfiguration.class, TestJwtConfiguration.class) - .run((context) -> { - assertThat(context).hasBean("jwtDecoder"); - assertThat(context).hasBean("jwkSource"); - - assertThat(context.getBean("jwtDecoder")).isNotInstanceOf(NimbusJwtDecoder.class); - assertThat(context.getBean("jwkSource")).isInstanceOf(ImmutableJWKSet.class); - }); - // @formatter:on - } - - @Test - void jwkSourceBacksOffWhenBeanPresent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestJwkSourceConfiguration.class, TestJwtConfiguration.class) - .run((context) -> { - assertThat(context).hasBean("jwtDecoder"); - assertThat(context).hasBean("jwkSource"); - - assertThat(context.getBean("jwtDecoder")).isInstanceOf(NimbusJwtDecoder.class); - assertThat(context.getBean("jwkSource")).isNotInstanceOf(ImmutableJWKSet.class); - }); - // @formatter:on - } - - @Configuration - @Import(OAuth2AuthorizationServerJwtConfiguration.class) - static class TestJwtConfiguration { - - } - - @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/OAuth2AuthorizationServerPropertiesConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java deleted file mode 100644 index ee552cc571..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerPropertiesConfigurationTests.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -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.settings.AuthorizationServerSettings; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class OAuth2AuthorizationServerPropertiesConfigurationTests { - - private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver"; - - private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - - @Test - void registeredClientRepositoryBeanShouldNotBeCreatedWhenPropertiesAbsent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(RegisteredClientRepository.class)); - // @formatter:on - } - - @Test - void registeredClientRepositoryBeanShouldBeCreatedWhenPropertiesPresent() { - // @formatter:off - 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) -> { - 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"); - }); - // @formatter:on - } - - @Test - void registeredClientRepositoryBacksOffWhenRegisteredClientRepositoryBeanPresent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestRegisteredClientRepositoryConfiguration.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.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"); - }); - // @formatter:on - } - - @Test - void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesAbsent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(AuthorizationServerSettings.class)); - // @formatter:on - } - - @Test - void authorizationServerSettingsBeanShouldBeCreatedWhenPropertiesPresent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestOAuth2AuthorizationServerConfiguration.class) - .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"); - }); - // @formatter:on - } - - @Test - void authorizationServerSettingsBacksOffWhenAuthorizationServerSettingsBeanPresent() { - // @formatter:off - this.contextRunner.withUserConfiguration(TestAuthorizationServerSettingsConfiguration.class, - TestOAuth2AuthorizationServerConfiguration.class) - .withPropertyValues(PROPERTIES_PREFIX + ".issuer=https://test.com") - .run((context) -> { - AuthorizationServerSettings settings = context.getBean(AuthorizationServerSettings.class); - assertThat(settings.getIssuer()).isEqualTo("https://example.com"); - }); - // @formatter:on - } - - @Configuration - @EnableWebSecurity - @Import({ OAuth2AuthorizationServerPropertiesConfiguration.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); - } - - } - - @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/OAuth2AuthorizationServerWebSecurityConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfigurationTests.java index 3276fa42d7..dbf32229a9 100644 --- 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 @@ -21,8 +21,8 @@ import java.util.List; import jakarta.servlet.Filter; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; +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; @@ -31,10 +31,16 @@ 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; @@ -49,74 +55,69 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageGenera import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link OAuth2AuthorizationServerWebSecurityConfiguration}. + * * @author Steve Riesenberg */ -public class OAuth2AuthorizationServerWebSecurityConfigurationTests { +class OAuth2AuthorizationServerWebSecurityConfigurationTests { private static final String PROPERTIES_PREFIX = "spring.security.oauth2.authorizationserver"; private static final String CLIENT_PREFIX = PROPERTIES_PREFIX + ".client"; - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(); @Test void webSecurityConfigurationConfiguresAuthorizationServerWithFormLogin() { - // @formatter:off 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(); - }); - // @formatter:on + .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() { - // @formatter:off - 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(); - }); - // @formatter:on + 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(AssertableApplicationContext context, Class filter, + private Filter findFilter(AssertableWebApplicationContext context, Class filter, int filterChainIndex) { FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); List filterChains = filterChain.getFilterChains(); @@ -126,12 +127,35 @@ public class OAuth2AuthorizationServerWebSecurityConfigurationTests { @Configuration @EnableWebSecurity - @Import({ OAuth2AuthorizationServerPropertiesConfiguration.class, - OAuth2AuthorizationServerWebSecurityConfiguration.class, OAuth2AuthorizationServerJwtConfiguration.class }) + @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 { diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8568aef17d..e03296ac6c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1336,7 +1336,7 @@ bom { ] } } - library("Spring Authorization Server", "1.1.0-M1") { + library("Spring Authorization Server", "1.1.0-M2") { group("org.springframework.security") { modules = [ "spring-security-oauth2-authorization-server" 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 index d1616700f5..ce2d7b75f0 100644 --- 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 @@ -102,18 +102,14 @@ class SampleOAuth2AuthorizationServerApplicationTests { HttpHeaders headers = new HttpHeaders(); headers.setBasicAuth("messaging-client", "secret"); HttpEntity request = new HttpEntity<>(headers); - // @formatter:off 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(); - // @formatter:on - + .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();