From 0848c7dbf5bf28f60ad98fa5dbea2a91b202c328 Mon Sep 17 00:00:00 2001 From: Greg Li Date: Mon, 8 Jan 2024 14:23:49 +0800 Subject: [PATCH] gh-1454: Disable device authorization grant by default. Such as, deviceAuthorizationEndpoint and deviceVerificationEndpoint --- docs/src/test/java/sample/jpa/JpaTests.java | 9 ++- .../OAuth2AuthorizationServerConfigurer.java | 65 +++++++++++++++---- ...orizationServerMetadataEndpointFilter.java | 2 +- .../OAuth2DeviceCodeGrantTests.java | 59 ++++++++++++++--- 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/docs/src/test/java/sample/jpa/JpaTests.java b/docs/src/test/java/sample/jpa/JpaTests.java index 8720383dd..cbad56d2b 100644 --- a/docs/src/test/java/sample/jpa/JpaTests.java +++ b/docs/src/test/java/sample/jpa/JpaTests.java @@ -69,6 +69,7 @@ * Tests for the guide How-to: Implement core services with JPA. * * @author Steve Riesenberg + * @author Greg Li */ @ExtendWith(SpringTestContextExtension.class) public class JpaTests { @@ -88,7 +89,7 @@ public class JpaTests { @Test public void oidcLoginWhenJpaCoreServicesAutowiredThenUsed() throws Exception { - this.spring.register(AuthorizationServerConfig.class).autowire(); + this.spring.register(AuthorizationServerConfigDeviceAuthorize.class).autowire(); assertThat(this.registeredClientRepository).isInstanceOf(JpaRegisteredClientRepository.class); assertThat(this.authorizationService).isInstanceOf(JpaOAuth2AuthorizationService.class); assertThat(this.authorizationConsentService).isInstanceOf(JpaOAuth2AuthorizationConsentService.class); @@ -135,7 +136,7 @@ public void oidcLoginWhenJpaCoreServicesAutowiredThenUsed() throws Exception { @Test public void deviceAuthorizationWhenJpaCoreServicesAutowiredThenSuccess() throws Exception { - this.spring.register(AuthorizationServerConfig.class).autowire(); + this.spring.register(AuthorizationServerConfigDeviceAuthorize.class).autowire(); assertThat(this.registeredClientRepository).isInstanceOf(JpaRegisteredClientRepository.class); assertThat(this.authorizationService).isInstanceOf(JpaOAuth2AuthorizationService.class); assertThat(this.authorizationConsentService).isInstanceOf(JpaOAuth2AuthorizationConsentService.class); @@ -191,13 +192,15 @@ private OAuth2Authorization findAuthorization(String token, String tokenType) { @EnableWebSecurity @EnableAutoConfiguration @ComponentScan - static class AuthorizationServerConfig { + static class AuthorizationServerConfigDeviceAuthorize { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .deviceAuthorizationEndpoint(Customizer.withDefaults()) + .deviceVerificationEndpoint(Customizer.withDefaults()) .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 // @formatter:off diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java index 1b04ac158..64596f6b2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java @@ -45,6 +45,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter; @@ -55,6 +57,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; /** * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support. @@ -64,6 +67,7 @@ * @author Gerardo Roza * @author Ovidiu Popa * @author Gaurav Tiwari + * @author Greg Li * @since 0.0.1 * @see AbstractHttpConfigurer * @see OAuth2ClientAuthenticationConfigurer @@ -218,26 +222,40 @@ public OAuth2AuthorizationServerConfigurer tokenRevocationEndpoint(Customizer deviceAuthorizationEndpointCustomizer) { - deviceAuthorizationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class)); + OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = + getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class); + if (deviceAuthorizationEndpointConfigurer == null) { + addConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class, + new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess)); + deviceAuthorizationEndpointConfigurer = getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class); + } + deviceAuthorizationEndpointCustomizer.customize(deviceAuthorizationEndpointConfigurer); return this; } /** - * Configures the OAuth 2.0 Device Verification Endpoint. + * Configures the OAuth 2.0 Device Verification Endpoint (disabled by default). * * @param deviceVerificationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2DeviceVerificationEndpointConfigurer} * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration * @since 1.1 */ public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(Customizer deviceVerificationEndpointCustomizer) { - deviceVerificationEndpointCustomizer.customize(getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class)); + OAuth2DeviceVerificationEndpointConfigurer deviceVerificationEndpointConfigurer = + getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class); + if (deviceVerificationEndpointConfigurer == null) { + addConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class, + new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess)); + deviceVerificationEndpointConfigurer = getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class); + } + deviceVerificationEndpointCustomizer.customize(deviceVerificationEndpointConfigurer); return this; } @@ -319,19 +337,45 @@ public void init(HttpSecurity httpSecurity) { ExceptionHandlingConfigurer exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling != null) { + OrRequestMatcher preferredRequestMatcher = null; + if (getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class) != null) { + preferredRequestMatcher = new OrRequestMatcher( + getRequestMatcher(OAuth2TokenEndpointConfigurer.class), + getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class), + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class), + getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class)); + } else { + preferredRequestMatcher = new OrRequestMatcher( + getRequestMatcher(OAuth2TokenEndpointConfigurer.class), + getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class), + getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class)); + } exceptionHandling.defaultAuthenticationEntryPointFor( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new OrRequestMatcher( - getRequestMatcher(OAuth2TokenEndpointConfigurer.class), - getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class), - getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class), - getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class)) + preferredRequestMatcher ); } } @Override public void configure(HttpSecurity httpSecurity) { + OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = + getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class); + if (deviceAuthorizationEndpointConfigurer != null) { + OAuth2AuthorizationServerMetadataEndpointConfigurer auth2AuthorizationServerMetadataEndpointConfigurer = + getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class); + + auth2AuthorizationServerMetadataEndpointConfigurer + .addDefaultAuthorizationServerMetadataCustomizer((builder) -> { + AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext(); + String issuer = authorizationServerContext.getIssuer(); + AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings(); + String deviceAuthorizationEndpoint = UriComponentsBuilder.fromUriString(issuer) + .path(authorizationServerSettings.getDeviceAuthorizationEndpoint()).build().toUriString(); + + builder.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint); + }); + } this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity)); AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity); @@ -359,8 +403,7 @@ private Map, AbstractOAuth2Configurer> configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess)); configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess)); configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess)); - configurers.put(OAuth2DeviceAuthorizationEndpointConfigurer.class, new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess)); - configurers.put(OAuth2DeviceVerificationEndpointConfigurer.class, new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess)); + //configurers.put(OAuth2DeviceVerificationEndpointConfigurer.class, new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess)); return configurers; } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index c561260c0..39da328af 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -46,6 +46,7 @@ * * @author Daniel Garnier-Moiroux * @author Joe Grandja + * @author Greg Li * @since 0.1.1 * @see OAuth2AuthorizationServerMetadata * @see AuthorizationServerSettings @@ -92,7 +93,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() .issuer(issuer) .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) - .deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) .jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint())) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java index f8b039833..ccec19d29 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java @@ -33,6 +33,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -45,6 +47,8 @@ import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -72,6 +76,7 @@ import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.test.SpringTestContext; import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.LinkedMultiValueMap; @@ -90,6 +95,7 @@ * Integration tests for OAuth 2.0 Device Grant. * * @author Steve Riesenberg + * @author Greg Li */ @ExtendWith(SpringTestContextExtension.class) public class OAuth2DeviceCodeGrantTests { @@ -158,7 +164,7 @@ public static void destroy() { @Test public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -179,9 +185,32 @@ public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorize // @formatter:on } + @Test + public void requestWhenDeviceAuthorizationRequestDisabledThenUnauthorized() throws Exception { + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); + + // @formatter:off + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE) + .build(); + // @formatter:on + this.registeredClientRepository.save(registeredClient); + + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()); + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " ")); + + // @formatter:off + this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI) + .params(parameters)) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + @Test public void requestWhenRegisteredClientMissingThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -204,7 +233,7 @@ public void requestWhenRegisteredClientMissingThenUnauthorized() throws Exceptio @Test public void requestWhenDeviceAuthorizationRequestValidThenReturnDeviceAuthorizationResponse() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -252,7 +281,7 @@ public void requestWhenDeviceAuthorizationRequestValidThenReturnDeviceAuthorizat @Test public void requestWhenDeviceVerificationRequestUnauthenticatedThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -286,7 +315,7 @@ public void requestWhenDeviceVerificationRequestUnauthenticatedThenUnauthorized( @Test public void requestWhenDeviceVerificationRequestValidThenDisplaysConsentPage() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -335,7 +364,7 @@ public void requestWhenDeviceVerificationRequestValidThenDisplaysConsentPage() t @Test public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRequest() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -373,7 +402,7 @@ public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRe @Test public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSuccessPage() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -423,7 +452,7 @@ public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSucc @Test public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -459,7 +488,7 @@ public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throw @Test public void requestWhenAccessTokenRequestValidThenReturnAccessTokenResponse() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationDeviceAuthorize.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -545,7 +574,17 @@ private static Function, Boolea @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfiguration { + static class AuthorizationServerConfigurationDeviceAuthorize { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .deviceAuthorizationEndpoint(Customizer.withDefaults()) // Enable deviceAuthorizationEndpoint + .deviceVerificationEndpoint(Customizer.withDefaults()); // Enable deviceVerificationEndpoint + return http.build(); + } @Bean RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {