/*
 * Copyright 2012-2019 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.boot.autoconfigure.security.oauth2;

import java.net.URI;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.authserver.OAuth2AuthorizationServerConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.method.OAuth2MethodSecurityConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource;
import org.springframework.security.access.method.MethodSecurityMetadataSource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdvice;
import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

/**
 * Verify Spring Security OAuth2 auto-configuration secures end points properly, accepts
 * environmental overrides, and also backs off in the presence of other
 * resource/authorization components.
 *
 * @author Greg Turnquist
 * @author Dave Syer
 */
public class OAuth2AutoConfigurationTests {

	private static final Class<?> RESOURCE_SERVER_CONFIG = OAuth2ResourceServerConfiguration.class;

	private static final Class<?> AUTHORIZATION_SERVER_CONFIG = OAuth2AuthorizationServerConfiguration.class;

	private AnnotationConfigServletWebServerApplicationContext context;

	@Test
	public void testDefaultConfiguration() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(AuthorizationAndResourceServerConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		this.context.getBean(AUTHORIZATION_SERVER_CONFIG);
		this.context.getBean(RESOURCE_SERVER_CONFIG);
		this.context.getBean(OAuth2MethodSecurityConfiguration.class);
		ClientDetails config = this.context.getBean(BaseClientDetails.class);
		AuthorizationEndpoint endpoint = this.context.getBean(AuthorizationEndpoint.class);
		UserApprovalHandler handler = (UserApprovalHandler) ReflectionTestUtils.getField(endpoint,
				"userApprovalHandler");
		ClientDetailsService clientDetailsService = this.context.getBean(ClientDetailsService.class);
		ClientDetails clientDetails = clientDetailsService.loadClientByClientId(config.getClientId());
		assertThat(AopUtils.isJdkDynamicProxy(clientDetailsService)).isTrue();
		assertThat(AopUtils.getTargetClass(clientDetailsService).getName())
				.isEqualTo(InMemoryClientDetailsService.class.getName());
		assertThat(handler).isInstanceOf(ApprovalStoreUserApprovalHandler.class);
		assertThat(clientDetails).isEqualTo(config);
		verifyAuthentication(config);
		assertThat(this.context.getBeanNamesForType(OAuth2RestOperations.class)).isEmpty();
	}

	@Test
	public void methodSecurityExpressionHandlerIsConfiguredWithRoleHierarchyFromTheContext() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(RoleHierarchyConfiguration.class, AuthorizationAndResourceServerConfiguration.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		PreInvocationAuthorizationAdvice advice = this.context.getBean(PreInvocationAuthorizationAdvice.class);
		MethodSecurityExpressionHandler expressionHandler = (MethodSecurityExpressionHandler) ReflectionTestUtils
				.getField(advice, "expressionHandler");
		RoleHierarchy roleHierarchy = (RoleHierarchy) ReflectionTestUtils.getField(expressionHandler, "roleHierarchy");
		assertThat(roleHierarchy).isSameAs(this.context.getBean(RoleHierarchy.class));
	}

	@Test
	public void methodSecurityExpressionHandlerIsConfiguredWithPermissionEvaluatorFromTheContext() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(PermissionEvaluatorConfiguration.class, AuthorizationAndResourceServerConfiguration.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		PreInvocationAuthorizationAdvice advice = this.context.getBean(PreInvocationAuthorizationAdvice.class);
		MethodSecurityExpressionHandler expressionHandler = (MethodSecurityExpressionHandler) ReflectionTestUtils
				.getField(advice, "expressionHandler");
		PermissionEvaluator permissionEvaluator = (PermissionEvaluator) ReflectionTestUtils.getField(expressionHandler,
				"permissionEvaluator");
		assertThat(permissionEvaluator).isSameAs(this.context.getBean(PermissionEvaluator.class));
	}

	@Test
	public void testEnvironmentalOverrides() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		TestPropertyValues.of("security.oauth2.client.clientId:myclientid",
				"security.oauth2.client.clientSecret:mysecret", "security.oauth2.client.autoApproveScopes:read,write",
				"security.oauth2.client.accessTokenValiditySeconds:40",
				"security.oauth2.client.refreshTokenValiditySeconds:80").applyTo(this.context);
		this.context.register(AuthorizationAndResourceServerConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		ClientDetails config = this.context.getBean(ClientDetails.class);
		assertThat(config.getClientId()).isEqualTo("myclientid");
		assertThat(config.getClientSecret()).isEqualTo("mysecret");
		assertThat(config.isAutoApprove("read")).isTrue();
		assertThat(config.isAutoApprove("write")).isTrue();
		assertThat(config.isAutoApprove("foo")).isFalse();
		assertThat(config.getAccessTokenValiditySeconds()).isEqualTo(40);
		assertThat(config.getRefreshTokenValiditySeconds()).isEqualTo(80);
		verifyAuthentication(config);
	}

