Skip to content

Commit 51fe7ff

Browse files
committed
Return device_code grant metadata when enabled
Issue gh-17998
1 parent 9595d37 commit 51fe7ff

File tree

9 files changed

+119
-8
lines changed

9 files changed

+119
-8
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.security.core.Authentication;
3939
import org.springframework.security.core.session.SessionRegistry;
4040
import org.springframework.security.core.session.SessionRegistryImpl;
41+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
4142
import org.springframework.security.oauth2.core.OAuth2Error;
4243
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
4344
import org.springframework.security.oauth2.core.OAuth2Token;
@@ -459,6 +460,28 @@ public void configure(HttpSecurity httpSecurity) {
459460
});
460461
}
461462

463+
OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = getConfigurer(
464+
OAuth2DeviceAuthorizationEndpointConfigurer.class);
465+
if (deviceAuthorizationEndpointConfigurer != null) {
466+
OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
467+
OAuth2AuthorizationServerMetadataEndpointConfigurer.class);
468+
469+
authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
470+
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
471+
String issuer = authorizationServerContext.getIssuer();
472+
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
473+
.getAuthorizationServerSettings();
474+
475+
String deviceAuthorizationEndpoint = UriComponentsBuilder.fromUriString(issuer)
476+
.path(authorizationServerSettings.getDeviceAuthorizationEndpoint())
477+
.build()
478+
.toUriString();
479+
480+
builder.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint);
481+
builder.grantType(AuthorizationGrantType.DEVICE_CODE.getValue());
482+
});
483+
}
484+
462485
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
463486

464487
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
@@ -501,7 +524,7 @@ private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer>
501524
}
502525

503526
@SuppressWarnings("unchecked")
504-
private <T> T getConfigurer(Class<T> type) {
527+
<T> T getConfigurer(Class<T> type) {
505528
return (T) this.configurers.get(type);
506529
}
507530

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcConfigurer.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.security.config.Customizer;
2525
import org.springframework.security.config.ObjectPostProcessor;
2626
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
27+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2728
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
2829
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
2930
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
@@ -147,6 +148,29 @@ void configure(HttpSecurity httpSecurity) {
147148
});
148149
}
149150

151+
OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = httpSecurity
152+
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
153+
.getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class);
154+
if (deviceAuthorizationEndpointConfigurer != null) {
155+
OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer = getConfigurer(
156+
OidcProviderConfigurationEndpointConfigurer.class);
157+
158+
providerConfigurationEndpointConfigurer.addDefaultProviderConfigurationCustomizer((builder) -> {
159+
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
160+
String issuer = authorizationServerContext.getIssuer();
161+
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
162+
.getAuthorizationServerSettings();
163+
164+
String deviceAuthorizationEndpoint = UriComponentsBuilder.fromUriString(issuer)
165+
.path(authorizationServerSettings.getDeviceAuthorizationEndpoint())
166+
.build()
167+
.toUriString();
168+
169+
builder.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint);
170+
builder.grantType(AuthorizationGrantType.DEVICE_CODE.getValue());
171+
});
172+
}
173+
150174
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
151175
}
152176

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
4343
import org.springframework.security.config.test.SpringTestContext;
4444
import org.springframework.security.config.test.SpringTestContextExtension;
45+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
4546
import org.springframework.security.oauth2.jose.TestJwks;
4647
import org.springframework.security.oauth2.jwt.JwtDecoder;
4748
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
@@ -172,6 +173,18 @@ public void requestWhenAuthorizationServerMetadataRequestAndClientRegistrationEn
172173
.value(ISSUER.concat(this.authorizationServerSettings.getClientRegistrationEndpoint())));
173174
}
174175

176+
@Test
177+
public void requestWhenAuthorizationServerMetadataRequestAndDeviceCodeGrantEnabledThenMetadataResponseIncludesDeviceAuthorizationEndpoint()
178+
throws Exception {
179+
this.spring.register(AuthorizationServerConfigurationWithDeviceCodeGrantEnabled.class).autowire();
180+
181+
this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
182+
.andExpect(status().is2xxSuccessful())
183+
.andExpect(jsonPath("$.device_authorization_endpoint")
184+
.value(ISSUER.concat(this.authorizationServerSettings.getDeviceAuthorizationEndpoint())))
185+
.andExpect(jsonPath("$.grant_types_supported[4]").value(AuthorizationGrantType.DEVICE_CODE.getValue()));
186+
}
187+
175188
@EnableWebSecurity
176189
@Import(OAuth2AuthorizationServerConfiguration.class)
177190
static class AuthorizationServerConfiguration {
@@ -267,4 +280,25 @@ SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) th
267280

268281
}
269282

283+
@EnableWebSecurity
284+
@Configuration(proxyBeanMethods = false)
285+
static class AuthorizationServerConfigurationWithDeviceCodeGrantEnabled extends AuthorizationServerConfiguration {
286+
287+
// @formatter:off
288+
@Bean
289+
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
290+
http
291+
.oauth2AuthorizationServer((authorizationServer) ->
292+
authorizationServer
293+
.deviceAuthorizationEndpoint(Customizer.withDefaults())
294+
)
295+
.authorizeHttpRequests((authorize) ->
296+
authorize.anyRequest().authenticated()
297+
);
298+
return http.build();
299+
}
300+
// @formatter:on
301+
302+
}
303+
270304
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2DeviceCodeGrantTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,6 @@ SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) th
657657
.oauth2AuthorizationServer((authorizationServer) ->
658658
authorizationServer
659659
.deviceAuthorizationEndpoint(Customizer.withDefaults())
660-
.deviceVerificationEndpoint(Customizer.withDefaults())
661660
)
662661
.authorizeHttpRequests((authorize) ->
663662
authorize.anyRequest().authenticated()

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcProviderConfigurationTests.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfi
146146
.value(ISSUER.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint())));
147147
}
148148

