package no.niths.services.auth;
import java.util.GregorianCalendar;
import java.util.List;
import no.niths.application.rest.auth.SessionParcel;
import no.niths.application.rest.exception.ExpiredTokenException;
import no.niths.application.rest.exception.ObjectInCollectionException;
import no.niths.application.rest.exception.ObjectNotFoundException;
import no.niths.application.rest.exception.UnvalidEmailException;
import no.niths.application.rest.exception.UnvalidTokenException;
import no.niths.common.constants.MiscConstants;
import no.niths.common.constants.SecurityConstants;
import no.niths.common.helpers.ValidationHelper;
import no.niths.domain.development.Application;
import no.niths.domain.development.Developer;
import no.niths.domain.school.Role;
import no.niths.domain.school.Student;
import no.niths.security.ApplicationToken;
import no.niths.security.DeveloperToken;
import no.niths.security.RequestHolderDetails;
import no.niths.security.SessionToken;
import no.niths.services.auth.interfaces.AuthenticationService;
import no.niths.services.auth.interfaces.GoogleAuthenticationService;
import no.niths.services.auth.interfaces.KeyGeneratorService;
import no.niths.services.auth.interfaces.TokenGeneratorService;
import no.niths.services.development.interfaces.ApplicationService;
import no.niths.services.development.interfaces.DeveloperService;
import no.niths.services.development.interfaces.RequestStatisticsService;
import no.niths.services.interfaces.MailSenderService;
import no.niths.services.school.interfaces.StudentService;
import org.apache.commons.validator.routines.EmailValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
/**
* Authenticates user trying to request a resource
* <p>
* This class delegates the request to the classes responsible for verifying
* tokens and fetching the belonging apps, developers and students
* </p>
*
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
private static final Logger logger = LoggerFactory
.getLogger(AuthenticationServiceImpl.class);
@Autowired
private StudentService studentService;
@Autowired
private DeveloperService developerService;
@Autowired
private GoogleAuthenticationService googleService;
@Autowired
private TokenGeneratorService tokenService;
@Autowired
private ApplicationService appService;
@Autowired
private MailSenderService mailService;
@Autowired
private KeyGeneratorService keyService;
@Autowired
private RequestStatisticsService reqService;
/**
* {@inheritDoc}
*/
@Override
public SessionParcel authenticateAtGoogle(String googleToken)
throws UnvalidEmailException {
// Authenticate user from Google,
// and then check to see if the email is valid
String userEmail = googleService.authenticateAndGetEmail(googleToken);
isUserValid(userEmail); // Verify email
// Get the matching student
// If no student exists, we persist
Student authenticatedStudent = getStudent(userEmail);
// Generate "session token" that the app will use from now on
String generatedToken = tokenService.generateToken(authenticatedStudent
.getId());
// Add the generated token to the student,
// and update last login time
Student temp = new Student();
temp.setId(authenticatedStudent.getId());
temp.setSessionToken(generatedToken);
temp.setLastLogon(getCurrentTime());
studentService.mergeUpdate(temp);
// Create a wrapper to give to the request holder
SessionToken sessionToken = new SessionToken();
sessionToken.setToken(generatedToken);
sessionToken.setStudentId(authenticatedStudent.getId());
SessionParcel sessionParcel = new SessionParcel(authenticatedStudent,
sessionToken);
return sessionParcel;
}
/**
* {@inheritDoc}
*/
@Override
public RequestHolderDetails authenticateSessionToken(String sessionToken)
throws AuthenticationException {
logger.debug("Will authenticate session-token: " + sessionToken);
// First check the format of the token
Long id = tokenService.verifyTokenFormat(sessionToken, true);
// Fetch student owning the session token
Student wantAccess = studentService.getById(id);
// Then we verify the last login time of the student
if (wantAccess == null || wantAccess.getSessionToken() == null) {
logger.debug("No student has that session-token");
throw new UnvalidTokenException(
"Token does not belong to a student");
}
if (!(wantAccess.getSessionToken().equals(sessionToken))
|| wantAccess.getLastLogon() == null) {
throw new UnvalidTokenException("Can not find last login");
}
verifyLastLogonTime(wantAccess.getLastLogon());
// The information added here is used in the @Security annotations
RequestHolderDetails authenticatedUser = new RequestHolderDetails(); // ROLE_ANONYMOUS
// -->
// Wrapper
authenticatedUser.setUserName(wantAccess.getEmail());
authenticatedUser.setStudentId(wantAccess.getId());
// Checking roles of student and adding them to User wrapper
List<Role> roles = wantAccess.getRoles();
if (!(roles.isEmpty())) {
String loggerText = "Student logging in has role(s): ";
for (Role role : roles) {
loggerText += role.getRoleName() + " ";
authenticatedUser.addRoleName(role.getRoleName());
}
logger.debug(loggerText);
}
// Update last login time
wantAccess.setLastLogon(getCurrentTime());
Student temp = new Student();
temp.setId(wantAccess.getId());
temp.setLastLogon(wantAccess.getLastLogon());
temp.setSessionToken(wantAccess.getSessionToken());
studentService.mergeUpdate(temp);
// studentService.update(wantAccess);
return authenticatedUser;
}
/**
* {@inheritDoc}
*/
@Override
public void logout(Long studentId) {
Student wantToLogout = studentService.getById(studentId);
ValidationHelper.isObjectNull(wantToLogout, Student.class);
wantToLogout.setSessionToken(null);
studentService.update(wantToLogout);
}
/**
* {@inheritDoc}
*/
@Override
public DeveloperToken registerDeveloper(Developer dev) {
// Verify developer email
isEmailValid(dev.getEmail());
// Passed checks! Generate a key and persist the developer
String developerKey = keyService.generateDeveloperKey();
dev.setDeveloperKey(developerKey);
developerService.create(dev);
logger.debug("Developer[" + dev.getId()
+ "] has been created and given key: " + dev.getDeveloperKey());
dev.setDeveloperToken(tokenService.generateToken(dev.getId()));
developerService.update(dev);
logger.debug("Developer[" + dev.getId()
+ "] has been updated and given token: "
+ dev.getDeveloperToken());
// Create response to the request holder
DeveloperToken devToken = new DeveloperToken();
devToken.setKey(developerKey);
devToken.setToken(dev.getDeveloperToken());
mailService.sendDeveloperRegistratedConfirmation(dev);
return devToken;
}
/**
* {@inheritDoc}
*/
@Override
public ApplicationToken registerApplication(Application app,
String developerKey) throws ObjectNotFoundException,
ObjectInCollectionException {
Developer dev = developerService
.getDeveloperByDeveloperKey(developerKey);
if (dev == null) {
throw new ObjectNotFoundException("No developer found");
}
ApplicationToken appToken = new ApplicationToken("No token");
String appKey = keyService.generateApplicationKey();
if (dev.getApps().contains(app)) {
throw new ObjectInCollectionException(
"App already added to developer");
}
appToken.setAppKey(appKey);
app.setApplicationKey(appKey);
dev.getApps().add(app);
developerService.update(dev);
mailService.sendDeveloperAddedAppConfirmation(dev, app);
return appToken;
}
/**
* Authenticates the developer token. Verifies the format of the token and
* and fetches matching student from DB. Then checks if developer token is
* correct
* <p>
*
* @param devToken
* the developer token
* @return Developer that has the token
* @throws AuthenticationException
* if no matching student is found
* </p>
*/
@Override
public Developer authenticateDeveloperToken(String devToken)
throws AuthenticationException {
Long id = tokenService.verifyTokenFormat(devToken, false);
Developer dev = developerService.getById(id);
if (dev == null) {
throw new UnvalidTokenException("No developer found for token/key");
} else if (dev.getEnabled() == null || !dev.getEnabled()) {
throw new UnvalidTokenException("Developer is not enabled");
} else if (dev.getDeveloperToken() == null
|| !(dev.getDeveloperToken().equals(devToken))) {
throw new UnvalidTokenException("Not a correct token");
}
return dev;
}
/**
* Authenticates the application token.
* <p>
* Verifies the token format and, fetches the matching application from DB,
* if it is enabled.
* <p>
*
* @param applicationToken
* the application token
* @return the application that has the token
* @throws AuthenticationException
* if no matching app is found
*/
@Override
public Application authenticateApplicationToken(String applicationToken)
throws AuthenticationException {
Long id = tokenService.verifyTokenFormat(applicationToken, false);
Application app = appService.getById(id);
// If else for specified error messages
if (app == null) {
throw new UnvalidTokenException(
"No app found or app is not enabled");
} else if (app.getApplicationToken() == null) {
throw new UnvalidTokenException("Application does not have a token");
} else if (!(app.getApplicationToken().equals(applicationToken))) {
throw new UnvalidTokenException("Application token is not correct");
} else if (app.getEnabled() == null || !app.getEnabled()) {
throw new UnvalidTokenException("Application not enabled");
}
// Register request for application statistics
reqService.registerRequest(app);
return app;
}
/**
* {@inheritDoc}
*/
@Override
public Developer enableDeveloper(String developerKey)
throws AuthenticationException {
logger.debug("Trying to enable developer with token: " + developerKey);
String[] keyParts = developerKey.split("-");
Developer dev = developerService
.getDeveloperByDeveloperKey(developerKey);
if (dev == null) {
throw new UnvalidTokenException("No developer found with that key");
} else if (dev.getEnabled() != null && dev.getEnabled()) {
throw new UnvalidTokenException("Developer is already enabled");
} else if (keyParts.length != 2) {
throw new UnvalidTokenException("Key is not valid");
}
try {
long issued = Long.parseLong(keyParts[1]);
long now = new GregorianCalendar().getTimeInMillis();
if (now - issued < 3600000) { // One hour
dev.setEnabled(true);
developerService.update(dev);
// Send confirmation email
mailService.sendDeveloperEnabledConfirmation(dev);
} else {
throw new UnvalidTokenException(
"You waited too long, get admin to enable your account");
}
} catch (NumberFormatException e) {
throw new UnvalidTokenException("No hax");
}
return dev;
}
/**
* {@inheritDoc}
*/
@Override
public Application enableApplication(String applicationKey)
throws AuthenticationException {
logger.debug("Trying to enable application with key: " + applicationKey);
Application app = appService.getByApplicationKey(applicationKey, false);
if (app == null) {
throw new UnvalidTokenException(
"No application found with that key");
}
if (app.getEnabled() != null && app.getEnabled()) {
throw new UnvalidTokenException("Application is already enabled");
}
// Generate a personal token and set app to enabled
app.setApplicationToken(tokenService.generateToken(app.getId()));
app.setEnabled(true);
appService.update(app);
logger.debug("Application enabled " + applicationKey);
// Send confirmation email
if (app.getDeveloper() != null) {
mailService.sendApplicationEnabledConfirmation(app.getDeveloper(),
app);
}
return app;
}
/**
* Verifies the last login time against the {@value
* SecurityConstants.SESSION_VALID_TIME}
*
* @param lastLogon
* long time student had last login in ms
* @throws AuthenticationException
* when session token has expired
*/
private void verifyLastLogonTime(long lastLogon)
throws AuthenticationException {
logger.debug("Verifying last login time...");
if (!(System.currentTimeMillis() - lastLogon <= SecurityConstants.SESSION_VALID_TIME)) {
logger.debug("Token expired");
throw new ExpiredTokenException("Session-token has expired");
}
logger.debug("Verified");
}
/**
* Fetches student from DB. If no student matches the email, a new student
* will be created and persisted
*
* @param userEmail
* the email to the student
* @return Student with the email, existing or newly created
*/
private Student getStudent(String userEmail) {
// Fetches the student from DB, if first time user, he/she gets
// persisted
Student student = studentService.getStudentByEmail(userEmail);
if (student == null) { // First time user, persist!
logger.debug("Student is a first time user, persisting.");
student = new Student(userEmail);
Long id = studentService.create(student);
student.setId(id);
}
return student;
}
/**
* Check if the email of the user is valid(nith.no) and passes bean
* validation
*
* @param email
* the string to check
* @throws UnvalidEmailException
*/
private void isUserValid(String email) throws UnvalidEmailException {
isEmailValid(email);
if (!email.endsWith(MiscConstants.VALID_EMAIL_DOMAIN)) {
logger.debug("email is unvalid: " + email);
throw new UnvalidEmailException("Unvalid email, must end with "
+ MiscConstants.VALID_EMAIL_DOMAIN);
}
logger.debug("Email valid: " + email);
}
/**
* Return true if the email is valid
*
* @param email
* string to check
*/
private void isEmailValid(String email) {
EmailValidator validator = EmailValidator.getInstance();
if (!validator.isValid(email)) {
throw new UnvalidEmailException("Unvalid email, did you forget @?");
}
}
// Private helper
private long getCurrentTime() {
return new GregorianCalendar().getTimeInMillis();
}
}