diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 2a55f12862..8a027fe3e2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -38,6 +38,7 @@ import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2Token; @@ -459,6 +460,28 @@ public final class OAuth2AuthorizationServerConfigurer }); } + OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = getConfigurer( + OAuth2DeviceAuthorizationEndpointConfigurer.class); + if (deviceAuthorizationEndpointConfigurer != null) { + OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer( + OAuth2AuthorizationServerMetadataEndpointConfigurer.class); + + authorizationServerMetadataEndpointConfigurer.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); + builder.grantType(AuthorizationGrantType.DEVICE_CODE.getValue()); + }); + } + this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity)); AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils @@ -501,7 +524,7 @@ public final class OAuth2AuthorizationServerConfigurer } @SuppressWarnings("unchecked") - private T getConfigurer(Class type) { + T getConfigurer(Class type) { return (T) this.configurers.get(type); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java index 87108a95f2..d1d909d31b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java @@ -24,6 +24,7 @@ import java.util.Map; import org.springframework.security.config.Customizer; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; 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; @@ -147,6 +148,29 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { }); } + OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = httpSecurity + .getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class); + if (deviceAuthorizationEndpointConfigurer != null) { + OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer = getConfigurer( + OidcProviderConfigurationEndpointConfigurer.class); + + providerConfigurationEndpointConfigurer.addDefaultProviderConfigurationCustomizer((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); + builder.grantType(AuthorizationGrantType.DEVICE_CODE.getValue()); + }); + } + this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity)); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java index dcd15b69f3..00f50ca807 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java @@ -42,6 +42,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; @@ -172,6 +173,18 @@ public class OAuth2AuthorizationServerMetadataTests { .value(ISSUER.concat(this.authorizationServerSettings.getClientRegistrationEndpoint()))); } + @Test + public void requestWhenAuthorizationServerMetadataRequestAndDeviceCodeGrantEnabledThenMetadataResponseIncludesDeviceAuthorizationEndpoint() + throws Exception { + this.spring.register(AuthorizationServerConfigurationWithDeviceCodeGrantEnabled.class).autowire(); + + this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.device_authorization_endpoint") + .value(ISSUER.concat(this.authorizationServerSettings.getDeviceAuthorizationEndpoint()))) + .andExpect(jsonPath("$.grant_types_supported[4]").value(AuthorizationGrantType.DEVICE_CODE.getValue())); + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfiguration { @@ -267,4 +280,25 @@ public class OAuth2AuthorizationServerMetadataTests { } + @EnableWebSecurity + @Configuration(proxyBeanMethods = false) + static class AuthorizationServerConfigurationWithDeviceCodeGrantEnabled extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + http + .oauth2AuthorizationServer((authorizationServer) -> + authorizationServer + .deviceAuthorizationEndpoint(Customizer.withDefaults()) + ) + .authorizeHttpRequests((authorize) -> + authorize.anyRequest().authenticated() + ); + return http.build(); + } + // @formatter:on + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2DeviceCodeGrantTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2DeviceCodeGrantTests.java index e3fb73f2ea..a2938fddc8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2DeviceCodeGrantTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2DeviceCodeGrantTests.java @@ -657,7 +657,6 @@ public class OAuth2DeviceCodeGrantTests { .oauth2AuthorizationServer((authorizationServer) -> authorizationServer .deviceAuthorizationEndpoint(Customizer.withDefaults()) - .deviceVerificationEndpoint(Customizer.withDefaults()) ) .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated() diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcProviderConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcProviderConfigurationTests.java index 815bc0534c..69f7182cac 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcProviderConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcProviderConfigurationTests.java @@ -146,6 +146,19 @@ public class OidcProviderConfigurationTests { .value(ISSUER.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint()))); } + @Test + public void requestWhenConfigurationRequestAndDeviceCodeGrantEnabledThenConfigurationResponseIncludesDeviceAuthorizationEndpoint() + throws Exception { + this.spring.register(AuthorizationServerConfigurationWithDeviceCodeGrantEnabled.class).autowire(); + + this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))) + .andExpect(status().is2xxSuccessful()) + .andExpectAll(defaultConfigurationMatchers(ISSUER)) + .andExpect(jsonPath("$.device_authorization_endpoint") + .value(ISSUER.concat(this.authorizationServerSettings.getDeviceAuthorizationEndpoint()))) + .andExpect(jsonPath("$.grant_types_supported[4]").value(AuthorizationGrantType.DEVICE_CODE.getValue())); + } + private ResultMatcher[] defaultConfigurationMatchers(String issuer) { // @formatter:off return new ResultMatcher[] { @@ -163,6 +176,7 @@ public class OidcProviderConfigurationTests { jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()), jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()), + jsonPath("$.grant_types_supported[3]").value(AuthorizationGrantType.TOKEN_EXCHANGE.getValue()), jsonPath("revocation_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())), jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), @@ -324,6 +338,25 @@ public class OidcProviderConfigurationTests { } + @EnableWebSecurity + @Configuration(proxyBeanMethods = false) + static class AuthorizationServerConfigurationWithDeviceCodeGrantEnabled extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + http + .oauth2AuthorizationServer((authorizationServer) -> + authorizationServer + .deviceAuthorizationEndpoint(Customizer.withDefaults()) + .oidc(Customizer.withDefaults()) + ); + return http.build(); + } + // @formatter:on + + } + @EnableWebSecurity @Configuration(proxyBeanMethods = false) static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration { diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index c2f39bb00f..eebbfccad4 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -103,7 +103,6 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) .pushedAuthorizationRequestEndpoint( asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint())) - .deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) .jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint())) @@ -113,7 +112,6 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue()) - .grantType(AuthorizationGrantType.DEVICE_CODE.getValue()) .grantType(AuthorizationGrantType.TOKEN_EXCHANGE.getValue()) .tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint())) .tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods()) diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index 924f6ef9ae..d5f061cd0f 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -103,7 +103,6 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) .pushedAuthorizationRequestEndpoint( asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint())) - .deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) .jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint())) @@ -111,7 +110,6 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue()) - .grantType(AuthorizationGrantType.DEVICE_CODE.getValue()) .grantType(AuthorizationGrantType.TOKEN_EXCHANGE.getValue()) .tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint())) .tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods()) diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java index 54831f86b5..04d55e6baf 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java @@ -136,13 +136,14 @@ public class OidcProviderConfigurationEndpointFilterTests { .contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\""); assertThat(providerConfigurationResponse) .contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\""); + assertThat(providerConfigurationResponse).doesNotContain("\"device_authorization_endpoint\""); assertThat(providerConfigurationResponse) .contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\""); assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\""); assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]"); assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); assertThat(providerConfigurationResponse).contains( - "\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]"); + "\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]"); assertThat(providerConfigurationResponse) .contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\""); assertThat(providerConfigurationResponse).contains( diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java index dbab3710f7..3abbe2b8e9 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java @@ -132,6 +132,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { .contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\""); assertThat(authorizationServerMetadataResponse) .contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\""); + assertThat(authorizationServerMetadataResponse).doesNotContain("\"device_authorization_endpoint\""); assertThat(authorizationServerMetadataResponse) .contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\""); assertThat(authorizationServerMetadataResponse).contains( @@ -139,7 +140,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\""); assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]"); assertThat(authorizationServerMetadataResponse).contains( - "\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:device_code\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]"); + "\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]"); assertThat(authorizationServerMetadataResponse) .contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\""); assertThat(authorizationServerMetadataResponse).contains(