149+
@Test
150+
public void requestWhenConfigurationRequestAndDeviceCodeGrantEnabledThenConfigurationResponseIncludesDeviceAuthorizationEndpoint()
151+
throws Exception {
152+
this.spring.register(AuthorizationServerConfigurationWithDeviceCodeGrantEnabled.class).autowire();
153+
154+
this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
155+
.andExpect(status().is2xxSuccessful())
156+
.andExpectAll(defaultConfigurationMatchers(ISSUER))
157+
.andExpect(jsonPath("$.device_authorization_endpoint")
158+
.value(ISSUER.concat(this.authorizationServerSettings.getDeviceAuthorizationEndpoint())))
159+
.andExpect(jsonPath("$.grant_types_supported[4]").value(AuthorizationGrantType.DEVICE_CODE.getValue()));
160+
}
161+
149162
private ResultMatcher[] defaultConfigurationMatchers(String issuer) {
150163
// @formatter:off
151164
return new ResultMatcher[] {
@@ -163,6 +176,7 @@ private ResultMatcher[] defaultConfigurationMatchers(String issuer) {
163176
jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
164177
jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),
165178
jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()),
179+
jsonPath("$.grant_types_supported[3]").value(AuthorizationGrantType.TOKEN_EXCHANGE.getValue()),
166180
jsonPath("revocation_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())),
167181
jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
168182
jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
@@ -324,6 +338,25 @@ SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) th
324338

325339
}
326340

341+
@EnableWebSecurity
342+
@Configuration(proxyBeanMethods = false)
343+
static class AuthorizationServerConfigurationWithDeviceCodeGrantEnabled extends AuthorizationServerConfiguration {
344+
345+
// @formatter:off
346+
@Bean
347+
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
348+
http
349+
.oauth2AuthorizationServer((authorizationServer) ->
350+
authorizationServer
351+
.deviceAuthorizationEndpoint(Customizer.withDefaults())
352+
.oidc(Customizer.withDefaults())
353+
);
354+
return http.build();
355+
}
356+
// @formatter:on
357+
358+
}
359+
327360
@EnableWebSecurity
328361
@Configuration(proxyBeanMethods = false)
329362
static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration {

oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
103103
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
104104
.pushedAuthorizationRequestEndpoint(
105105
asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint()))
106-
.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
107106
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
108107
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
109108
.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
@@ -113,7 +112,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
113112
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
114113
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
115114
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
116-
.grantType(AuthorizationGrantType.DEVICE_CODE.getValue())
117115
.grantType(AuthorizationGrantType.TOKEN_EXCHANGE.getValue())
118116
.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
119117
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())

oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
103103
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
104104
.pushedAuthorizationRequestEndpoint(
105105
asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint()))
106-
.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
107106
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
108107
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
109108
.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
110109
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
111110
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
112111
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
113112
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
114-
.grantType(AuthorizationGrantType.DEVICE_CODE.getValue())
115113
.grantType(AuthorizationGrantType.TOKEN_EXCHANGE.getValue())
116114
.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
117115
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())

oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,14 @@ public void doFilterWhenConfigurationRequestThenConfigurationResponse() throws E
136136
.contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\"");
137137
assertThat(providerConfigurationResponse)
138138
.contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\"");
139+
assertThat(providerConfigurationResponse).doesNotContain("\"device_authorization_endpoint\"");
139140
assertThat(providerConfigurationResponse)
140141
.contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\"");
141142
assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\"");
142143
assertThat(providerConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
143144
assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
144145
assertThat(providerConfigurationResponse).contains(
145-
"\"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\"]");
146+
"\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]");
146147
assertThat(providerConfigurationResponse)
147148
.contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\"");
148149
assertThat(providerConfigurationResponse).contains(

oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,15 @@ public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse()
132132
.contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\"");
133133
assertThat(authorizationServerMetadataResponse)
134134
.contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\"");
135+
assertThat(authorizationServerMetadataResponse).doesNotContain("\"device_authorization_endpoint\"");
135136
assertThat(authorizationServerMetadataResponse)
136137
.contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\"");
137138
assertThat(authorizationServerMetadataResponse).contains(
138139
"\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\",\"tls_client_auth\",\"self_signed_tls_client_auth\"]");
139140
assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\"");
140141
assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]");
141142
assertThat(authorizationServerMetadataResponse).contains(
142-
"\"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\"]");
143+
"\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"urn:ietf:params:oauth:grant-type:token-exchange\"]");
143144
assertThat(authorizationServerMetadataResponse)
144145
.contains("\"revocation_endpoint\":\"https://example.com/oauth2/v1/revoke\"");
145146
assertThat(authorizationServerMetadataResponse).contains(

0 commit comments

Comments
 (0)