From e0340f7b81cf674490aec44e56ec698a295a9a0a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 1 May 2023 18:09:50 -0500 Subject: [PATCH] Add integration tests for device grant Issue gh-1116 --- .../OAuth2DeviceCodeGrantTests.java | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java 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 new file mode 100644 index 00000000..e3ac6ece --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java @@ -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 jwkSource; + + private static final HttpMessageConverter deviceAuthorizationResponseHttpMessageConverter = + new OAuth2DeviceAuthorizationResponseHttpMessageConverter(); + + private static final HttpMessageConverter 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 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 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 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 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 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 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 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 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 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> withInvalidated() { + return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true); + } + + private static Function, 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 jwkSource() { + return jwkSource; + } + + @Bean + PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + + } + +}