/*
 * 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();
		}

	}

}
