/*
 * 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.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.text.MessageFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.assertj.core.matcher.AssertionMatcher;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.AuthenticationProvider;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
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.OAuth2AuthorizationCode;
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.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
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.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.test.SpringTestContext;
import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.RequestMatcher;
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 org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * Integration tests for the OAuth 2.0 Authorization Code Grant.
 *
 * @author Joe Grandja
 * @author Daniel Garnier-Moiroux
 * @author Dmitriy Dubson
 * @author Steve Riesenberg
 */
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2AuthorizationCodeGrantTests {
	private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
	// See RFC 7636: Appendix B.  Example for the S256 code_challenge_method
	// https://tools.ietf.org/html/rfc7636#appendix-B
	private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
	private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
	private static final String AUTHORITIES_CLAIM = "authorities";
	private static final String STATE_URL_UNENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ+004pwm9j55li7BoydXYysH4enZMF21Q";
	private static final String STATE_URL_ENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ%2B004pwm9j55li7BoydXYysH4enZMF21Q";

	private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);

	private static EmbeddedDatabase db;
	private static JWKSource<SecurityContext> jwkSource;
	private static NimbusJwtEncoder jwtEncoder;
	private static AuthorizationServerSettings authorizationServerSettings;
	private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
			new OAuth2AccessTokenResponseHttpMessageConverter();
	private static AuthenticationConverter authorizationRequestConverter;
	private static Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer;
	private static AuthenticationProvider authorizationRequestAuthenticationProvider;
	private static Consumer<List<AuthenticationProvider>> authorizationRequestAuthenticationProvidersConsumer;
	private static AuthenticationSuccessHandler authorizationResponseHandler;
	private static AuthenticationFailureHandler authorizationErrorResponseHandler;
	private static SecurityContextRepository securityContextRepository;
	private static String consentPage = "/oauth2/consent";

	public final SpringTestContext spring = new SpringTestContext();

	@Autowired
	private MockMvc mvc;

	@Autowired
	private JdbcOperations jdbcOperations;

	@Autowired
	private RegisteredClientRepository registeredClientRepository;

	@Autowired
	private OAuth2AuthorizationService authorizationService;

	@Autowired
	private JwtDecoder jwtDecoder;

	@Autowired(required = false)
	private OAuth2TokenGenerator<?> tokenGenerator;

	@BeforeAll
	public static void init() {
		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
		jwtEncoder = new NimbusJwtEncoder(jwkSource);
		authorizationServerSettings = AuthorizationServerSettings.builder()
				.authorizationEndpoint("/test/authorize")
				.tokenEndpoint("/test/token")
				.build();
		authorizationRequestConverter = mock(AuthenticationConverter.class);
		authorizationRequestConvertersConsumer = mock(Consumer.class);
		authorizationRequestAuthenticationProvider = mock(AuthenticationProvider.class);
		authorizationRequestAuthenticationProvidersConsumer = mock(Consumer.class);
		authorizationResponseHandler = mock(AuthenticationSuccessHandler.class);
		authorizationErrorResponseHandler = mock(AuthenticationFailureHandler.class);
		securityContextRepository = spy(new HttpSessionSecurityContextRepository());
		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();
	}

	@BeforeEach
	public void setup() {
		reset(securityContextRepository);
	}

	@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 requestWhenAuthorizationRequestNotAuthenticatedThenUnauthorized() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		this.registeredClientRepository.save(registeredClient);

		this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient)))
				.andExpect(status().isUnauthorized())
				.andReturn();
	}

	@Test
	public void requestWhenRegisteredClientMissingThenBadRequest() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

		this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient)))
				.andExpect(status().isBadRequest())
				.andReturn();
	}

	@Test
	public void requestWhenAuthorizationRequestAuthenticatedThenRedirectToClient() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		assertAuthorizationRequestRedirectsToClient(DEFAULT_AUTHORIZATION_ENDPOINT_URI);
	}

	@Test
	public void requestWhenAuthorizationRequestCustomEndpointThenRedirectToClient() throws Exception {
		this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();

		assertAuthorizationRequestRedirectsToClient(authorizationServerSettings.getAuthorizationEndpoint());
	}

	private void assertAuthorizationRequestRedirectsToClient(String authorizationEndpointUri) throws Exception {
		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.redirectUris(redirectUris -> {
					redirectUris.clear();
					redirectUris.add("https://example.com/callback-1?param=encoded%20parameter%20value");	// gh-1011
				})
				.build();
		this.registeredClientRepository.save(registeredClient);

		MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
		MvcResult mvcResult = this.mvc.perform(get(authorizationEndpointUri)
				.params(authorizationRequestParameters)
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		String redirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
		String code = extractParameterFromRedirectUri(redirectedUrl, "code");
		assertThat(redirectedUrl).isEqualTo(redirectUri + "&code=" + code + "&state=" + STATE_URL_ENCODED);

		String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
		OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
		assertThat(authorization).isNotNull();
		assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
	}

	@Test
	public void requestWhenTokenRequestValidThenReturnAccessTokenResponse() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		this.registeredClientRepository.save(registeredClient);

		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
		this.authorizationService.save(authorization);

		OAuth2AccessTokenResponse accessTokenResponse = assertTokenRequestReturnsAccessTokenResponse(
				registeredClient, authorization, DEFAULT_TOKEN_ENDPOINT_URI);

		// Assert user authorities was propagated as claim in JWT
		Jwt jwt = this.jwtDecoder.decode(accessTokenResponse.getAccessToken().getTokenValue());
		List<String> authoritiesClaim = jwt.getClaim(AUTHORITIES_CLAIM);
		Authentication principal = authorization.getAttribute(Principal.class.getName());
		Set<String> userAuthorities = new HashSet<>();
		for (GrantedAuthority authority : principal.getAuthorities()) {
			userAuthorities.add(authority.getAuthority());
		}

		assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
	}

	@Test
	public void requestWhenTokenRequestCustomEndpointThenReturnAccessTokenResponse() throws Exception {
		this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		this.registeredClientRepository.save(registeredClient);

		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
		this.authorizationService.save(authorization);

		assertTokenRequestReturnsAccessTokenResponse(
				registeredClient, authorization, authorizationServerSettings.getTokenEndpoint());
	}

	private OAuth2AccessTokenResponse assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
			OAuth2Authorization authorization, String tokenEndpointUri) throws Exception {
		MvcResult mvcResult = this.mvc.perform(post(tokenEndpointUri)
				.params(getTokenRequestParameters(registeredClient, authorization))
				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(status().isOk())
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(jsonPath("$.access_token").isNotEmpty())
				.andExpect(jsonPath("$.token_type").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andExpect(jsonPath("$.refresh_token").isNotEmpty())
				.andExpect(jsonPath("$.scope").isNotEmpty())
				.andReturn();

		OAuth2Authorization accessTokenAuthorization = this.authorizationService.findById(authorization.getId());
		assertThat(accessTokenAuthorization).isNotNull();
		assertThat(accessTokenAuthorization.getAccessToken()).isNotNull();
		assertThat(accessTokenAuthorization.getRefreshToken()).isNotNull();

		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCodeToken = accessTokenAuthorization.getToken(OAuth2AuthorizationCode.class);
		assertThat(authorizationCodeToken).isNotNull();
		assertThat(authorizationCodeToken.getMetadata().get(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME)).isEqualTo(true);

		MockHttpServletResponse servletResponse = mvcResult.getResponse();
		MockClientHttpResponse httpResponse = new MockClientHttpResponse(
				servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
		return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
	}

	@Test
	public void requestWhenPublicClientWithPkceThenReturnAccessTokenResponse() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
		this.registeredClientRepository.save(registeredClient);

		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient))
				.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
				.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);

		String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
		assertThat(authorizationCodeAuthorization).isNotNull();
		assertThat(authorizationCodeAuthorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);

		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.access_token").isNotEmpty())
				.andExpect(jsonPath("$.token_type").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andExpect(jsonPath("$.refresh_token").doesNotExist())
				.andExpect(jsonPath("$.scope").isNotEmpty());

		OAuth2Authorization accessTokenAuthorization = this.authorizationService.findById(authorizationCodeAuthorization.getId());
		assertThat(accessTokenAuthorization).isNotNull();
		assertThat(accessTokenAuthorization.getAccessToken()).isNotNull();

		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCodeToken = accessTokenAuthorization.getToken(OAuth2AuthorizationCode.class);
		assertThat(authorizationCodeToken).isNotNull();
		assertThat(authorizationCodeToken.getMetadata().get(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME)).isEqualTo(true);
	}

	@Test
	public void requestWhenConfidentialClientWithPkceAndMissingCodeVerifierThenBadRequest() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		this.registeredClientRepository.save(registeredClient);

		MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(authorizationRequestParameters)
				.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
				.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		String expectedRedirectUri = authorizationRequestParameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
		assertThat(redirectedUrl).matches(expectedRedirectUri + "\\?code=.{15,}&state=" + STATE_URL_ENCODED);

		String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
		assertThat(authorizationCodeAuthorization).isNotNull();
		assertThat(authorizationCodeAuthorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);

		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(status().isBadRequest());
	}

	// gh-1011
	@Test
	public void requestWhenConfidentialClientWithPkceAndMissingCodeChallengeThenErrorResponseEncoded() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		String redirectUri = "https://example.com/callback-1?param=encoded%20parameter%20value";
		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.redirectUris(redirectUris -> {
					redirectUris.clear();
					redirectUris.add(redirectUri);
				})
				.clientSettings(ClientSettings.builder().requireProofKey(true).build())
				.build();
		this.registeredClientRepository.save(registeredClient);

		MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(authorizationRequestParameters)
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		String expectedRedirectUri = redirectUri + "&" +
				"error=invalid_request&" +
				"error_description=" + UriUtils.encode("OAuth 2.0 Parameter: code_challenge", StandardCharsets.UTF_8) + "&" +
				"error_uri=" + UriUtils.encode("https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1", StandardCharsets.UTF_8) + "&" +
				"state=" + STATE_URL_ENCODED;
		assertThat(redirectedUrl).isEqualTo(expectedRedirectUri);
	}

	@Test
	public void requestWhenCustomTokenGeneratorThenUsed() throws Exception {
		this.spring.register(AuthorizationServerConfigurationWithTokenGenerator.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		this.registeredClientRepository.save(registeredClient);

		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
		this.authorizationService.save(authorization);

		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorization))
				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(status().isOk());

		verify(this.tokenGenerator, times(2)).generate(any());
	}

	@Test
	public void requestWhenRequiresConsentThenDisplaysConsentPage() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.scopes(scopes -> {
					scopes.clear();
					scopes.add("message.read");
					scopes.add("message.write");
				})
				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
				.build();
		this.registeredClientRepository.save(registeredClient);

		String consentPage = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient))
				.with(user("user")))
				.andExpect(status().is2xxSuccessful())
				.andReturn()
				.getResponse()
				.getContentAsString();

		assertThat(consentPage).contains("Consent required");
		assertThat(consentPage).contains(scopeCheckbox("message.read"));
		assertThat(consentPage).contains(scopeCheckbox("message.write"));
	}

	@Test
	public void requestWhenConsentRequestThenReturnAccessTokenResponse() throws Exception {
		this.spring.register(AuthorizationServerConfiguration.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.scopes(scopes -> {
					scopes.clear();
					scopes.add("message.read");
					scopes.add("message.write");
				})
				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
				.build();
		this.registeredClientRepository.save(registeredClient);

		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
				.principalName("user")
				.build();
		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
		OAuth2AuthorizationRequest updatedAuthorizationRequest =
				OAuth2AuthorizationRequest.from(authorizationRequest)
						.state(STATE_URL_UNENCODED)
						.build();
		authorization = OAuth2Authorization.from(authorization)
				.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
				.build();
		this.authorizationService.save(authorization);

		MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
				.param(OAuth2ParameterNames.SCOPE, "message.read")
				.param(OAuth2ParameterNames.SCOPE, "message.write")
				.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();

		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		assertThat(redirectedUrl).matches(authorizationRequest.getRedirectUri() + "\\?code=.{15,}&state=" + STATE_URL_ENCODED);

		String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);

		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(status().isOk())
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(jsonPath("$.access_token").isNotEmpty())
				.andExpect(jsonPath("$.token_type").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andExpect(jsonPath("$.refresh_token").isNotEmpty())
				.andExpect(jsonPath("$.scope").isNotEmpty())
				.andReturn();
	}

	@Test
	public void requestWhenCustomConsentPageConfiguredThenRedirect() throws Exception {
		this.spring.register(AuthorizationServerConfigurationCustomConsentPage.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.scopes(scopes -> {
					scopes.clear();
					scopes.add("message.read");
					scopes.add("message.write");
				})
				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
				.build();
		this.registeredClientRepository.save(registeredClient);

		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient))
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		assertThat(redirectedUrl).matches("http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+");

		String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
		UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
		MultiValueMap<String, String> redirectQueryParams = uriComponents.getQueryParams();

		assertThat(uriComponents.getPath()).isEqualTo(consentPage);
		assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("message.read message.write");
		assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo(registeredClient.getClientId());

		String state = extractParameterFromRedirectUri(redirectedUrl, "state");
		OAuth2Authorization authorization = this.authorizationService.findByToken(state, STATE_TOKEN_TYPE);
		assertThat(authorization).isNotNull();
	}

	@Test
	public void requestWhenCustomConsentCustomizerConfiguredThenUsed() throws Exception {
		this.spring.register(AuthorizationServerConfigurationCustomConsentRequest.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
				.clientSettings(ClientSettings.builder()
						.requireAuthorizationConsent(true)
						.setting("custom.allowed-authorities", "authority-1 authority-2")
						.build())
				.build();
		this.registeredClientRepository.save(registeredClient);

		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
				.build();
		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
		OAuth2AuthorizationRequest updatedAuthorizationRequest =
				OAuth2AuthorizationRequest.from(authorizationRequest)
						.state(STATE_URL_UNENCODED)
						.build();
		authorization = OAuth2Authorization.from(authorization)
				.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
				.build();
		this.authorizationService.save(authorization);

		MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
				.param("authority", "authority-1 authority-2")
				.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
				.with(user("principal")))
				.andExpect(status().is3xxRedirection())
				.andReturn();

		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		assertThat(redirectedUrl).matches(authorizationRequest.getRedirectUri() + "\\?code=.{15,}&state=" + STATE_URL_ENCODED);

		String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);

		mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(status().isOk())
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(jsonPath("$.access_token").isNotEmpty())
				.andExpect(jsonPath("$.access_token").value(new AssertionMatcher<String>() {
					@Override
					public void assertion(String accessToken) throws AssertionError {
						Jwt jwt = jwtDecoder.decode(accessToken);
						assertThat(jwt.getClaimAsStringList(AUTHORITIES_CLAIM))
								.containsExactlyInAnyOrder("authority-1", "authority-2");
					}
				}))
				.andExpect(jsonPath("$.token_type").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andExpect(jsonPath("$.refresh_token").isNotEmpty())
				.andExpect(jsonPath("$.scope").doesNotExist())
				.andReturn();
	}

	@Test
	public void requestWhenAuthorizationEndpointCustomizedThenUsed() throws Exception {
		this.spring.register(AuthorizationServerConfigurationCustomAuthorizationEndpoint.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
		TestingAuthenticationToken principal = new TestingAuthenticationToken("principalName", "password");
		OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
				"code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
				new OAuth2AuthorizationCodeRequestAuthenticationToken(
						"https://provider.com/oauth2/authorize", registeredClient.getClientId(), principal, authorizationCode,
						registeredClient.getRedirectUris().iterator().next(), STATE_URL_UNENCODED, registeredClient.getScopes());
		when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
		when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true);
		when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult);

		this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient))
				.with(user("user")))
				.andExpect(status().isOk());

		verify(authorizationRequestConverter).convert(any());

		@SuppressWarnings("unchecked")
		ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
		verify(authorizationRequestConvertersConsumer).accept(authenticationConvertersCaptor.capture());
		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
		assertThat(authenticationConverters).allMatch((converter) ->
				converter == authorizationRequestConverter ||
						converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter ||
						converter instanceof OAuth2AuthorizationConsentAuthenticationConverter);

		verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult));

		@SuppressWarnings("unchecked")
		ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
		verify(authorizationRequestAuthenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
		List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
		assertThat(authenticationProviders).allMatch((provider) ->
				provider == authorizationRequestAuthenticationProvider ||
						provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider ||
						provider instanceof OAuth2AuthorizationConsentAuthenticationProvider);

		verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult));
	}

	// gh-482
	@Test
	public void requestWhenClientObtainsAccessTokenThenClientAuthenticationNotPersisted() throws Exception {
		this.spring.register(AuthorizationServerConfigurationWithSecurityContextRepository.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
		this.registeredClientRepository.save(registeredClient);

		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
				.params(getAuthorizationRequestParameters(registeredClient))
				.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
				.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
				.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();

		ArgumentCaptor<org.springframework.security.core.context.SecurityContext> securityContextCaptor =
				ArgumentCaptor.forClass(org.springframework.security.core.context.SecurityContext.class);
		verify(securityContextRepository, times(1)).saveContext(securityContextCaptor.capture(), any(), any());
		assertThat(securityContextCaptor.getValue().getAuthentication()).isInstanceOf(UsernamePasswordAuthenticationToken.class);
		reset(securityContextRepository);

		String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code");
		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);

		mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.access_token").isNotEmpty())
				.andExpect(jsonPath("$.token_type").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andExpect(jsonPath("$.refresh_token").doesNotExist())
				.andExpect(jsonPath("$.scope").isNotEmpty())
				.andReturn();

		org.springframework.security.core.context.SecurityContext securityContext =
				securityContextRepository.loadDeferredContext(mvcResult.getRequest()).get();
		assertThat(securityContext.getAuthentication()).isNull();
	}

	private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
		parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
		parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
		parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
		parameters.set(OAuth2ParameterNames.SCOPE,
				StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
		parameters.set(OAuth2ParameterNames.STATE, STATE_URL_UNENCODED);
		return parameters;
	}

	private static MultiValueMap<String, String> getTokenRequestParameters(RegisteredClient registeredClient,
			OAuth2Authorization authorization) {
		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
		parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
		parameters.set(OAuth2ParameterNames.CODE, authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
		parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
		return parameters;
	}

	private static String getAuthorizationHeader(RegisteredClient registeredClient) throws Exception {
		String clientId = registeredClient.getClientId();
		String clientSecret = registeredClient.getClientSecret();
		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
		clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8.name());
		String credentialsString = clientId + ":" + clientSecret;
		byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
		return "Basic " + new String(encodedBytes, StandardCharsets.UTF_8);
	}

	private static String scopeCheckbox(String scope) {
		return MessageFormat.format(
				"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"{0}\" id=\"{0}\">",
				scope
		);
	}

	private String extractParameterFromRedirectUri(String redirectUri, String param) throws UnsupportedEncodingException {
		String locationHeader = URLDecoder.decode(redirectUri, StandardCharsets.UTF_8.name());
		UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
		return uriComponents.getQueryParams().getFirst(param);
	}

	@EnableWebSecurity
	@Import(OAuth2AuthorizationServerConfiguration.class)
	static class AuthorizationServerConfiguration {

		@Bean
		OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
			JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
			authorizationService.setAuthorizationRowMapper(new RowMapper(registeredClientRepository));
			authorizationService.setAuthorizationParametersMapper(new ParametersMapper());
			return authorizationService;
		}

		@Bean
		OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
			return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
		}

		@Bean
		RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
			JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations);
			RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
			jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
			return jdbcRegisteredClientRepository;
		}

		@Bean
		JdbcOperations jdbcOperations() {
			return new JdbcTemplate(db);
		}

		@Bean
		JWKSource<SecurityContext> jwkSource() {
			return jwkSource;
		}

		@Bean
		JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
			return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
		}

		@Bean
		OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
			return context -> {
				if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
						OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
					Authentication principal = context.getPrincipal();
					Set<String> authorities = new HashSet<>();
					for (GrantedAuthority authority : principal.getAuthorities()) {
						authorities.add(authority.getAuthority());
					}
					context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
				}
			};
		}

		@Bean
		PasswordEncoder passwordEncoder() {
			return NoOpPasswordEncoder.getInstance();
		}

		static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {

			RowMapper(RegisteredClientRepository registeredClientRepository) {
				super(registeredClientRepository);
				getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
			}

		}

		static class ParametersMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper {

			ParametersMapper() {
				super();
				getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class);
			}

		}

	}

	@EnableWebSecurity
	@Configuration(proxyBeanMethods = false)
	static class AuthorizationServerConfigurationWithSecurityContextRepository extends AuthorizationServerConfiguration {
		// @formatter:off
		@Bean
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
					new OAuth2AuthorizationServerConfigurer();
			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

			http
					.securityMatcher(endpointsMatcher)
					.authorizeHttpRequests(authorize ->
							authorize.anyRequest().authenticated()
					)
					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
					.securityContext(securityContext ->
							securityContext.securityContextRepository(securityContextRepository))
					.apply(authorizationServerConfigurer);
			return http.build();
		}
		// @formatter:on
	}

	@EnableWebSecurity
	@Import(OAuth2AuthorizationServerConfiguration.class)
	static class AuthorizationServerConfigurationWithTokenGenerator extends AuthorizationServerConfiguration {

		@Bean
		JwtEncoder jwtEncoder() {
			return jwtEncoder;
		}

		@Bean
		OAuth2TokenGenerator<?> tokenGenerator() {
			JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder());
			jwtGenerator.setJwtCustomizer(jwtCustomizer());
			OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
			OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator =
					new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
			return spy(new OAuth2TokenGenerator<OAuth2Token>() {
				@Override
				public OAuth2Token generate(OAuth2TokenContext context) {
					return delegatingTokenGenerator.generate(context);
				}
			});
		}

	}

	@EnableWebSecurity
	@Import(OAuth2AuthorizationServerConfiguration.class)
	static class AuthorizationServerConfigurationCustomEndpoints extends AuthorizationServerConfiguration {

		@Bean
		AuthorizationServerSettings authorizationServerSettings() {
			return authorizationServerSettings;
		}
	}

	@EnableWebSecurity
	@Configuration(proxyBeanMethods = false)
	static class AuthorizationServerConfigurationCustomConsentPage extends AuthorizationServerConfiguration {
		// @formatter:off
		@Bean
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
					new OAuth2AuthorizationServerConfigurer();
			authorizationServerConfigurer
					.authorizationEndpoint(authorizationEndpoint ->
							authorizationEndpoint.consentPage(consentPage));
			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

			http
					.securityMatcher(endpointsMatcher)
					.authorizeHttpRequests(authorize ->
							authorize.anyRequest().authenticated()
					)
					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
					.apply(authorizationServerConfigurer);
			return http.build();
		}
		// @formatter:on
	}

	@EnableWebSecurity
	@Configuration(proxyBeanMethods = false)
	static class AuthorizationServerConfigurationCustomConsentRequest extends AuthorizationServerConfiguration {

		@Autowired
		private OAuth2AuthorizationConsentService authorizationConsentService;

		// @formatter:off
		@Bean
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
					new OAuth2AuthorizationServerConfigurer();
			authorizationServerConfigurer
					.authorizationEndpoint(authorizationEndpoint ->
							authorizationEndpoint.authenticationProviders(configureAuthenticationProviders()));
			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

			http
					.securityMatcher(endpointsMatcher)
					.authorizeHttpRequests(authorize ->
							authorize.anyRequest().authenticated()
					)
					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
					.apply(authorizationServerConfigurer);
			return http.build();
		}
		// @formatter:on

		@Bean
		@Override
		OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
			return context -> {
				if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
						OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
					OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
							context.getRegisteredClient().getId(), context.getPrincipal().getName());

					Set<String> authorities = new HashSet<>();
					for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
						authorities.add(authority.getAuthority());
					}
					context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
				}
			};
		}

		private Consumer<List<AuthenticationProvider>> configureAuthenticationProviders() {
			return (authenticationProviders) ->
				authenticationProviders.forEach((authenticationProvider) -> {
					if (authenticationProvider instanceof OAuth2AuthorizationConsentAuthenticationProvider) {
						((OAuth2AuthorizationConsentAuthenticationProvider) authenticationProvider)
								.setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer());
					}
				});
		}

		static class AuthorizationConsentCustomizer implements Consumer<OAuth2AuthorizationConsentAuthenticationContext> {

			@Override
			public void accept(OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext) {
				OAuth2AuthorizationConsent.Builder authorizationConsentBuilder =
						authorizationConsentAuthenticationContext.getAuthorizationConsent();
				OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
						authorizationConsentAuthenticationContext.getAuthentication();
				Map<String, Object> additionalParameters =
						authorizationConsentAuthentication.getAdditionalParameters();
				RegisteredClient registeredClient = authorizationConsentAuthenticationContext.getRegisteredClient();
				ClientSettings clientSettings = registeredClient.getClientSettings();

				Set<String> requestedAuthorities = authorities((String) additionalParameters.get("authority"));
				Set<String> allowedAuthorities = authorities(clientSettings.getSetting("custom.allowed-authorities"));
				for (String requestedAuthority : requestedAuthorities) {
					if (allowedAuthorities.contains(requestedAuthority)) {
						authorizationConsentBuilder.authority(new SimpleGrantedAuthority(requestedAuthority));
					}
				}
			}

			private static Set<String> authorities(String param) {
				Set<String> authorities = new HashSet<>();
				if (param != null) {
					List<String> authorityValues = Arrays.asList(param.split(" "));
					authorities.addAll(authorityValues);
				}

				return authorities;
			}
		}
	}

	@EnableWebSecurity
	@Configuration(proxyBeanMethods = false)
	static class AuthorizationServerConfigurationCustomAuthorizationEndpoint extends AuthorizationServerConfiguration {
		// @formatter:off
		@Bean
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
					new OAuth2AuthorizationServerConfigurer();
			authorizationServerConfigurer
					.authorizationEndpoint(authorizationEndpoint ->
							authorizationEndpoint
									.authorizationRequestConverter(authorizationRequestConverter)
									.authorizationRequestConverters(authorizationRequestConvertersConsumer)
									.authenticationProvider(authorizationRequestAuthenticationProvider)
									.authenticationProviders(authorizationRequestAuthenticationProvidersConsumer)
									.authorizationResponseHandler(authorizationResponseHandler)
									.errorResponseHandler(authorizationErrorResponseHandler));
			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

			http
					.securityMatcher(endpointsMatcher)
					.authorizeHttpRequests(authorize ->
							authorize.anyRequest().authenticated()
					)
					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
					.apply(authorizationServerConfigurer);
			return http.build();
		}
		// @formatter:on
	}

}
