/*
 * Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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 jakarta.servlet.http.HttpServletResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.data.TemporalUnitWithinOffset;
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.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
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.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
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.converter.OAuth2ClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2ClientRegistrationHttpMessageConverter;
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.web.authentication.OAuth2ClientRegistrationAuthenticationConverter;
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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.CollectionUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
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 OAuth 2.0 Dynamic Client Registration.
 *
 * @author Joe Grandja
 */
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2ClientRegistrationTests {

	private static final String ISSUER = "https://example.com:8443/issuer1";

	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";

	private static final String DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI = "/oauth2/register";

	private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();

	private static final HttpMessageConverter<OAuth2ClientRegistration> clientRegistrationHttpMessageConverter = new OAuth2ClientRegistrationHttpMessageConverter();

	private static EmbeddedDatabase db;

	private static JWKSource<SecurityContext> jwkSource;

	public final SpringTestContext spring = new SpringTestContext(this);

	@Autowired
	private MockMvc mvc;

	@Autowired
	private JdbcOperations jdbcOperations;

	@Autowired
	private RegisteredClientRepository registeredClientRepository;

	private static AuthenticationConverter authenticationConverter;

	private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;

	private static AuthenticationProvider authenticationProvider;

	private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;

	private static AuthenticationSuccessHandler authenticationSuccessHandler;

	private static AuthenticationFailureHandler authenticationFailureHandler;

	private MockWebServer server;

	@BeforeAll
	public static void init() {
		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
		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/client/oauth2-registered-client-schema.sql")
			.build();
		authenticationConverter = mock(AuthenticationConverter.class);
		authenticationConvertersConsumer = mock(Consumer.class);
		authenticationProvider = mock(AuthenticationProvider.class);
		authenticationProvidersConsumer = mock(Consumer.class);
		authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
		authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
	}

	@BeforeEach
	public void setup() throws Exception {
		this.server = new MockWebServer();
		this.server.start();
		given(authenticationProvider.supports(OAuth2ClientRegistrationAuthenticationToken.class)).willReturn(true);
	}

	@AfterEach
	public void tearDown() throws Exception {
		this.server.shutdown();
		this.jdbcOperations.update("truncate table oauth2_authorization");
		this.jdbcOperations.update("truncate table oauth2_registered_client");
		reset(authenticationConverter);
		reset(authenticationConvertersConsumer);
		reset(authenticationProvider);
		reset(authenticationProvidersConsumer);
		reset(authenticationSuccessHandler);
		reset(authenticationFailureHandler);
	}

	@AfterAll
	public static void destroy() {
		db.shutdown();
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);

		assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		MvcResult mvcResult = this.mvc
			.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI))
				.contentType(MediaType.APPLICATION_JSON)
				.content(getClientRegistrationRequestContent(clientRegistration)))
			.andExpect(status().isCreated())
			.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
			.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
			.andReturn();

		OAuth2ClientRegistration clientRegistrationResponse = readClientRegistrationResponse(mvcResult.getResponse());

		assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		willAnswer((invocation) -> {
			HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
			ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
			httpResponse.setStatusCode(HttpStatus.CREATED);
			new OAuth2ClientRegistrationHttpMessageConverter().write(clientRegistration, null, httpResponse);
			return null;
		}).given(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());

		registerClient(clientRegistration);

		verify(authenticationConverter).convert(any());
		ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
			.forClass(List.class);
		verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
		assertThat(authenticationConverters).hasSize(2)
			.allMatch((converter) -> converter == authenticationConverter
					|| converter instanceof OAuth2ClientRegistrationAuthenticationConverter);

		verify(authenticationProvider).authenticate(any());
		ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
			.forClass(List.class);
		verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
		List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
		assertThat(authenticationProviders).hasSize(2)
			.allMatch((provider) -> provider == authenticationProvider
					|| provider instanceof OAuth2ClientRegistrationAuthenticationProvider);

		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
		verifyNoInteractions(authenticationFailureHandler);
	}

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

		given(authenticationProvider.authenticate(any())).willThrow(new OAuth2AuthenticationException("error"));

		this.mvc.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).with(jwt()));

		verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
		verifyNoInteractions(authenticationSuccessHandler);
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);

		this.mvc
			.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.param(OAuth2ParameterNames.SCOPE, "scope1")
				.with(httpBasic(clientRegistrationResponse.getClientId(),
						clientRegistrationResponse.getClientSecret())))
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.access_token").isNotEmpty())
			.andExpect(jsonPath("$.scope").value("scope1"))
			.andReturn();
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.claim("custom-metadata-name-1", "value-1")
				.claim("custom-metadata-name-2", "value-2")
				.claim("non-registered-custom-metadata", "value-3")
				.build();
		// @formatter:on

		OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);

		RegisteredClient registeredClient = this.registeredClientRepository
			.findByClientId(clientRegistrationResponse.getClientId());

		assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
		assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-1")).isEqualTo("value-1");
		assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-2")).isEqualTo("value-2");
		assertThat(clientRegistrationResponse.<String>getClaim("non-registered-custom-metadata")).isNull();

		assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-1"))
			.isEqualTo("value-1");
		assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-2"))
			.isEqualTo("value-2");
		assertThat(registeredClient.getClientSettings().<String>getSetting("non-registered-custom-metadata")).isNull();
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);

		Instant expectedSecretExpiryDate = Instant.now().plus(Duration.ofHours(24));
		TemporalUnitWithinOffset allowedDelta = new TemporalUnitWithinOffset(1, ChronoUnit.MINUTES);

		// Returned response contains expiration date
		assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNotNull()
			.isCloseTo(expectedSecretExpiryDate, allowedDelta);

		RegisteredClient registeredClient = this.registeredClientRepository
			.findByClientId(clientRegistrationResponse.getClientId());

		// Persisted RegisteredClient contains expiration date
		assertThat(registeredClient).isNotNull();
		assertThat(registeredClient.getClientSecretExpiresAt()).isNotNull()
			.isCloseTo(expectedSecretExpiryDate, allowedDelta);
	}

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

		// @formatter:off
		OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
				.clientName("client-name")
				.redirectUri("https://client.example.com")
				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.scope("scope1")
				.scope("scope2")
				.build();
		// @formatter:on

		OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);

		RegisteredClient registeredClient = this.registeredClientRepository
			.findByClientId(clientRegistrationResponse.getClientId());

		assertThat(registeredClient).isNotNull();
		assertThat(registeredClient.getTokenSettings().getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
	}

	private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
		// ***** (1) Obtain the "initial" access token used for registering the client

		String clientRegistrationScope = "client.create";
		// @formatter:off
		RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-1")
				.clientId("client-registrar-1")
				.clientSecret("{noop}secret")
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
				.scope(clientRegistrationScope)
				.build();
		// @formatter:on
		this.registeredClientRepository.save(clientRegistrar);

		MvcResult mvcResult = this.mvc
			.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
				.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
				.with(httpBasic("client-registrar-1", "secret")))
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.access_token").isNotEmpty())
			.andExpect(jsonPath("$.scope").value(clientRegistrationScope))
			.andReturn();

		OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken();

		// ***** (2) Register the client

		HttpHeaders httpHeaders = new HttpHeaders();
		httpHeaders.setBearerAuth(accessToken.getTokenValue());

		// Register the client
		mvcResult = this.mvc
			.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
				.contentType(MediaType.APPLICATION_JSON)
				.content(getClientRegistrationRequestContent(clientRegistration)))
			.andExpect(status().isCreated())
			.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
			.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
			.andReturn();

		return readClientRegistrationResponse(mvcResult.getResponse());
	}

	private static void assertClientRegistrationResponse(OAuth2ClientRegistration clientRegistrationRequest,
			OAuth2ClientRegistration clientRegistrationResponse) {
		assertThat(clientRegistrationResponse.getClientId()).isNotNull();
		assertThat(clientRegistrationResponse.getClientIdIssuedAt()).isNotNull();
		assertThat(clientRegistrationResponse.getClientSecret()).isNotNull();
		assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNull();
		assertThat(clientRegistrationResponse.getClientName()).isEqualTo(clientRegistrationRequest.getClientName());
		assertThat(clientRegistrationResponse.getRedirectUris())
			.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getRedirectUris());
		assertThat(clientRegistrationResponse.getGrantTypes())
			.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getGrantTypes());
		assertThat(clientRegistrationResponse.getResponseTypes())
			.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
		assertThat(clientRegistrationResponse.getScopes())
			.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getScopes());
		assertThat(clientRegistrationResponse.getTokenEndpointAuthenticationMethod())
			.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
	}

	private static OAuth2AccessTokenResponse readAccessTokenResponse(MockHttpServletResponse response)
			throws Exception {
		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
				HttpStatus.valueOf(response.getStatus()));
		return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
	}

	private static byte[] getClientRegistrationRequestContent(OAuth2ClientRegistration clientRegistration)
			throws Exception {
		MockHttpOutputMessage httpRequest = new MockHttpOutputMessage();
		clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpRequest);
		return httpRequest.getBodyAsBytes();
	}

	private static OAuth2ClientRegistration readClientRegistrationResponse(MockHttpServletResponse response)
			throws Exception {
		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
				HttpStatus.valueOf(response.getStatus()));
		return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
	}

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

		// @formatter:off
		@Bean
		@Override
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint((clientRegistration) ->
											clientRegistration
													.clientRegistrationRequestConverter(authenticationConverter)
													.clientRegistrationRequestConverters(authenticationConvertersConsumer)
													.authenticationProvider(authenticationProvider)
													.authenticationProviders(authenticationProvidersConsumer)
													.clientRegistrationResponseHandler(authenticationSuccessHandler)
													.errorResponseHandler(authenticationFailureHandler)
									)
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

	}

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

		// @formatter:off
		@Bean
		@Override
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint((clientRegistration) ->
											clientRegistration
													.authenticationProviders(configureClientRegistrationConverters())
									)
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

		private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
			// @formatter:off
			return (authenticationProviders) ->
					authenticationProviders.forEach((authenticationProvider) -> {
						List<String> supportedCustomClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2");
						if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
							provider.setRegisteredClientConverter(new CustomRegisteredClientConverter(supportedCustomClientMetadata));
							provider.setClientRegistrationConverter(new CustomClientRegistrationConverter(supportedCustomClientMetadata));
						}
					});
			// @formatter:on
		}

	}

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

		// @formatter:off
		@Bean
		@Override
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint((clientRegistration) ->
											clientRegistration
													.authenticationProviders(configureClientRegistrationConverters())
									)
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

		private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
			// @formatter:off
			return (authenticationProviders) ->
					authenticationProviders.forEach((authenticationProvider) -> {
						if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
							provider.setRegisteredClientConverter(new ClientSecretExpirationRegisteredClientConverter());
						}
					});
			// @formatter:on
		}

	}

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

		// @formatter:off
		@Bean
		@Override
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint((clientRegistration) ->
											clientRegistration
													.authenticationProviders(configureClientRegistrationConverters())
									)
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

		private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
			// @formatter:off
			return (authenticationProviders) ->
					authenticationProviders.forEach((authenticationProvider) -> {
						if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
							OAuth2ClientRegistrationRegisteredClientConverter clientRegistrationRegisteredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
							clientRegistrationRegisteredClientConverter.setTokenSettingsCustomizer((tokenSettings) -> tokenSettings.accessTokenTimeToLive(Duration.ofMinutes(60)));
							provider.setRegisteredClientConverter(clientRegistrationRegisteredClientConverter);
						}
					});
			// @formatter:on
		}

	}

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

		// @formatter:off
		@Bean
		@Override
		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint((clientRegistration) ->
											clientRegistration
													.openRegistrationAllowed(true)
									)
					)
					.authorizeHttpRequests((authorize) ->
							authorize
									.requestMatchers("/**/oauth2/register").permitAll()
									.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

	}

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

		// @formatter:off
		@Bean
		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			http
					.oauth2AuthorizationServer((authorizationServer) ->
							authorizationServer
									.clientRegistrationEndpoint(Customizer.withDefaults())
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

		@Bean
		@SuppressWarnings("removal")
		RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
			RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
			RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
			JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(
					jdbcOperations);
			registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
			registeredClientRepository.save(registeredClient);
			return registeredClientRepository;
		}

		@Bean
		OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
				RegisteredClientRepository registeredClientRepository) {
			return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
		}

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

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

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

		@Bean
		AuthorizationServerSettings authorizationServerSettings() {
			return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
		}

		@Bean
		PasswordEncoder passwordEncoder() {
			return PasswordEncoderFactories.createDelegatingPasswordEncoder();
		}

	}

	private static final class CustomRegisteredClientConverter
			implements Converter<OAuth2ClientRegistration, RegisteredClient> {

		private final OAuth2ClientRegistrationRegisteredClientConverter delegate = new OAuth2ClientRegistrationRegisteredClientConverter();

		private final List<String> supportedCustomClientMetadata;

		private CustomRegisteredClientConverter(List<String> supportedCustomClientMetadata) {
			this.supportedCustomClientMetadata = supportedCustomClientMetadata;
		}

		@Override
		public RegisteredClient convert(OAuth2ClientRegistration clientRegistration) {
			RegisteredClient registeredClient = this.delegate.convert(clientRegistration);

			ClientSettings.Builder clientSettingsBuilder = ClientSettings
				.withSettings(registeredClient.getClientSettings().getSettings());
			if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
				clientRegistration.getClaims().forEach((claim, value) -> {
					if (this.supportedCustomClientMetadata.contains(claim)) {
						clientSettingsBuilder.setting(claim, value);
					}
				});
			}

			return RegisteredClient.from(registeredClient).clientSettings(clientSettingsBuilder.build()).build();
		}

	}

	private static final class CustomClientRegistrationConverter
			implements Converter<RegisteredClient, OAuth2ClientRegistration> {

		private final RegisteredClientOAuth2ClientRegistrationConverter delegate = new RegisteredClientOAuth2ClientRegistrationConverter();

		private final List<String> supportedCustomClientMetadata;

		private CustomClientRegistrationConverter(List<String> supportedCustomClientMetadata) {
			this.supportedCustomClientMetadata = supportedCustomClientMetadata;
		}

		@Override
		public OAuth2ClientRegistration convert(RegisteredClient registeredClient) {
			OAuth2ClientRegistration clientRegistration = this.delegate.convert(registeredClient);

			Map<String, Object> clientMetadata = new HashMap<>(clientRegistration.getClaims());
			if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
				Map<String, Object> clientSettings = registeredClient.getClientSettings().getSettings();
				this.supportedCustomClientMetadata.forEach((customClaim) -> {
					if (clientSettings.containsKey(customClaim)) {
						clientMetadata.put(customClaim, clientSettings.get(customClaim));
					}
				});
			}

			return OAuth2ClientRegistration.withClaims(clientMetadata).build();
		}

	}

	/**
	 * This customization adds client secret expiration time by setting
	 * {@code RegisteredClient.clientSecretExpiresAt} during
	 * {@code OAuth2ClientRegistration} -> {@code RegisteredClient} conversion
	 */
	private static final class ClientSecretExpirationRegisteredClientConverter
			implements Converter<OAuth2ClientRegistration, RegisteredClient> {

		private static final OAuth2ClientRegistrationRegisteredClientConverter delegate = new OAuth2ClientRegistrationRegisteredClientConverter();

		@Override
		public RegisteredClient convert(OAuth2ClientRegistration clientRegistration) {
			RegisteredClient registeredClient = delegate.convert(clientRegistration);
			RegisteredClient.Builder registeredClientBuilder = RegisteredClient.from(registeredClient);

			Instant clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24));
			registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt);

			return registeredClientBuilder.build();
		}

	}

}
