/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * 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.authentication.jaas;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.security.Security;
import java.util.*;

import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionDestroyedEvent;

/**
 * Tests for the JaasAuthenticationProvider
 *
 * @author Ray Krueger
 */
public class JaasAuthenticationProviderTests {
	// ~ Instance fields
	// ================================================================================================

	private ApplicationContext context;
	private JaasAuthenticationProvider jaasProvider;
	private JaasEventCheck eventCheck;

	// ~ Methods
	// ========================================================================================================

	@Before
	public void setUp() throws Exception {
		String resName = "/" + getClass().getName().replace('.', '/') + ".xml";
		context = new ClassPathXmlApplicationContext(resName);
		eventCheck = (JaasEventCheck) context.getBean("eventCheck");
		jaasProvider = (JaasAuthenticationProvider) context
				.getBean("jaasAuthenticationProvider");
	}

	@Test
	public void testBadPassword() {
		try {
			jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("user",
					"asdf"));
			fail("LoginException should have been thrown for the bad password");
		}
		catch (AuthenticationException e) {
		}

		assertThat(eventCheck.failedEvent).as("Failure event not fired").isNotNull();
		assertThat(eventCheck.failedEvent.getException()).withFailMessage("Failure event exception was null").isNotNull();
		assertThat(eventCheck.successEvent).as("Success event was fired").isNull();
	}

	@Test
	public void testBadUser() {
		try {
			jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("asdf",
					"password"));
			fail("LoginException should have been thrown for the bad user");
		}
		catch (AuthenticationException e) {
		}

		assertThat(eventCheck.failedEvent).as("Failure event not fired").isNotNull();
		assertThat(eventCheck.failedEvent.getException()).withFailMessage("Failure event exception was null").isNotNull();
		assertThat(eventCheck.successEvent).as("Success event was fired").isNull();
	}

	@Test
	public void testConfigurationLoop() throws Exception {
		String resName = "/" + getClass().getName().replace('.', '/') + ".conf";
		URL url = getClass().getResource(resName);

		Security.setProperty("login.config.url.1", url.toString());

		setUp();
		testFull();
	}

	@Test
	public void detectsMissingLoginConfig() throws Exception {
		JaasAuthenticationProvider myJaasProvider = new JaasAuthenticationProvider();
		myJaasProvider.setApplicationEventPublisher(context);
		myJaasProvider.setAuthorityGranters(jaasProvider.getAuthorityGranters());
		myJaasProvider.setCallbackHandlers(jaasProvider.getCallbackHandlers());
		myJaasProvider.setLoginContextName(jaasProvider.getLoginContextName());

		try {
			myJaasProvider.afterPropertiesSet();
			fail("Should have thrown ApplicationContextException");
		}
		catch (IllegalArgumentException expected) {
			assertThat(expected.getMessage().startsWith("loginConfig must be set on")).isTrue();
		}
	}

	// SEC-1239
	@Test
	public void spacesInLoginConfigPathAreAccepted() throws Exception {
		File configFile;
		// Create temp directory with a space in the name
		File configDir = new File(System.getProperty("java.io.tmpdir") + File.separator
				+ "jaas test");
		configDir.deleteOnExit();

		if (configDir.exists()) {
			configDir.delete();
		}
		configDir.mkdir();
		configFile = File.createTempFile("login", "conf", configDir);
		configFile.deleteOnExit();
		FileOutputStream fos = new FileOutputStream(configFile);
		PrintWriter pw = new PrintWriter(fos);
		pw.append("JAASTestBlah {"
				+ "org.springframework.security.authentication.jaas.TestLoginModule required;"
				+ "};");
		pw.flush();
		pw.close();

		JaasAuthenticationProvider myJaasProvider = new JaasAuthenticationProvider();
		myJaasProvider.setApplicationEventPublisher(context);
		myJaasProvider.setLoginConfig(new FileSystemResource(configFile));
		myJaasProvider.setAuthorityGranters(jaasProvider.getAuthorityGranters());
		myJaasProvider.setCallbackHandlers(jaasProvider.getCallbackHandlers());
		myJaasProvider.setLoginContextName(jaasProvider.getLoginContextName());

		myJaasProvider.afterPropertiesSet();
	}

	@Test
	public void detectsMissingLoginContextName() throws Exception {
		JaasAuthenticationProvider myJaasProvider = new JaasAuthenticationProvider();
		myJaasProvider.setApplicationEventPublisher(context);
		myJaasProvider.setAuthorityGranters(jaasProvider.getAuthorityGranters());
		myJaasProvider.setCallbackHandlers(jaasProvider.getCallbackHandlers());
		myJaasProvider.setLoginConfig(jaasProvider.getLoginConfig());
		myJaasProvider.setLoginContextName(null);

		try {
			myJaasProvider.afterPropertiesSet();
			fail("Should have thrown IllegalArgumentException");
		}
		catch (IllegalArgumentException expected) {
			assertThat(expected.getMessage()).startsWith("loginContextName must be set on");
		}

		myJaasProvider.setLoginContextName("");

		try {
			myJaasProvider.afterPropertiesSet();
			fail("Should have thrown IllegalArgumentException");
		}
		catch (IllegalArgumentException expected) {
			assertThat(expected.getMessage().startsWith("loginContextName must be set on"));
		}
	}

	@Test
	public void testFull() throws Exception {
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
				"user", "password", AuthorityUtils.createAuthorityList("ROLE_ONE"));

		assertThat(jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();

		Authentication auth = jaasProvider.authenticate(token);

		assertThat(jaasProvider.getAuthorityGranters()).isNotNull();
		assertThat(jaasProvider.getCallbackHandlers()).isNotNull();
		assertThat(jaasProvider.getLoginConfig()).isNotNull();
		assertThat(jaasProvider.getLoginContextName()).isNotNull();

		Collection<? extends GrantedAuthority> list = auth.getAuthorities();
		Set<String> set = AuthorityUtils.authorityListToSet(list);

		assertThat(set.contains("ROLE_ONE")).withFailMessage("GrantedAuthorities should not contain ROLE_ONE").isFalse();
		assertThat(set.contains("ROLE_TEST1")).withFailMessage("GrantedAuthorities should contain ROLE_TEST1").isTrue();
		assertThat(set.contains("ROLE_TEST2")).withFailMessage("GrantedAuthorities should contain ROLE_TEST2").isTrue();
		boolean foundit = false;

		for (GrantedAuthority a : list) {
			if (a instanceof JaasGrantedAuthority) {
				JaasGrantedAuthority grant = (JaasGrantedAuthority) a;
				assertThat(grant.getPrincipal()).withFailMessage("Principal was null on JaasGrantedAuthority").isNotNull();
				foundit = true;
			}
		}

		assertThat(foundit).as("Could not find a JaasGrantedAuthority").isTrue();

		assertThat(eventCheck.successEvent).as("Success event should be fired").isNotNull();
		assertThat(eventCheck.successEvent.getAuthentication()).withFailMessage("Auth objects should be equal").isEqualTo(auth);
		assertThat(eventCheck.failedEvent).as("Failure event should not be fired").isNull();
	}

	@Test
	public void testGetApplicationEventPublisher() throws Exception {
		assertThat(jaasProvider.getApplicationEventPublisher()).isNotNull();
	}

	@Test
	public void testLoginExceptionResolver() {
		assertThat(jaasProvider.getLoginExceptionResolver()).isNotNull();
		jaasProvider.setLoginExceptionResolver(new LoginExceptionResolver() {
			public AuthenticationException resolveException(LoginException e) {
				return new LockedException("This is just a test!");
			}
		});

		try {
			jaasProvider.authenticate(new UsernamePasswordAuthenticationToken("user",
					"password"));
		}
		catch (LockedException e) {
		}
		catch (Exception e) {
			fail("LockedException should have been thrown and caught");
		}
	}

	@Test
	public void testLogout() throws Exception {
		MockLoginContext loginContext = new MockLoginContext(
				jaasProvider.getLoginContextName());

		JaasAuthenticationToken token = new JaasAuthenticationToken(null, null,
				loginContext);

		SecurityContext context = SecurityContextHolder.createEmptyContext();
		context.setAuthentication(token);

		SessionDestroyedEvent event = mock(SessionDestroyedEvent.class);
		when(event.getSecurityContexts()).thenReturn(Arrays.asList(context));

		jaasProvider.handleLogout(event);

		assertThat(loginContext.loggedOut).isTrue();
	}

	@Test
	public void testNullDefaultAuthorities() {
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
				"user", "password");

		assertThat(jaasProvider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();

		Authentication auth = jaasProvider.authenticate(token);
		assertThat(auth
				.getAuthorities()).withFailMessage("Only ROLE_TEST1 and ROLE_TEST2 should have been returned").hasSize(2);
	}

	@Test
	public void testUnsupportedAuthenticationObjectReturnsNull() {
		assertThat(jaasProvider.authenticate(new TestingAuthenticationToken("foo", "bar",
				AuthorityUtils.NO_AUTHORITIES))).isNull();
	}

	// ~ Inner Classes
	// ==================================================================================================

	private static class MockLoginContext extends LoginContext {
		boolean loggedOut = false;

		public MockLoginContext(String loginModule) throws LoginException {
			super(loginModule);
		}

		public void logout() throws LoginException {
			this.loggedOut = true;
		}
	}
}