	@Test
	public void testDisablingResourceServer() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(AuthorizationServerConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
	}

	@Test
	public void testClientIsNotResourceServer() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(ClientConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(0);
		// Scoped target and proxy:
		assertThat(countBeans(OAuth2ClientContext.class)).isEqualTo(2);
	}

	@Test
	public void testCanUseClientCredentials() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(TestSecurityConfiguration.class, MinimalSecureWebApplication.class);
		TestPropertyValues
				.of("security.oauth2.client.clientId=client", "security.oauth2.client.grantType=client_credentials")
				.applyTo(this.context);
		ConfigurationPropertySources.attach(this.context.getEnvironment());
		this.context.refresh();
		OAuth2ClientContext bean = this.context.getBean(OAuth2ClientContext.class);
		assertThat(bean.getAccessTokenRequest()).isNotNull();
		assertThat(countBeans(ClientCredentialsResourceDetails.class)).isEqualTo(1);
		assertThat(countBeans(OAuth2ClientContext.class)).isEqualTo(1);
	}

	@Test
	public void testCanUseClientCredentialsWithEnableOAuth2Client() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(ClientConfiguration.class, MinimalSecureWebApplication.class);
		TestPropertyValues
				.of("security.oauth2.client.clientId=client", "security.oauth2.client.grantType=client_credentials")
				.applyTo(this.context);
		ConfigurationPropertySources.attach(this.context.getEnvironment());
		this.context.refresh();
		// The primary context is fine (not session scoped):
		OAuth2ClientContext bean = this.context.getBean(OAuth2ClientContext.class);
		assertThat(bean.getAccessTokenRequest()).isNotNull();
		assertThat(countBeans(ClientCredentialsResourceDetails.class)).isEqualTo(1);
		// Kind of a bug (should ideally be 1), but the cause is in Spring OAuth2 (there
		// is no need for the extra session-scoped bean). What this test proves is that
		// even if the user screws up and does @EnableOAuth2Client for client
		// credentials,
		// it will still just about work (because of the @Primary annotation on the
		// Boot-created instance of OAuth2ClientContext).
		assertThat(countBeans(OAuth2ClientContext.class)).isEqualTo(2);
	}

	@Test
	public void testClientIsNotAuthCode() {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(MinimalSecureNonWebApplication.class);
		TestPropertyValues.of("security.oauth2.client.clientId=client").applyTo(context);
		context.refresh();
		assertThat(countBeans(context, ClientCredentialsResourceDetails.class)).isEqualTo(1);
		context.close();
	}

	@Test
	public void testDisablingAuthorizationServer() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(ResourceServerConfiguration.class, MinimalSecureWebApplication.class);
		TestPropertyValues.of("security.oauth2.resource.jwt.keyValue:DEADBEEF").applyTo(this.context);
		ConfigurationPropertySources.attach(this.context.getEnvironment());
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(UserApprovalHandler.class)).isEqualTo(0);
		assertThat(countBeans(DefaultTokenServices.class)).isEqualTo(1);
	}

	@Test
	public void testResourceServerOverride() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(AuthorizationAndResourceServerConfiguration.class, CustomResourceServer.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		ClientDetails config = this.context.getBean(ClientDetails.class);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(CustomResourceServer.class)).isEqualTo(1);
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(1);
		verifyAuthentication(config);
	}

	@Test
	public void testAuthorizationServerOverride() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		TestPropertyValues.of("security.oauth2.resourceId:resource-id").applyTo(this.context);
		this.context.register(AuthorizationAndResourceServerConfiguration.class, CustomAuthorizationServer.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		BaseClientDetails config = new BaseClientDetails();
		config.setClientId("client");
		config.setClientSecret("secret");
		config.setResourceIds(Arrays.asList("resource-id"));
		config.setAuthorizedGrantTypes(Arrays.asList("password"));
		config.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList("USER"));
		config.setScope(Arrays.asList("read"));
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(CustomAuthorizationServer.class)).isEqualTo(1);
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(1);
		verifyAuthentication(config);
	}

	@Test
	public void testDefaultPrePostSecurityAnnotations() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(AuthorizationAndResourceServerConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		this.context.getBean(OAuth2MethodSecurityConfiguration.class);
		ClientDetails config = this.context.getBean(ClientDetails.class);
		DelegatingMethodSecurityMetadataSource source = this.context
				.getBean(DelegatingMethodSecurityMetadataSource.class);
		List<MethodSecurityMetadataSource> sources = source.getMethodSecurityMetadataSources();
		assertThat(sources.size()).isEqualTo(1);
		assertThat(sources.get(0).getClass().getName())
				.isEqualTo(PrePostAnnotationSecurityMetadataSource.class.getName());
		verifyAuthentication(config);
	}

	@Test
	public void testClassicSecurityAnnotationOverride() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(SecuredEnabledConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		this.context.getBean(OAuth2MethodSecurityConfiguration.class);
		ClientDetails config = this.context.getBean(ClientDetails.class);
		DelegatingMethodSecurityMetadataSource source = this.context
				.getBean(DelegatingMethodSecurityMetadataSource.class);
		List<MethodSecurityMetadataSource> sources = source.getMethodSecurityMetadataSources();
		assertThat(sources.size()).isEqualTo(1);
		assertThat(sources.get(0).getClass().getName())
				.isEqualTo(SecuredAnnotationSecurityMetadataSource.class.getName());
		verifyAuthentication(config, HttpStatus.OK);
	}

	@Test
	public void testJsr250SecurityAnnotationOverride() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(Jsr250EnabledConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		this.context.getBean(OAuth2MethodSecurityConfiguration.class);
		ClientDetails config = this.context.getBean(ClientDetails.class);
		DelegatingMethodSecurityMetadataSource source = this.context
				.getBean(DelegatingMethodSecurityMetadataSource.class);
		List<MethodSecurityMetadataSource> sources = source.getMethodSecurityMetadataSources();
		assertThat(sources.size()).isEqualTo(1);
		assertThat(sources.get(0).getClass().getName()).isEqualTo(Jsr250MethodSecurityMetadataSource.class.getName());
		verifyAuthentication(config, HttpStatus.OK);
	}

	@Test
	public void testMethodSecurityBackingOff() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(CustomMethodSecurity.class, TestSecurityConfiguration.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		DelegatingMethodSecurityMetadataSource source = this.context
				.getBean(DelegatingMethodSecurityMetadataSource.class);
		List<MethodSecurityMetadataSource> sources = source.getMethodSecurityMetadataSources();
		assertThat(sources.size()).isEqualTo(1);
		assertThat(sources.get(0).getClass().getName())
				.isEqualTo(PrePostAnnotationSecurityMetadataSource.class.getName());
	}

	@Test
	public void resourceServerConditionWhenJwkConfigurationPresentShouldMatch() throws Exception {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		TestPropertyValues.of("security.oauth2.resource.jwk.key-set-uri:https://idp.example.com/token_keys")
				.applyTo(this.context);
		this.context.register(ResourceServerConfiguration.class, MinimalSecureWebApplication.class);
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(1);
	}

	@Test
	public void authorizationServerWhenUsingJwtConfigurationThenConfiguresJwt() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		this.context.register(AuthorizationServerConfiguration.class, MinimalSecureWebApplication.class);
		TestPropertyValues.of("security.oauth2.authorization.jwt.keyValue:DEADBEEF").applyTo(this.context);
		ConfigurationPropertySources.attach(this.context.getEnvironment());
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(JwtAccessTokenConverter.class)).isEqualTo(1);
	}

	@Test
	public void authorizationServerWhenJwtConfigurationAndCustomAuthorizationServerThenConfiguresJwt() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		TestPropertyValues.of("security.oauth2.authorization.jwt.keyValue:DEADBEEF").applyTo(this.context);
		this.context.register(AuthorizationServerConfiguration.class, CustomAuthorizationServer.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(CustomAuthorizationServer.class)).isEqualTo(1);
		assertThat(countBeans(JwtAccessTokenConverter.class)).isEqualTo(1);
	}

	@Test
	public void authorizationServerWhenJwtKeyStoreConfigurationAndCustomAuthorizationServerThenConfiguresJwt() {
		this.context = new AnnotationConfigServletWebServerApplicationContext();
		TestPropertyValues.of(
				"security.oauth2.authorization.jwt.keyStore:classpath:"
						+ "org/springframework/boot/autoconfigure/security/oauth2/authserver/keystore.jks",
				"security.oauth2.authorization.jwt.keyStorePassword:changeme",
				"security.oauth2.authorization.jwt.keyAlias:jwt").applyTo(this.context);
		this.context.register(AuthorizationServerConfiguration.class, CustomAuthorizationServer.class,
				MinimalSecureWebApplication.class);
		this.context.refresh();
		assertThat(countBeans(RESOURCE_SERVER_CONFIG)).isEqualTo(0);
		assertThat(countBeans(AUTHORIZATION_SERVER_CONFIG)).isEqualTo(1);
		assertThat(countBeans(CustomAuthorizationServer.class)).isEqualTo(1);
		assertThat(countBeans(JwtAccessTokenConverter.class)).isEqualTo(1);
	}

	/**
	 * Connect to the oauth service, get a token, and then attempt some operations using
	 * it.
	 * @param config the client details.
	 */
	private void verifyAuthentication(ClientDetails config) {
		verifyAuthentication(config, HttpStatus.FORBIDDEN);
	}

	private void verifyAuthentication(ClientDetails config, HttpStatus finalStatus) {
		String baseUrl = "http://localhost:" + this.context.getWebServer().getPort();
		TestRestTemplate rest = new TestRestTemplate();
		// First, verify the web endpoint can't be reached
		assertEndpointUnauthorized(baseUrl, rest);
		// Since we can't reach it, need to collect an authorization token
		HttpHeaders headers = getHeaders(config);
		String url = baseUrl + "/oauth/token";
		JsonNode tokenResponse = rest.postForObject(url, new HttpEntity<>(getBody(), headers), JsonNode.class);
		String authorizationToken = tokenResponse.findValue("access_token").asText();
		String tokenType = tokenResponse.findValue("token_type").asText();
		String scope = tokenResponse.findValues("scope").get(0).toString();
		assertThat(tokenType).isEqualTo("bearer");
		assertThat(scope).isEqualTo("\"read\"");
		// Now we should be able to see that endpoint.
		headers.set("Authorization", "BEARER " + authorizationToken);
		ResponseEntity<String> securedResponse = rest.exchange(
				new RequestEntity<Void>(headers, HttpMethod.GET, URI.create(baseUrl + "/securedFind")), String.class);
		assertThat(securedResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
		assertThat(securedResponse.getBody())
				.isEqualTo("You reached an endpoint " + "secured by Spring Security OAuth2");
		ResponseEntity<String> entity = rest.exchange(
				new RequestEntity<Void>(headers, HttpMethod.POST, URI.create(baseUrl + "/securedSave")), String.class);
		assertThat(entity.getStatusCode()).isEqualTo(finalStatus);
	}

	private HttpHeaders getHeaders(ClientDetails config) {
		HttpHeaders headers = new HttpHeaders();
		String token = new String(
				Base64.getEncoder().encode((config.getClientId() + ":" + config.getClientSecret()).getBytes()));
		headers.set("Authorization", "Basic " + token);
		return headers;
	}

	private MultiValueMap<String, Object> getBody() {
		MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
		body.set("grant_type", "password");
		body.set("username", "foo");
		body.set("password", "bar");
		body.set("scope", "read");
		return body;
	}

	private void assertEndpointUnauthorized(String baseUrl, TestRestTemplate rest) {
		URI uri = URI.create(baseUrl + "/secured");
		ResponseEntity<String> entity = rest.exchange(new RequestEntity<Void>(HttpMethod.GET, uri), String.class);
		assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
	}

	private int countBeans(Class<?> type) {
		return countBeans(this.context, type);
	}

	private int countBeans(ApplicationContext context, Class<?> type) {
		return context.getBeanNamesForType(type).length;
	}

	@Configuration
	@Import({ UseFreePortEmbeddedContainerConfiguration.class, SecurityAutoConfiguration.class,
			DispatcherServletAutoConfiguration.class, OAuth2AutoConfiguration.class, WebMvcAutoConfiguration.class,
			HttpMessageConvertersAutoConfiguration.class })
	protected static class MinimalSecureWebApplication {

	}

	@Configuration
	@Import({ SecurityAutoConfiguration.class, OAuth2AutoConfiguration.class })
	protected static class MinimalSecureNonWebApplication {

	}

	@Configuration
	protected static class TestSecurityConfiguration extends WebSecurityConfigurerAdapter {

		@Override
		@Bean
		public AuthenticationManager authenticationManagerBean() throws Exception {
			return super.authenticationManagerBean();
		}

		@Autowired
		public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
			// @formatter:off
			auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance()).withUser("foo")
					.password("bar").roles("USER");
			// @formatter:on
		}

		@Bean
		TestWebApp testWebApp() {
			return new TestWebApp();
		}

	}

	@Configuration
	@EnableOAuth2Client
	protected static class ClientConfiguration extends TestSecurityConfiguration {

	}

	@Configuration
	@EnableAuthorizationServer
	@EnableResourceServer
	@EnableGlobalMethodSecurity(prePostEnabled = true)
	protected static class AuthorizationAndResourceServerConfiguration extends TestSecurityConfiguration {

	}

	@Configuration
	@EnableAuthorizationServer
	@EnableResourceServer
	@EnableGlobalMethodSecurity(securedEnabled = true)
	protected static class SecuredEnabledConfiguration extends TestSecurityConfiguration {

	}

	@Configuration
	@EnableAuthorizationServer
	@EnableResourceServer
	@EnableGlobalMethodSecurity(jsr250Enabled = true)
	protected static class Jsr250EnabledConfiguration extends TestSecurityConfiguration {

	}

	@Configuration
	@EnableAuthorizationServer
	protected static class AuthorizationServerConfiguration extends TestSecurityConfiguration {

	}

	@Configuration
	@EnableResourceServer
	protected static class ResourceServerConfiguration extends TestSecurityConfiguration {

	}

	@RestController
	protected static class TestWebApp {

		@GetMapping("/securedFind")
		@PreAuthorize("#oauth2.hasScope('read')")
		public String secureFind() {
			return "You reached an endpoint secured by Spring Security OAuth2";
		}

		@PostMapping("/securedSave")
		@PreAuthorize("#oauth2.hasScope('write')")
		public String secureSave() {
			return "You reached an endpoint secured by Spring Security OAuth2";
		}

	}

	@Configuration
	protected static class UseFreePortEmbeddedContainerConfiguration {

		@Bean
		TomcatServletWebServerFactory webServerFactory() {
			return new TomcatServletWebServerFactory(0);
		}

	}

	@Configuration
	@EnableResourceServer
	protected static class CustomResourceServer extends ResourceServerConfigurerAdapter {

		private final ResourceServerProperties config;

		protected CustomResourceServer(ResourceServerProperties config) {
			this.config = config;
		}

		@Override
		public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
			if (this.config.getId() != null) {
				resources.resourceId(this.config.getId());
			}
		}

		@Override
		public void configure(HttpSecurity http) throws Exception {
			// @formatter:off
			http.authorizeRequests().anyRequest().authenticated().and().httpBasic().and().csrf().disable();
			// @formatter:on
		}

	}

	@Configuration
	@EnableAuthorizationServer
	protected static class CustomAuthorizationServer extends AuthorizationServerConfigurerAdapter {

		private final AuthenticationManager authenticationManager;

		protected CustomAuthorizationServer(AuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
		}

		@Bean
		public TokenStore tokenStore() {
			return new InMemoryTokenStore();
		}

		@Bean
		public ApprovalStore approvalStore(final TokenStore tokenStore) {
			TokenApprovalStore approvalStore = new TokenApprovalStore();
			approvalStore.setTokenStore(tokenStore);
			return approvalStore;
		}

		@Override
		public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
			// @formatter:off
			clients.inMemory().withClient("client").secret("secret").resourceIds("resource-id")
					.authorizedGrantTypes("password").authorities("USER").scopes("read")
					.redirectUris("http://localhost:8080");
			// @formatter:on
		}

		@Override
		public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
			endpoints.tokenStore(tokenStore()).authenticationManager(this.authenticationManager);
		}

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

	}

	@Configuration
	@EnableGlobalMethodSecurity(prePostEnabled = true)
	protected static class CustomMethodSecurity extends GlobalMethodSecurityConfiguration {

		@Override
		protected MethodSecurityExpressionHandler createExpressionHandler() {
			return new OAuth2MethodSecurityExpressionHandler();
		}

	}

	@Configuration
	protected static class RoleHierarchyConfiguration {

		@Bean
		public RoleHierarchy roleHierarchy() {
			return mock(RoleHierarchy.class);
		}

	}

	@Configuration
	protected static class PermissionEvaluatorConfiguration {

		@Bean
		public PermissionEvaluator permissionEvaluator() {
			return mock(PermissionEvaluator.class);
		}

	}

}