From 50f0d04b064a09f94090c196ac868fa73685062e Mon Sep 17 00:00:00 2001 From: renechoi <115696395+renechoi@users.noreply.github.com> Date: Sun, 29 Jun 2025 12:22:32 +0900 Subject: [PATCH] feat(config): disable device_code grant by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: Device Authorization Grant endpoints were always enabled, exposing an attack surface that most deployments do not need. After: The grant is now opt-in. A new deviceGrantEnabled flag on AuthorizationServerSettings defaults to false; filters and providers are registered only when it is explicitly true. This change strengthens the framework’s secure-by-default posture and aligns with the discussion in gh-1709. Closes gh-1709 Signed-off-by: renechoi renechoi90@gmail.com Signed-off-by: renechoi <115696395+renechoi@users.noreply.github.com> --- .../OAuth2AuthorizationServerConfigurer.java | 10 ++++- .../OAuth2ClientAuthenticationConfigurer.java | 24 ++++++----- ...DeviceAuthorizationEndpointConfigurer.java | 13 ++++-- ...2DeviceVerificationEndpointConfigurer.java | 13 ++++-- .../settings/AuthorizationServerSettings.java | 38 +++++++++++++---- .../settings/ConfigurationSettingNames.java | 10 ++++- .../OAuth2DeviceCodeGrantTests.java | 41 ++++++++++++++----- 7 files changed, 111 insertions(+), 38 deletions(-) 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 094fc5813..798ae09d1 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 @@ -365,7 +365,10 @@ public void init(HttpSecurity httpSecurity) throws Exception { List requestMatchers = new ArrayList<>(); this.configurers.values().forEach((configurer) -> { configurer.init(httpSecurity); - requestMatchers.add(configurer.getRequestMatcher()); + RequestMatcher matcher = configurer.getRequestMatcher(); + if (matcher != null) { + requestMatchers.add(matcher); + } }); String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint()) @@ -380,7 +383,10 @@ public void init(HttpSecurity httpSecurity) throws Exception { preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class)); preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class)); preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class)); - preferredMatchers.add(getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class)); + RequestMatcher deviceAuthMatcher = getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class); + if (deviceAuthMatcher != null) { + preferredMatchers.add(deviceAuthMatcher); + } RequestMatcher preferredMatcher = getRequestMatcher( OAuth2PushedAuthorizationRequestEndpointConfigurer.class); if (preferredMatcher != null) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java index bf7f38d36..33603f16a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java @@ -192,19 +192,23 @@ void init(HttpSecurity httpSecurity) { ? OAuth2ConfigurerUtils .withMultipleIssuersPattern(authorizationServerSettings.getTokenRevocationEndpoint()) : authorizationServerSettings.getTokenRevocationEndpoint(); - String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() - ? OAuth2ConfigurerUtils - .withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()) - : authorizationServerSettings.getDeviceAuthorizationEndpoint(); String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils - .withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()) + .withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()) : authorizationServerSettings.getPushedAuthorizationRequestEndpoint(); - this.requestMatcher = new OrRequestMatcher(new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()), - new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name()), - new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name()), - new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()), - new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name())); + List requestMatchers = new ArrayList<>(); + requestMatchers.add(new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name())); + requestMatchers.add(new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name())); + requestMatchers.add(new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name())); + if (authorizationServerSettings.isDeviceGrantEnabled()) { + String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() + ? OAuth2ConfigurerUtils + .withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()) + : authorizationServerSettings.getDeviceAuthorizationEndpoint(); + requestMatchers.add(new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name())); + } + requestMatchers.add(new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name())); + this.requestMatcher = new OrRequestMatcher(requestMatchers); List authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); if (!this.authenticationProviders.isEmpty()) { authenticationProviders.addAll(0, this.authenticationProviders); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java index 40df4530e..610ac5840 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java @@ -197,10 +197,13 @@ public OAuth2DeviceAuthorizationEndpointConfigurer verificationUri(String verifi @Override public void init(HttpSecurity builder) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils - .getAuthorizationServerSettings(builder); + .getAuthorizationServerSettings(builder); + if (!authorizationServerSettings.isDeviceGrantEnabled()) { + return; + } String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils - .withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()) + .withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()) : authorizationServerSettings.getDeviceAuthorizationEndpoint(); this.requestMatcher = new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()); @@ -217,7 +220,11 @@ public void init(HttpSecurity builder) { public void configure(HttpSecurity builder) { AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils - .getAuthorizationServerSettings(builder); + .getAuthorizationServerSettings(builder); + + if (!authorizationServerSettings.isDeviceGrantEnabled()) { + return; + } String deviceAuthorizationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java index 3f8510c5e..c24a51e23 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java @@ -232,10 +232,13 @@ public OAuth2DeviceVerificationEndpointConfigurer consentPage(String consentPage @Override public void init(HttpSecurity builder) { AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils - .getAuthorizationServerSettings(builder); + .getAuthorizationServerSettings(builder); + if (!authorizationServerSettings.isDeviceGrantEnabled()) { + return; + } String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils - .withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint()) + .withMultipleIssuersPattern(authorizationServerSettings.getDeviceVerificationEndpoint()) : authorizationServerSettings.getDeviceVerificationEndpoint(); this.requestMatcher = new OrRequestMatcher( new AntPathRequestMatcher(deviceVerificationEndpointUri, HttpMethod.GET.name()), @@ -254,7 +257,11 @@ public void init(HttpSecurity builder) { public void configure(HttpSecurity builder) { AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils - .getAuthorizationServerSettings(builder); + .getAuthorizationServerSettings(builder); + + if (!authorizationServerSettings.isDeviceGrantEnabled()) { + return; + } String deviceVerificationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() ? OAuth2ConfigurerUtils diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java index 418554b58..9af655c3c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java @@ -163,21 +163,31 @@ public String getOidcLogoutEndpoint() { return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_LOGOUT_ENDPOINT); } + /** + * Returns {@code true} if the OAuth 2.0 Device Authorization Grant is enabled. + * The default is {@code false}. + * @return {@code true} if the Device Authorization Grant is enabled, {@code false} otherwise + */ + public boolean isDeviceGrantEnabled() { + return getSetting(ConfigurationSettingNames.AuthorizationServer.DEVICE_GRANT_ENABLED); + } + /** * Constructs a new {@link Builder} with the default settings. * @return the {@link Builder} */ public static Builder builder() { return new Builder().multipleIssuersAllowed(false) - .authorizationEndpoint("/oauth2/authorize") - .pushedAuthorizationRequestEndpoint("/oauth2/par") - .deviceAuthorizationEndpoint("/oauth2/device_authorization") - .deviceVerificationEndpoint("/oauth2/device_verification") - .tokenEndpoint("/oauth2/token") - .jwkSetEndpoint("/oauth2/jwks") - .tokenRevocationEndpoint("/oauth2/revoke") - .tokenIntrospectionEndpoint("/oauth2/introspect") - .oidcClientRegistrationEndpoint("/connect/register") + .authorizationEndpoint("/oauth2/authorize") + .pushedAuthorizationRequestEndpoint("/oauth2/par") + .deviceAuthorizationEndpoint("/oauth2/device_authorization") + .deviceVerificationEndpoint("/oauth2/device_verification") + .deviceGrantEnabled(false) + .tokenEndpoint("/oauth2/token") + .jwkSetEndpoint("/oauth2/jwks") + .tokenRevocationEndpoint("/oauth2/revoke") + .tokenIntrospectionEndpoint("/oauth2/introspect") + .oidcClientRegistrationEndpoint("/connect/register") .oidcUserInfoEndpoint("/userinfo") .oidcLogoutEndpoint("/connect/logout"); } @@ -281,6 +291,16 @@ public Builder deviceVerificationEndpoint(String deviceVerificationEndpoint) { deviceVerificationEndpoint); } + /** + * Enables the OAuth 2.0 Device Authorization Grant. + * @param deviceGrantEnabled {@code true} to enable the Device Authorization Grant + * @return the {@link Builder} for further configuration + */ + public Builder deviceGrantEnabled(boolean deviceGrantEnabled) { + return setting(ConfigurationSettingNames.AuthorizationServer.DEVICE_GRANT_ENABLED, + deviceGrantEnabled); + } + /** * Sets the OAuth 2.0 Token endpoint. * @param tokenEndpoint the Token endpoint diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java index 96edd2a54..f186b0bfd 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java @@ -129,7 +129,15 @@ public static final class AuthorizationServer { * Set the OAuth 2.0 Device Verification endpoint. */ public static final String DEVICE_VERIFICATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE - .concat("device-verification-endpoint"); + .concat("device-verification-endpoint"); + + /** + * Set to {@code true} if the OAuth 2.0 Device Authorization Grant is enabled. + * The default is {@code false}. + */ + public static final String DEVICE_GRANT_ENABLED = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE + .concat("device-grant-enabled"); + /** * Set the OAuth 2.0 Token endpoint. 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 338438f07..c2fdb110c 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 @@ -176,9 +176,17 @@ public static void destroy() { } @Test - public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception { + public void requestWhenDeviceAuthorizationEndpointDisabledThenNotFound() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)) + .andExpect(status().isNotFound()); + } + + @Test + public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); + // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE) @@ -200,7 +208,7 @@ public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorize @Test public void requestWhenRegisteredClientMissingThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -272,7 +280,7 @@ public void requestWhenDeviceAuthorizationRequestValidThenReturnDeviceAuthorizat @Test public void requestWhenDeviceVerificationRequestUnauthenticatedThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -357,7 +365,7 @@ public void requestWhenDeviceVerificationRequestValidThenDisplaysConsentPage() t @Test public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRequest() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -395,7 +403,7 @@ public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRe @Test public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSuccessPage() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -445,7 +453,7 @@ public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSucc @Test public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -481,7 +489,7 @@ public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throw @Test public void requestWhenAccessTokenRequestValidThenReturnAccessTokenResponse() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -553,7 +561,7 @@ public void requestWhenAccessTokenRequestValidThenReturnAccessTokenResponse() th @Test public void requestWhenAccessTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception { - this.spring.register(AuthorizationServerConfiguration.class).autowire(); + this.spring.register(AuthorizationServerConfigurationWithDeviceGrant.class).autowire(); // @formatter:off RegisteredClient registeredClient = TestRegisteredClients.registeredClient() @@ -683,11 +691,24 @@ PasswordEncoder passwordEncoder() { @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) - static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfiguration { + static class AuthorizationServerConfigurationWithDeviceGrant extends AuthorizationServerConfiguration { + + @Bean + AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().deviceGrantEnabled(true).build(); + } + + } + + @EnableWebSecurity + @Import(OAuth2AuthorizationServerConfiguration.class) + static class AuthorizationServerConfigurationWithMultipleIssuersAllowed extends AuthorizationServerConfigurationWithDeviceGrant { @Bean AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build(); + return AuthorizationServerSettings.builder().multipleIssuersAllowed(true) + .deviceGrantEnabled(true) + .build(); } }