// Copyright © 2015 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.core.service; import static fi.hsl.parkandride.core.domain.Permission.ALL_OPERATORS; import static fi.hsl.parkandride.core.domain.Permission.FACILITY_UTILIZATION_UPDATE; import static fi.hsl.parkandride.core.domain.Permission.FACILITY_UPDATE; import static fi.hsl.parkandride.core.domain.Permission.HUB_UPDATE; import static fi.hsl.parkandride.core.domain.Permission.OPERATOR_CREATE; import static fi.hsl.parkandride.core.domain.Role.ADMIN; import static fi.hsl.parkandride.core.domain.Role.OPERATOR; import static fi.hsl.parkandride.core.domain.Role.OPERATOR_API; import static fi.hsl.parkandride.core.service.AuthenticationService.*; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.regex.Matcher; import org.apache.commons.lang3.StringUtils; import org.hamcrest.core.IsNull; import org.jasypt.util.password.PasswordEncryptor; import org.joda.time.DateTime; import org.joda.time.DateTimeUtils; import org.joda.time.Period; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import fi.hsl.parkandride.core.back.UserRepository; import fi.hsl.parkandride.core.domain.*; public class AuthenticationServiceTest { @Rule public ExpectedException thrown = ExpectedException.none(); @Mock UserRepository userRepository; @Mock PasswordEncryptor passwordEncryptor; private AuthenticationService service; private final static String secret = StringUtils.repeat('x', AuthenticationService.SECRET_MIN_LENGTH); private final static int EXPIRES_IN_SECONDS = 30; private final static int PASSWORD_EXPIRES_IN_DAYS = 60; private final static int PASSWORD_REMINDER_IN_DAYS = 14; private final static Period expires = Period.seconds(EXPIRES_IN_SECONDS); private final static Period passwordExpires = Period.days(PASSWORD_EXPIRES_IN_DAYS); private final static Period passwordReminder = Period.days(PASSWORD_REMINDER_IN_DAYS); private final UserSecret adminUser = new UserSecret(1l, "admin", "admin-password", ADMIN); private final UserSecret apiUser = new UserSecret(2l, "api", null, OPERATOR_API); @Before public void initMocks() { MockitoAnnotations.initMocks(this); when(userRepository.getCurrentTime()).thenReturn(DateTime.now()); this.service = new AuthenticationService(userRepository, passwordEncryptor, secret, expires, passwordExpires, passwordReminder); } @After public void resetTime() { DateTimeUtils.setCurrentMillisSystem(); } @Test public void requires_a_long_enough_shared_secret() { String tooShortSecret = StringUtils.repeat('x', AuthenticationService.SECRET_MIN_LENGTH - 1); thrown.expect(IllegalArgumentException.class); thrown.expectMessage("secret must be at least 32 characters long, but it was only 31"); new AuthenticationService(userRepository, passwordEncryptor, tooShortSecret, expires, passwordExpires, passwordReminder); } @Test public void token_pattern_regexp() { Matcher matcher = TOKEN_PATTERN.matcher("T|123|456|xyz"); assertThat(matcher.matches()).isTrue(); assertThat(matcher.group("message")).isEqualTo("T|123|456|"); assertThat(matcher.group("type")).isEqualTo("T"); assertThat(matcher.group("userId")).isEqualTo("123"); assertThat(matcher.group("timestamp")).isEqualTo("456"); assertThat(matcher.group("hmac")).isEqualTo("xyz"); } @Test(expected = ValidationException.class) public void login_with_bad_username() { when(userRepository.getUser("username")).thenThrow(NotFoundException.class); service.login("username", "whatever"); } @Test(expected = ValidationException.class) public void login_with_api_role_should_not_be_allowed() { when(userRepository.getUser("api")).thenReturn(apiUser); service.login("api", "whatever"); } @Test(expected = ValidationException.class) public void login_with_bac_credentials() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(false); service.login("admin", "admin-password"); } @Test public void login_and_authenticate() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(true); Login login = service.login("admin", "admin-password"); assertThat(login.token).startsWith("T"); assertThat(login.username).isEqualTo("admin"); assertThat(login.role).isEqualTo(ADMIN); when(userRepository.getUser(1l)).thenReturn(adminUser); User user = service.authenticate(login.token); assertThat(user).isSameAs(adminUser.user); } @Test(expected = AuthenticationRequiredException.class) public void expired_token() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(true); Login login = service.login("admin", "admin-password"); DateTimeUtils.setCurrentMillisOffset(EXPIRES_IN_SECONDS * 1001); service.authenticate(login.token); } @Test(expected = AuthenticationRequiredException.class) public void missing_token() { service.authenticate(null); } @Test(expected = AuthenticationRequiredException.class) public void invalid_token_format() { service.authenticate("T:123:456:xyz"); } @Test(expected = AuthenticationRequiredException.class) public void invalid_hmac() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(true); Login login = service.login("admin", "admin-password"); service.authenticate("P" + login.token.substring(1)); } @Test(expected = AuthenticationRequiredException.class) public void token_type_mismatch() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(true); Login login = service.login("admin", "admin-password"); when(userRepository.getUser(1l)).thenReturn(apiUser); service.authenticate(login.token); } @Test(expected = AuthenticationRequiredException.class) public void revoked_token() { when(userRepository.getUser("admin")).thenReturn(adminUser); when(passwordEncryptor.checkPassword("admin-password", "admin-password")).thenReturn(true); Login login = service.login("admin", "admin-password"); adminUser.minTokenTimestamp = new DateTime().plus(1); when(userRepository.getUser(1l)).thenReturn(adminUser); service.authenticate(login.token); } @Test public void authorize_admin_non_contextual_permission() { User user = new User(1l, "admin", ADMIN); authorize(user, ALL_OPERATORS); authorize(user, HUB_UPDATE); } @Test public void authorize_all_operators() { User user = new User(1l, "admin", ADMIN); authorize(user, () -> 5l, FACILITY_UPDATE); } @Test(expected = AccessDeniedException.class) public void authorize_operator_mismatch() { User user = new User(1l, "operator", OPERATOR); user.operatorId = 1l; authorize(user, () -> 5l, FACILITY_UPDATE); } @Test(expected = AccessDeniedException.class) public void authorize_required_operator_missing() { User user = new User(1l, "operator", OPERATOR); user.operatorId = null; authorize(user, () -> 5l, FACILITY_UPDATE); } @Test(expected = AccessDeniedException.class) public void authorize_admin_exclusion() { User user = new User(1l, "admin", ADMIN); authorize(user, () -> 1l, FACILITY_UTILIZATION_UPDATE); } @Test public void authorize_operator() { User user = new User(1l, "operator", OPERATOR); user.operatorId = 1l; authorize(user, () -> 1l, FACILITY_UPDATE); } @Test(expected = IllegalArgumentException.class) public void authorize_missing_operator_parameter() { User user = new User(1l, "operator", OPERATOR); user.operatorId = 1l; authorize(user, FACILITY_UPDATE); } @Test(expected = IllegalArgumentException.class) public void authorize_unnecessary_operator_parameter() { User user = new User(1l, "operator", OPERATOR); user.operatorId = 1l; authorize(user, () -> 1l, OPERATOR_CREATE); } @Test public void perpetual_token_doesnt_expire() { when(userRepository.getUser(2l)).thenReturn(apiUser); DateTime now = DateTime.now(); when(userRepository.getCurrentTime()).thenReturn(now); String token = service.resetToken(2l); verify(userRepository).revokeTokens(2l, now); DateTimeUtils.setCurrentMillisOffset(now.plusYears(1).getMillis()); User user = service.authenticate(token); assertThat(user).isSameAs(apiUser.user); } @Test public void admin_limited_operator() { User user = new User(1l, "admin", ADMIN); assertThat(getLimitedOperatorId(user)).isNull(); } @Test public void operator_limited_operator() { User user = new User(1l, "operator", OPERATOR); user.operatorId = 42l; assertThat(getLimitedOperatorId(user)).isEqualTo(42L); } @Test(expected = AccessDeniedException.class) public void operator_operator_mismatch() { User user = new User(1l, "operator", OPERATOR); getLimitedOperatorId(user); } }