/* * Copyright (C) 2007-2014 Crafter Software Corporation. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.craftercms.profile.services.impl; import java.util.Calendar; import java.util.Date; import java.util.UUID; import org.craftercms.commons.crypto.CipherUtils; import org.craftercms.commons.i10n.I10nLogger; import org.craftercms.commons.logging.Logged; import org.craftercms.commons.mongo.MongoDataException; import org.craftercms.commons.security.exception.ActionDeniedException; import org.craftercms.commons.security.permissions.PermissionEvaluator; import org.craftercms.profile.api.AccessToken; import org.craftercms.profile.api.PersistentLogin; import org.craftercms.profile.api.Profile; import org.craftercms.profile.api.ProfileConstants; import org.craftercms.profile.api.TenantAction; import org.craftercms.profile.api.Ticket; import org.craftercms.profile.api.exceptions.I10nProfileException; import org.craftercms.profile.api.exceptions.ProfileException; import org.craftercms.profile.api.services.AuthenticationService; import org.craftercms.profile.api.services.ProfileService; import org.craftercms.profile.exceptions.BadCredentialsException; import org.craftercms.profile.exceptions.DisabledProfileException; import org.craftercms.profile.exceptions.NoSuchPersistentLoginException; import org.craftercms.profile.exceptions.NoSuchProfileException; import org.craftercms.profile.exceptions.ProfileLockedException; import org.craftercms.profile.repositories.PersistentLoginRepository; import org.craftercms.profile.repositories.TicketRepository; import org.springframework.beans.factory.annotation.Required; /** * Default implementation of {@link org.craftercms.profile.api.services.AuthenticationService}. * * @author avasquez */ @Logged public class AuthenticationServiceImpl implements AuthenticationService { private static final I10nLogger logger = new I10nLogger(AuthenticationServiceImpl.class, "crafter.profile.messages.logging"); public static final String LOG_KEY_AUTHENTICATION_SUCCESSFUL = "profile.auth.authenticationSuccessful"; public static final String LOG_KEY_TICKET_CREATED = "profile.auth.ticketCreated"; public static final String LOG_KEY_TICKET_REQUESTED = "profile.auth.ticketRequested"; public static final String LOG_KEY_TICKET_INVALIDATED = "profile.auth.tickedInvalidated"; public static final String LOG_KEY_PERSISTENT_LOGIN_CREATED = "profile.auth.persistentLoginCreated"; public static final String LOG_KEY_PERSISTENT_LOGIN_TOKEN_REFRESHED = "profile.auth.persistentLoginTokenRefreshed"; public static final String LOG_KEY_PERSISTENT_LOGIN_DELETED = "profile.auth.persistentLoginDeleted"; public static final String ERROR_KEY_CREATE_TICKET_ERROR = "profile.auth.createTicketError"; public static final String ERROR_KEY_GET_TICKET_ERROR = "profile.auth.getTicketError"; public static final String ERROR_KEY_UPDATE_TICKET_ERROR = "profile.auth.updateTicketError"; public static final String ERROR_KEY_DELETE_TICKET_ERROR = "profile.auth.deleteTicketError"; public static final String ERROR_KEY_CREATE_PERSISTENT_LOGIN_ERROR = "profile.auth.createdPersistentLoginError"; public static final String ERROR_KEY_GET_PERSISTENT_LOGIN_ERROR = "profile.auth.getPersistentLoginError"; public static final String ERROR_KEY_UPDATE_PERSISTENT_LOGIN_ERROR = "profile.auth.updatePersistentLoginError"; public static final String ERROR_KEY_DELETE_PERSISTENT_LOGIN_ERROR = "profile.auth.deletePersistentLoginError"; public static final String ERROR_KEY_WAIT_IS_ABORTED = "profile.auth.delayWasInterupt"; protected PermissionEvaluator<AccessToken, String> permissionEvaluator; protected TicketRepository ticketRepository; protected PersistentLoginRepository persistentLoginRepository; protected ProfileService profileService; protected int lockTime; protected int failedLoginAttemptsBeforeLock; protected int failedLoginAttemptsBeforeDelay; @Required public void setPermissionEvaluator(PermissionEvaluator<AccessToken, String> permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } @Required public void setTicketRepository(TicketRepository ticketRepository) { this.ticketRepository = ticketRepository; } @Required public void setPersistentLoginRepository(PersistentLoginRepository persistentLoginRepository) { this.persistentLoginRepository = persistentLoginRepository; } @Required public void setProfileService(ProfileService profileService) { this.profileService = profileService; } @Override public Ticket authenticate(String tenantName, String username, String password) throws ProfileException { checkIfManageTicketsIsAllowed(tenantName); Profile profile = profileService.getProfileByUsername(tenantName, username, ProfileConstants.NO_ATTRIBUTE); if (profile == null) { // Invalid username throw new BadCredentialsException(); } if (!profile.isEnabled()) { throw new DisabledProfileException(profile.getId().toString(), tenantName); } if (isProfileInTimeOut(profile)) { throw new ProfileLockedException(); } try { if (!CipherUtils.matchPassword(profile.getPassword(), password)) { // Invalid password countAsFail(profile); throw new BadCredentialsException(); } clearAllLoginAttempts(profile); Ticket ticket = new Ticket(); ticket.setId(UUID.randomUUID().toString()); ticket.setTenant(tenantName); ticket.setProfileId(profile.getId().toString()); ticket.setLastRequestTime(new Date()); ticketRepository.insert(ticket); logger.debug(LOG_KEY_AUTHENTICATION_SUCCESSFUL, profile.getId(), ticket); return ticket; } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_CREATE_TICKET_ERROR, profile.getId()); } } private void clearAllLoginAttempts(final Profile profile) throws ProfileException { profile.setLastFailedLogin(new Date(0)); profile.setFailedLoginAttempts(0); profileService.setFailedLoginAttempts(profile.getId().toString(), 0, ProfileConstants.NO_ATTRIBUTE); } protected void countAsFail(final Profile profile) throws ProfileException { profile.increaseFailedLoginAttempts(); // This one counts !!! if ((failedLoginAttemptsBeforeDelay + failedLoginAttemptsBeforeLock) <= profile.getFailedLoginAttempts()) { profileService.setLastFailedLogin(profile.getId().toString(), new Date(), ProfileConstants.NO_ATTRIBUTE); } profileService.setFailedLoginAttempts(profile.getId().toString(), profile.getFailedLoginAttempts(), ProfileConstants.NO_ATTRIBUTE); if (profile.getFailedLoginAttempts() > failedLoginAttemptsBeforeDelay) { try { Thread.sleep(1000); } catch (InterruptedException e) { logger.debug(ERROR_KEY_WAIT_IS_ABORTED); } } } protected boolean isProfileInTimeOut(final Profile profile) { if (profile.getLastFailedLogin() == null || profile.getFailedLoginAttempts() <= 0) { return false; } Calendar calendar = Calendar.getInstance(); calendar.setTime(profile.getLastFailedLogin()); calendar.add(Calendar.MINUTE, lockTime); return new Date().before(calendar.getTime()); } @Override public Ticket createTicket(String profileId) throws ProfileException { Profile profile = profileService.getProfile(profileId, ProfileConstants.NO_ATTRIBUTE); if (profile != null) { String tenantName = profile.getTenant(); checkIfManageTicketsIsAllowed(tenantName); if (!profile.isEnabled()) { throw new DisabledProfileException(profile.getId().toString(), tenantName); } try { Ticket ticket = new Ticket(); ticket.setId(UUID.randomUUID().toString()); ticket.setTenant(tenantName); ticket.setProfileId(profile.getId().toString()); ticket.setLastRequestTime(new Date()); ticketRepository.insert(ticket); logger.debug(LOG_KEY_TICKET_CREATED, profile.getId(), ticket); return ticket; } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_CREATE_TICKET_ERROR, profile.getId()); } } else { throw new NoSuchProfileException(profileId); } } @Override public Ticket getTicket(String ticketId) throws ProfileException { Ticket ticket; try { ticket = ticketRepository.findByStringId(ticketId); } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_GET_TICKET_ERROR, e, ticketId); } if (ticket != null) { checkIfManageTicketsIsAllowed(ticket.getTenant()); ticket.setLastRequestTime(new Date()); try { ticketRepository.save(ticket); } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_UPDATE_TICKET_ERROR, ticketId); } logger.debug(LOG_KEY_TICKET_REQUESTED, ticketId); return ticket; } return null; } @Override public void invalidateTicket(String ticketId) throws ProfileException { try { Ticket ticket = ticketRepository.findByStringId(ticketId); if (ticket != null) { checkIfManageTicketsIsAllowed(ticket.getTenant()); ticketRepository.removeByStringId(ticketId); logger.debug(LOG_KEY_TICKET_INVALIDATED, ticketId); } } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_DELETE_TICKET_ERROR, ticketId); } } @Override public PersistentLogin createPersistentLogin(String profileId) throws ProfileException { Profile profile = profileService.getProfile(profileId, ProfileConstants.NO_ATTRIBUTE); if (profile != null) { String tenantName = profile.getTenant(); checkIfManageTicketsIsAllowed(tenantName); if (!profile.isEnabled()) { throw new DisabledProfileException(profile.getId().toString(), tenantName); } try { PersistentLogin login = new PersistentLogin(); login.setId(UUID.randomUUID().toString()); login.setTenant(tenantName); login.setProfileId(profileId); login.setToken(UUID.randomUUID().toString()); login.setTimestamp(new Date()); persistentLoginRepository.insert(login); logger.debug(LOG_KEY_PERSISTENT_LOGIN_CREATED, profile.getId(), login); return login; } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_CREATE_PERSISTENT_LOGIN_ERROR, profile.getId()); } } else { throw new NoSuchProfileException(profileId); } } @Override public PersistentLogin getPersistentLogin(String loginId) throws ProfileException { try { PersistentLogin login = persistentLoginRepository.findByStringId(loginId); if (login != null) { checkIfManageTicketsIsAllowed(login.getTenant()); } return login; } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_GET_PERSISTENT_LOGIN_ERROR, e, loginId); } } @Override public PersistentLogin refreshPersistentLoginToken(String loginId) throws ProfileException { PersistentLogin login = getPersistentLogin(loginId); if (login != null) { try { login.setToken(UUID.randomUUID().toString()); persistentLoginRepository.save(login); logger.debug(LOG_KEY_PERSISTENT_LOGIN_TOKEN_REFRESHED, loginId, login.getToken()); return login; } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_UPDATE_PERSISTENT_LOGIN_ERROR, loginId); } } else { throw new NoSuchPersistentLoginException(loginId); } } @Override public void deletePersistentLogin(String loginId) throws ProfileException { try { PersistentLogin login = persistentLoginRepository.findByStringId(loginId); if (login != null) { checkIfManageTicketsIsAllowed(login.getTenant()); persistentLoginRepository.removeByStringId(loginId); logger.debug(LOG_KEY_PERSISTENT_LOGIN_DELETED, loginId); } } catch (MongoDataException e) { throw new I10nProfileException(ERROR_KEY_DELETE_PERSISTENT_LOGIN_ERROR, loginId); } } protected void checkIfManageTicketsIsAllowed(String tenantName) { if (!permissionEvaluator.isAllowed(tenantName, TenantAction.MANAGE_TICKETS.toString())) { throw new ActionDeniedException(TenantAction.MANAGE_TICKETS.toString(), "tenant \"" + tenantName + "\""); } } public void setLockTime(final int lockTime) { this.lockTime = lockTime; } public void setFailedLoginAttemptsBeforeLock(int failedLoginAttemptsBeforeLock) { this.failedLoginAttemptsBeforeLock = failedLoginAttemptsBeforeLock; } public void setFailedLoginAttemptsBeforeDelay(int failedLoginAttemptsBeforeDelay) { this.failedLoginAttemptsBeforeDelay = failedLoginAttemptsBeforeDelay; } }