|
@@ -0,0 +1,584 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright 2020-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.security.oauth2.server.authorization.config.annotation.web.configurers;
|
|
|
|
+
|
|
|
|
+import java.security.Principal;
|
|
|
|
+import java.time.Instant;
|
|
|
|
+import java.util.Map;
|
|
|
|
+import java.util.function.Consumer;
|
|
|
|
+import java.util.function.Function;
|
|
|
|
+
|
|
|
|
+import com.nimbusds.jose.jwk.JWKSet;
|
|
|
|
+import com.nimbusds.jose.jwk.source.JWKSource;
|
|
|
|
+import com.nimbusds.jose.proc.SecurityContext;
|
|
|
|
+import org.junit.jupiter.api.AfterAll;
|
|
|
|
+import org.junit.jupiter.api.AfterEach;
|
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
|
+
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
+import org.springframework.context.annotation.Bean;
|
|
|
|
+import org.springframework.context.annotation.Import;
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
+import org.springframework.http.converter.HttpMessageConverter;
|
|
|
|
+import org.springframework.jdbc.core.JdbcOperations;
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
|
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
|
|
|
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
|
|
|
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
|
|
|
+import org.springframework.mock.http.client.MockClientHttpResponse;
|
|
|
|
+import org.springframework.mock.web.MockHttpServletResponse;
|
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
|
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
|
|
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
|
|
|
+import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
|
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
|
|
+import org.springframework.security.oauth2.core.OAuth2DeviceCode;
|
|
|
|
+import org.springframework.security.oauth2.core.OAuth2Token;
|
|
|
|
+import org.springframework.security.oauth2.core.OAuth2UserCode;
|
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
|
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
|
|
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
|
|
|
+import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
|
|
|
|
+import org.springframework.security.oauth2.jose.TestJwks;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
|
|
|
+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.client.TestRegisteredClients;
|
|
|
|
+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.test.web.servlet.MockMvc;
|
|
|
|
+import org.springframework.test.web.servlet.MvcResult;
|
|
|
|
+import org.springframework.util.LinkedMultiValueMap;
|
|
|
|
+import org.springframework.util.MultiValueMap;
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
+
|
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Integration tests for OAuth 2.0 Device Grant.
|
|
|
|
+ *
|
|
|
|
+ * @author Steve Riesenberg
|
|
|
|
+ */
|
|
|
|
+@ExtendWith(SpringTestContextExtension.class)
|
|
|
|
+public class OAuth2DeviceCodeGrantTests {
|
|
|
|
+ private static final String DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI = "/oauth2/device_authorization";
|
|
|
|
+ private static final String DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI = "/oauth2/device_verification";
|
|
|
|
+ private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
|
|
|
|
+ private static final OAuth2TokenType DEVICE_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.DEVICE_CODE);
|
|
|
|
+ private static final String USER_CODE = "ABCD-EFGH";
|
|
|
|
+ private static final String STATE = "123";
|
|
|
|
+ private static final String DEVICE_CODE = "abc-XYZ";
|
|
|
|
+
|
|
|
|
+ private static EmbeddedDatabase db;
|
|
|
|
+
|
|
|
|
+ private static JWKSource<SecurityContext> jwkSource;
|
|
|
|
+
|
|
|
|
+ private static final HttpMessageConverter<OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseHttpMessageConverter =
|
|
|
|
+ new OAuth2DeviceAuthorizationResponseHttpMessageConverter();
|
|
|
|
+
|
|
|
|
+ private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseHttpMessageConverter =
|
|
|
|
+ new OAuth2AccessTokenResponseHttpMessageConverter();
|
|
|
|
+
|
|
|
|
+ public final SpringTestContext spring = new SpringTestContext();
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private MockMvc mvc;
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private JdbcOperations jdbcOperations;
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private RegisteredClientRepository registeredClientRepository;
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private OAuth2AuthorizationService authorizationService;
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private OAuth2AuthorizationConsentService authorizationConsentService;
|
|
|
|
+
|
|
|
|
+ @BeforeAll
|
|
|
|
+ public static void init() {
|
|
|
|
+ JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
|
|
|
+ jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ db = new EmbeddedDatabaseBuilder()
|
|
|
|
+ .generateUniqueName(true)
|
|
|
|
+ .setType(EmbeddedDatabaseType.HSQL)
|
|
|
|
+ .setScriptEncoding("UTF-8")
|
|
|
|
+ .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
|
|
|
|
+ .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
|
|
|
|
+ .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @AfterEach
|
|
|
|
+ public void tearDown() {
|
|
|
|
+ jdbcOperations.update("truncate table oauth2_authorization");
|
|
|
|
+ jdbcOperations.update("truncate table oauth2_authorization_consent");
|
|
|
|
+ jdbcOperations.update("truncate table oauth2_registered_client");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @AfterAll
|
|
|
|
+ public static void destroy() {
|
|
|
|
+ db.shutdown();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> 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();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> 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)
|
|
|
|
+ .headers(withClientAuth(registeredClient)))
|
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceAuthorizationRequestValidThenReturnDeviceAuthorizationResponse() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.SCOPE,
|
|
|
|
+ StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ MvcResult mvcResult = this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)
|
|
|
|
+ .params(parameters)
|
|
|
|
+ .headers(withClientAuth(registeredClient)))
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
+ .andExpect(jsonPath("$.device_code").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.user_code").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.expires_in").isNumber())
|
|
|
|
+ .andExpect(jsonPath("$.verification_uri").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.verification_uri_complete").isNotEmpty())
|
|
|
|
+ .andReturn();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ MockHttpServletResponse servletResponse = mvcResult.getResponse();
|
|
|
|
+ MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
|
|
|
|
+ HttpStatus.OK);
|
|
|
|
+ OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse =
|
|
|
|
+ deviceAuthorizationResponseHttpMessageConverter.read(OAuth2DeviceAuthorizationResponse.class,
|
|
|
|
+ httpResponse);
|
|
|
|
+ String userCode = deviceAuthorizationResponse.getUserCode().getTokenValue();
|
|
|
|
+ assertThat(userCode).matches("[A-Z]{4}-[A-Z]{4}");
|
|
|
|
+ assertThat(deviceAuthorizationResponse.getVerificationUri())
|
|
|
|
+ .isEqualTo("http://localhost/oauth2/device_verification");
|
|
|
|
+ assertThat(deviceAuthorizationResponse.getVerificationUriComplete())
|
|
|
|
+ .isEqualTo("http://localhost/oauth2/device_verification?user_code=" + userCode);
|
|
|
|
+
|
|
|
|
+ String deviceCode = deviceAuthorizationResponse.getDeviceCode().getTokenValue();
|
|
|
|
+ OAuth2Authorization authorization = this.authorizationService.findByToken(deviceCode, DEVICE_CODE_TOKEN_TYPE);
|
|
|
|
+ assertThat(authorization.getToken(OAuth2DeviceCode.class)).isNotNull();
|
|
|
|
+ assertThat(authorization.getToken(OAuth2UserCode.class)).isNotNull();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceVerificationRequestUnauthenticatedThenUnauthorized() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName(registeredClient.getClientId())
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
|
|
|
|
+ .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ this.mvc.perform(get(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
|
|
|
|
+ .params(parameters))
|
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceVerificationRequestValidThenDisplaysConsentPage() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName(registeredClient.getClientId())
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
|
|
|
|
+ .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ MvcResult mvcResult = this.mvc.perform(get(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
|
|
|
|
+ .params(parameters)
|
|
|
|
+ .with(user("user")))
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
+ .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
|
|
|
|
+ .andReturn();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ String responseHtml = mvcResult.getResponse().getContentAsString();
|
|
|
|
+ assertThat(responseHtml).contains("Consent required");
|
|
|
|
+
|
|
|
|
+ OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
|
|
|
|
+ assertThat(updatedAuthorization.getPrincipalName()).isEqualTo("user");
|
|
|
|
+ assertThat(updatedAuthorization).isNotNull();
|
|
|
|
+ // @formatter:off
|
|
|
|
+ assertThat(updatedAuthorization.getToken(OAuth2UserCode.class))
|
|
|
|
+ .extracting(isInvalidated())
|
|
|
|
+ .isEqualTo(false);
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceAuthorizationConsentRequestUnauthenticatedThenBadRequest() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName("user")
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
|
|
|
|
+ .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
|
|
|
|
+ .attribute(OAuth2ParameterNames.STATE, STATE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
|
|
|
|
+ parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.SCOPE, registeredClient.getScopes().iterator().next());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.STATE, STATE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ this.mvc.perform(post(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
|
|
|
|
+ .params(parameters))
|
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenDeviceAuthorizationConsentRequestValidThenRedirectsToSuccessPage() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName("user")
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt))
|
|
|
|
+ .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
|
|
|
|
+ .attribute(OAuth2ParameterNames.STATE, STATE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
|
|
|
|
+ parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.SCOPE, registeredClient.getScopes().iterator().next());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.STATE, STATE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ MvcResult mvcResult = this.mvc.perform(post(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
|
|
|
|
+ .params(parameters)
|
|
|
|
+ .with(user("user")))
|
|
|
|
+ .andExpect(status().is3xxRedirection())
|
|
|
|
+ .andReturn();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ assertThat(mvcResult.getResponse().getHeader(HttpHeaders.LOCATION)).isEqualTo("/?success");
|
|
|
|
+
|
|
|
|
+ OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
|
|
|
|
+ assertThat(updatedAuthorization).isNotNull();
|
|
|
|
+ // @formatter:off
|
|
|
|
+ assertThat(updatedAuthorization.getToken(OAuth2UserCode.class))
|
|
|
|
+ .extracting(isInvalidated())
|
|
|
|
+ .isEqualTo(true);
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenAccessTokenRequestUnauthenticatedThenUnauthorized() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName(registeredClient.getClientId())
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
|
|
|
|
+ .authorizedScopes(registeredClient.getScopes())
|
|
|
|
+ .attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
|
|
|
+ .params(parameters))
|
|
|
|
+ .andExpect(status().isUnauthorized());
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenAccessTokenRequestValidThenReturnAccessTokenResponse() throws Exception {
|
|
|
|
+ this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.registeredClientRepository.save(registeredClient);
|
|
|
|
+
|
|
|
|
+ Instant issuedAt = Instant.now();
|
|
|
|
+ Instant expiresAt = issuedAt.plusSeconds(300);
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
|
|
|
+ .principalName(registeredClient.getClientId())
|
|
|
|
+ .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
|
|
|
+ .token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
|
|
|
+ .token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
|
|
|
|
+ .authorizedScopes(registeredClient.getScopes())
|
|
|
|
+ .attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationService.save(authorization);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OAuth2AuthorizationConsent authorizationConsent =
|
|
|
|
+ OAuth2AuthorizationConsent.withId(registeredClient.getClientId(), "user")
|
|
|
|
+ .scope(registeredClient.getScopes().iterator().next())
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ this.authorizationConsentService.save(authorizationConsent);
|
|
|
|
+
|
|
|
|
+ MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
|
|
|
+ parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
|
|
|
|
+ parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
|
|
|
+ .params(parameters)
|
|
|
|
+ .headers(withClientAuth(registeredClient)))
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
+ .andExpect(jsonPath("$.access_token").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.expires_in").isNumber())
|
|
|
|
+ .andExpect(jsonPath("$.scope").isNotEmpty())
|
|
|
|
+ .andExpect(jsonPath("$.token_type").isNotEmpty())
|
|
|
|
+ .andReturn();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
|
|
|
|
+ assertThat(updatedAuthorization).isNotNull();
|
|
|
|
+ assertThat(updatedAuthorization.getAccessToken()).isNotNull();
|
|
|
|
+ assertThat(updatedAuthorization.getRefreshToken()).isNotNull();
|
|
|
|
+ // @formatter:off
|
|
|
|
+ assertThat(updatedAuthorization.getToken(OAuth2DeviceCode.class))
|
|
|
|
+ .extracting(isInvalidated())
|
|
|
|
+ .isEqualTo(true);
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ MockHttpServletResponse servletResponse = mvcResult.getResponse();
|
|
|
|
+ MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
|
|
|
|
+ HttpStatus.OK);
|
|
|
|
+ OAuth2AccessTokenResponse accessTokenResponse =
|
|
|
|
+ accessTokenResponseHttpMessageConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
|
|
|
|
+
|
|
|
|
+ String accessToken = accessTokenResponse.getAccessToken().getTokenValue();
|
|
|
|
+ OAuth2Authorization accessTokenAuthorization = this.authorizationService.findByToken(accessToken,
|
|
|
|
+ OAuth2TokenType.ACCESS_TOKEN);
|
|
|
|
+ assertThat(accessTokenAuthorization).isEqualTo(updatedAuthorization);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static HttpHeaders withClientAuth(RegisteredClient registeredClient) {
|
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
|
+ headers.setBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret());
|
|
|
|
+ return headers;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static Consumer<Map<String, Object>> withInvalidated() {
|
|
|
|
+ return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() {
|
|
|
|
+ return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @EnableWebSecurity
|
|
|
|
+ @Import(OAuth2AuthorizationServerConfiguration.class)
|
|
|
|
+ static class AuthorizationServerConfiguration {
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
|
|
|
|
+ return new JdbcRegisteredClientRepository(jdbcOperations);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
|
|
|
|
+ RegisteredClientRepository registeredClientRepository) {
|
|
|
|
+ return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations,
|
|
|
|
+ RegisteredClientRepository registeredClientRepository) {
|
|
|
|
+ return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ JdbcOperations jdbcOperations() {
|
|
|
|
+ return new JdbcTemplate(db);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ JWKSource<SecurityContext> jwkSource() {
|
|
|
|
+ return jwkSource;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Bean
|
|
|
|
+ PasswordEncoder passwordEncoder() {
|
|
|
|
+ return NoOpPasswordEncoder.getInstance();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|