/*
* Copyright 2011-2014 Eric F. Savage, code@efsavage.com
*
* 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 com.ajah.user.login.data;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ajah.crypto.Crypto;
import com.ajah.crypto.CryptoException;
import com.ajah.crypto.HmacSha1Password;
import com.ajah.crypto.Password;
import com.ajah.lang.ConfigException;
import com.ajah.spring.jdbc.DataOperationResult;
import com.ajah.spring.jdbc.err.DataOperationException;
import com.ajah.user.AuthenticationFailureException;
import com.ajah.user.User;
import com.ajah.user.UserId;
import com.ajah.user.UserNotFoundException;
import com.ajah.user.UserStatus;
import com.ajah.user.data.UserManager;
import com.ajah.user.login.LogIn;
import com.ajah.user.login.LogInId;
import com.ajah.user.login.LogInSource;
import com.ajah.user.login.LogInStatus;
import com.ajah.user.login.LogInType;
/**
* Manages data operations for {@link LogIn}.
*
* @author Eric F. Savage <code@efsavage.com>
*
*/
@Service
@Log
public class LogInManager {
/**
* Returns token value for a user, that can be used to authenticate later.
* The actual scheme of token generation should be encapsulated fully here
* so it can be changed. Currently the only available scheme is to encrypt
* the username and password hash.
*
* Note: Tokens should always expire when a user changes their password.
*
* @param user
* The user to generate a token for.
* @param password
* Password object for user to include in the token.
* @return Token that can be used to authenticate.
* @throws ConfigException
* If there is a cryptographic error.
*/
public static String getTokenValue(final User user, final Password password) {
try {
return Crypto.toAES(user.getUsername() + "|" + password.toString());
} catch (final CryptoException e) {
throw new ConfigException(e);
}
}
@Autowired
private LogInDao logInDao;
@Autowired
private UserManager userManager;
/**
* Returns a count of all records.
*
* @return Count of all records.
* @throws DataOperationException
* If the query could not be executed.
*/
public long count() throws DataOperationException {
return count(null, null);
}
/**
* Counts the records available that match the criteria.
*
* @param type
* The logIn type to limit to, optional.
* @param status
* The status to limit to, optional.
* @return The number of matching records.
* @throws DataOperationException
* If the query could not be executed.
*/
public long count(final LogInType type, final LogInStatus status) throws DataOperationException {
return this.logInDao.count(type, status);
}
/**
* Creates a new {@link LogIn} with the given properties.
*
* @param name
* The name of the logIn, required.
* @param type
* The type of logIn, required.
* @param status
* The status of the logIn, required.
* @return The result of the creation, which will include the new logIn at
* {@link DataOperationResult#getEntity()}.
* @throws DataOperationException
* If the query could not be executed.
*/
public DataOperationResult<LogIn> create(final String name, final LogInType type, final LogInStatus status) throws DataOperationException {
final LogIn logIn = new LogIn();
logIn.setUsername(name);
logIn.setType(type);
logIn.setStatus(status);
final DataOperationResult<LogIn> result = save(logIn);
return result;
}
/**
* Returns a list of {@link LogIn}s that match the specified criteria.
*
* @param type
* The type of logIn, optional.
* @param status
* The status of the logIn, optional.
* @param page
* The page of results to fetch.
* @param count
* The number of results per page.
* @return A list of {@link LogIn}s, which may be empty.
* @throws DataOperationException
* If the query could not be executed.
*/
public List<LogIn> list(final LogInType type, final LogInStatus status, final long page, final long count) throws DataOperationException {
return this.logInDao.list(type, status, page, count);
}
/**
* Loads an {@link LogIn} by it's ID.
*
* @param logInId
* The ID to load, required.
* @return The matching logIn, if found. Will not return null.
* @throws DataOperationException
* If the query could not be executed.
* @throws LogInNotFoundException
* If the ID specified did not match any logIns.
*/
public LogIn load(final LogInId logInId) throws DataOperationException, LogInNotFoundException {
final LogIn logIn = this.logInDao.load(logInId);
if (logIn == null) {
throw new LogInNotFoundException(logInId);
}
return logIn;
}
/**
* Returns a login record for a user.
*
* @param username
* Username or email of the user logging in.
* @param password
* Password of the user logging in, unencrypted.
* @param ip
* IP of requesting user
* @param source
* Source of login attempt
* @param type
* Type of login attempt
* @return Login record, will never return null.
* @throws DataOperationException
* If the query could not be executed.
*/
public LogIn login(final String username, final Password password, final String ip, final LogInSource source, final LogInType type) throws DataOperationException {
log.fine("Login by user/pass attempt for: " + username);
final LogIn login = new LogIn();
login.setIp(ip);
login.setCreated(new Date());
login.setSource(source);
login.setType(type);
try {
final User user = this.userManager.getUser(username, password);
login.setUser(user);
login.setUsername(username);
login.setStatus(LogInStatus.SUCCESS);
log.fine("User " + user.getUsername() + " logged in");
} catch (final RuntimeException e) {
log.log(Level.SEVERE, e.getMessage(), e);
login.setStatus(LogInStatus.ABORT);
} catch (final AuthenticationFailureException e) {
log.log(Level.INFO, e.getMessage());
login.setUsername(e.getUsername());
login.setStatus(LogInStatus.FAIL);
} catch (final UserNotFoundException e) {
log.log(Level.INFO, e.getMessage());
login.setStatus(LogInStatus.FAIL);
}
try {
save(login);
} catch (DataOperationException e) {
// While this is a significant error, we don't want to fail the
// whole login process on the save.
log.log(Level.SEVERE, e.getMessage(), e);
}
return login;
}
/**
* Returns a login record for a user.
*
* @param userId
* The ID of the user attempting to log in.
* @param password
* Password of the user logging in, unencrypted.
* @param ip
* IP of requesting user
* @param source
* Source of login attempt
* @param type
* Type of login attempt
* @return Login record, will never return null.
* @throws DataOperationException
* If the query could not be executed.
*/
public LogIn login(final UserId userId, final Password password, final String ip, final LogInSource source, final LogInType type) throws DataOperationException {
log.fine("Login by user/pass attempt for: " + userId);
final LogIn login = new LogIn();
login.setIp(ip);
login.setCreated(new Date());
login.setSource(source);
login.setType(type);
try {
final User user = this.userManager.getUser(userId, password);
login.setUser(user);
login.setUsername(user.getUsername());
if (user.getStatus() == UserStatus.ACTIVE || user.getStatus() == UserStatus.NEW) {
login.setStatus(LogInStatus.SUCCESS);
} else {
log.warning("Failed login because user is status: " + user.getStatus());
login.setStatus(LogInStatus.FAIL);
}
log.fine("User " + user.getUsername() + " logged in");
} catch (final RuntimeException e) {
log.log(Level.SEVERE, e.getMessage(), e);
login.setStatus(LogInStatus.ABORT);
} catch (final AuthenticationFailureException e) {
log.log(Level.INFO, e.getMessage());
login.setUsername(e.getUsername());
login.setStatus(LogInStatus.FAIL);
} catch (final UserNotFoundException e) {
log.log(Level.INFO, e.getMessage());
login.setStatus(LogInStatus.FAIL);
}
try {
save(login);
} catch (DataOperationException e) {
// While this is a significant error, we don't want to fail the
// whole login process on the save.
log.log(Level.SEVERE, e.getMessage(), e);
}
return login;
}
/**
* Logs a user in by token value. Common usage would be to store token in a
* cookie.
*
* @param token
* Token value
* @param ip
* IP of requesting user
* @param source
* Source of login attempt
* @param type
* Type of login attempt
* @return Login record, will never be null.
* @throws DataOperationException
* If the query could not be executed.
*/
public LogIn loginByToken(final String token, final String ip, final LogInSource source, final LogInType type) throws DataOperationException {
log.fine("Login by token attempt for: " + token);
try {
final String decrypted = Crypto.fromAES(token);
log.fine("token contents: " + decrypted);
final String username = decrypted.split("\\|")[0];
final Password password = new HmacSha1Password(decrypted.split("\\|")[1], true);
final LogIn logIn = login(username, password, ip, source, type);
logIn.setToken(token);
return logIn;
} catch (final CryptoException e) {
log.log(Level.SEVERE, e.getMessage(), e);
final LogIn login = new LogIn();
login.setIp(ip);
login.setCreated(new Date());
login.setSource(source);
login.setStatus(LogInStatus.ERROR);
login.setType(type);
return login;
}
}
/**
* Saves an {@link LogIn}. Assigns a new ID ({@link UUID}) and sets the
* creation date if necessary. If either of these elements are set, will
* perform an insert. Otherwise will perform an update.
*
* @param logIn
* The logIn to save.
* @return The result of the save operation, which will include the new
* logIn at {@link DataOperationResult#getEntity()}.
* @throws DataOperationException
* If the query could not be executed.
*/
public DataOperationResult<LogIn> save(final LogIn logIn) throws DataOperationException {
boolean create = false;
if (logIn.getId() == null) {
logIn.setId(new LogInId(UUID.randomUUID().toString()));
create = true;
}
if (logIn.getCreated() == null) {
logIn.setCreated(new Date());
create = true;
}
if (create) {
final DataOperationResult<LogIn> result = this.logInDao.insert(logIn);
log.fine("Created LogIn " + logIn.getUsername() + " [" + logIn.getId() + "]");
return result;
}
final DataOperationResult<LogIn> result = this.logInDao.update(logIn);
if (result.getRowsAffected() > 0) {
log.fine("Updated LogIn " + logIn.getUsername() + " [" + logIn.getId() + "]");
}
return result;
}
/**
* Counts the records available that match the search criteria.
*
* @param search
* The search query.
* @return The number of matching records.
* @throws DataOperationException
* If the query could not be executed.
*/
public int searchCount(final String search) throws DataOperationException {
return this.logInDao.searchCount(search);
}
public void retry(LogIn logIn) throws DataOperationException {
logIn.setRetry(true);
save(logIn);
}
public String getUsernameByToken(String token) throws CryptoException {
final String decrypted = Crypto.fromAES(token);
log.fine("token contents: " + decrypted);
final String username = decrypted.split("\\|")[0];
return username;
}
}