/*
 * Copyright 2002-2018 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
 *
 *      http://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;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;

import javax.servlet.http.HttpServletResponse;
import java.util.Base64;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Demonstrate the samples.
 *
 * @author Rob Winch
 * @author Joe Grandja
 */
public class SampleWebSecurityConfigurerAdapterTests {
	@Rule
	public final SpringTestRule spring = new SpringTestRule();

	@Autowired
	private FilterChainProxy springSecurityFilterChain;

	private MockHttpServletRequest request;
	private MockHttpServletResponse response;
	private MockFilterChain chain;

	@Before
	public void setup() {
		this.request = new MockHttpServletRequest("GET", "");
		this.response = new MockHttpServletResponse();
		this.chain = new MockFilterChain();

		CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "CSRF-TOKEN-TEST");
		new HttpSessionCsrfTokenRepository().saveToken(csrfToken, this.request, this.response);
		this.request.setParameter(csrfToken.getParameterName(), csrfToken.getToken());
	}

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

		this.request.addHeader("Accept", "text/html");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("http://localhost/login");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.request.addHeader("Accept", "text/html");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/login?error");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.request.addHeader("Accept", "text/html");
		this.request.addParameter("username", "user");
		this.request.addParameter("password", "password");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/");
	}

	/**
	 * <code>
	 *   <http>
	 *     <intercept-url pattern="/resources/**" access="permitAll"/>
	 *     <intercept-url pattern="/**" access="authenticated"/>
	 *     <logout
	 *         logout-success-url="/login?logout"
	 *         logout-url="/logout"
	 *     <form-login
	 *         authentication-failure-url="/login?error"
	 *         login-page="/login" <!-- Except Spring Security renders the login page -->
	 *         login-processing-url="/login" <!-- but only POST -->
	 *         password-parameter="password"
	 *         username-parameter="username"
	 *     />
	 *   </http>
	 *   <authentication-manager>
	 *     <authentication-provider>
	 *       <user-service>
	 *         <user username="user" password="password" authorities="ROLE_USER"/>
	 *       </user-service>
	 *     </authentication-provider>
	 *   </authentication-manager>
	 * </code>
	 * @author Rob Winch
	 */
	@EnableWebSecurity
	public static class HelloWorldWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth
				.inMemoryAuthentication()
					.withUser(PasswordEncodedUser.user());
		}
	}


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

		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("http://localhost/login");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/login?error");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.request.addParameter("username", "user");
		this.request.addParameter("password", "password");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/");
	}

	/**
	 * <code>
	 *   <http security="none" pattern="/resources/**"/>
	 *   <http>
	 *     <intercept-url pattern="/logout" access="permitAll"/>
	 *     <intercept-url pattern="/login" access="permitAll"/>
	 *     <intercept-url pattern="/signup" access="permitAll"/>
	 *     <intercept-url pattern="/about" access="permitAll"/>
	 *     <intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
	 *     <logout
	 *         logout-success-url="/login?logout"
	 *         logout-url="/logout"
	 *     <form-login
	 *         authentication-failure-url="/login?error"
	 *         login-page="/login"
	 *         login-processing-url="/login" <!-- but only POST -->
	 *         password-parameter="password"
	 *         username-parameter="username"
	 *     />
	 *   </http>
	 *   <authentication-manager>
	 *     <authentication-provider>
	 *       <user-service>
	 *         <user username="user" password="password" authorities="ROLE_USER"/>
	 *         <user username="admin" password="password" authorities="ROLE_USER,ROLE_ADMIN"/>
	 *       </user-service>
	 *     </authentication-provider>
	 *   </authentication-manager>
	 * </code>
	 * @author Rob Winch
	 */
	@EnableWebSecurity
	public static class SampleWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

		@Override
		public void configure(WebSecurity web) throws Exception {
			web
				.ignoring()
					.antMatchers("/resources/**");
		}

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.antMatchers("/signup", "/about").permitAll()
					.anyRequest().hasRole("USER")
					.and()
				.formLogin()
					.loginPage("/login")
						// set permitAll for all URLs associated with Form Login
						.permitAll();
		}

		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth
				.inMemoryAuthentication()
					.withUser(PasswordEncodedUser.user())
					.withUser(PasswordEncodedUser.admin());
		}
	}


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

		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("http://localhost/login");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/login?error");
	}

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

		this.request.setServletPath("/login");
		this.request.setMethod("POST");
		this.request.addParameter("username", "user");
		this.request.addParameter("password", "password");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getRedirectedUrl()).isEqualTo("/");
	}

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

		this.request.setServletPath("/api/admin/test");
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
	}

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

		this.request.setServletPath("/api/admin/test");
		this.request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".getBytes()));
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
	}

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

		this.request.setServletPath("/api/admin/test");
		this.request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("admin:password".getBytes()));
		this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);

		assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
	}

	/**
	 * <code>
	 *   <http security="none" pattern="/resources/**"/>
	 *   <http pattern="/api/**">
	 *     <intercept-url pattern="/api/admin/**" access="hasRole('ROLE_ADMIN')"/>
	 *     <intercept-url pattern="/api/**" access="hasRole('ROLE_USER')"/>
	 *     <http-basic />
	 *   </http>
	 *   <http>
	 *     <intercept-url pattern="/logout" access="permitAll"/>
	 *     <intercept-url pattern="/login" access="permitAll"/>
	 *     <intercept-url pattern="/signup" access="permitAll"/>
	 *     <intercept-url pattern="/about" access="permitAll"/>
	 *     <intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
	 *     <logout
	 *         logout-success-url="/login?logout"
	 *         logout-url="/logout"
	 *     <form-login
	 *         authentication-failure-url="/login?error"
	 *         login-page="/login"
	 *         login-processing-url="/login" <!-- but only POST -->
	 *         password-parameter="password"
	 *         username-parameter="username"
	 *     />
	 *   </http>
	 *   <authentication-manager>
	 *     <authentication-provider>
	 *       <user-service>
	 *         <user username="user" password="password" authorities="ROLE_USER"/>
	 *         <user username="admin" password="password" authorities="ROLE_USER,ROLE_ADMIN"/>
	 *       </user-service>
	 *     </authentication-provider>
	 *   </authentication-manager>
	 * </code>
	 * @author Rob Winch
	 */
	@EnableWebSecurity
	public static class SampleMultiHttpSecurityConfig {
		@Autowired
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth
				.inMemoryAuthentication()
					.withUser(PasswordEncodedUser.user())
					.withUser(PasswordEncodedUser.admin());
		}

		@Configuration
		@Order(1)
		public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
			protected void configure(HttpSecurity http) throws Exception {
				http
					.antMatcher("/api/**")
						.authorizeRequests()
							.antMatchers("/api/admin/**").hasRole("ADMIN")
							.antMatchers("/api/**").hasRole("USER")
							.and()
						.httpBasic();
			}
		}

		@Configuration
		public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
			@Override
			public void configure(WebSecurity web) throws Exception {
				web
					.ignoring()
						.antMatchers("/resources/**");
			}

			@Override
			protected void configure(HttpSecurity http) throws Exception {
				http
					.authorizeRequests()
						.antMatchers("/signup", "/about").permitAll()
						.anyRequest().hasRole("USER")
						.and()
					.formLogin()
						.loginPage("/login")
							.permitAll();
			}
		}
	}
}
