#84: TOTP key generation and validation code checking on login

This commit is contained in:
jendib 2016-03-22 23:08:49 +01:00
parent 5f84da61c8
commit fb0bb62eaf
11 changed files with 118 additions and 44 deletions

View File

@ -36,9 +36,9 @@ public class UserDao {
* *
* @param username User login * @param username User login
* @param password User password * @param password User password
* @return ID of the authenticated user or null * @return The authenticated user or null
*/ */
public String authenticate(String username, String password) { public User authenticate(String username, String password) {
EntityManager em = ThreadLocalContext.get().getEntityManager(); EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null"); Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null");
q.setParameter("username", username); q.setParameter("username", username);
@ -47,7 +47,7 @@ public class UserDao {
if (!BCrypt.checkpw(password, user.getPassword())) { if (!BCrypt.checkpw(password, user.getPassword())) {
return null; return null;
} }
return user.getId(); return user;
} catch (NoResultException e) { } catch (NoResultException e) {
return null; return null;
} }
@ -104,6 +104,7 @@ public class UserDao {
userFromDb.setEmail(user.getEmail()); userFromDb.setEmail(user.getEmail());
userFromDb.setStorageQuota(user.getStorageQuota()); userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent()); userFromDb.setStorageCurrent(user.getStorageCurrent());
userFromDb.setTotpKey(user.getTotpKey());
// Create audit log // Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId); AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);

View File

@ -64,56 +64,63 @@ public class AuthenticationToken {
return id; return id;
} }
public void setId(String id) { public AuthenticationToken setId(String id) {
this.id = id; this.id = id;
return this;
} }
public String getUserId() { public String getUserId() {
return userId; return userId;
} }
public void setUserId(String userId) { public AuthenticationToken setUserId(String userId) {
this.userId = userId; this.userId = userId;
return this;
} }
public boolean isLongLasted() { public boolean isLongLasted() {
return longLasted; return longLasted;
} }
public void setLongLasted(boolean longLasted) { public AuthenticationToken setLongLasted(boolean longLasted) {
this.longLasted = longLasted; this.longLasted = longLasted;
return this;
} }
public String getIp() { public String getIp() {
return ip; return ip;
} }
public void setIp(String ip) { public AuthenticationToken setIp(String ip) {
this.ip = ip; this.ip = ip;
return this;
} }
public String getUserAgent() { public String getUserAgent() {
return userAgent; return userAgent;
} }
public void setUserAgent(String userAgent) { public AuthenticationToken setUserAgent(String userAgent) {
this.userAgent = userAgent; this.userAgent = userAgent;
return this;
} }
public Date getCreationDate() { public Date getCreationDate() {
return creationDate; return creationDate;
} }
public void setCreationDate(Date creationDate) { public AuthenticationToken setCreationDate(Date creationDate) {
this.creationDate = creationDate; this.creationDate = creationDate;
return this;
} }
public Date getLastConnectionDate() { public Date getLastConnectionDate() {
return lastConnectionDate; return lastConnectionDate;
} }
public void setLastConnectionDate(Date lastConnectionDate) { public AuthenticationToken setLastConnectionDate(Date lastConnectionDate) {
this.lastConnectionDate = lastConnectionDate; this.lastConnectionDate = lastConnectionDate;
return this;
} }
@Override @Override

View File

@ -48,6 +48,12 @@ public class User implements Loggable {
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100) @Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
private String privateKey; private String privateKey;
/**
* TOTP secret key.
*/
@Column(name = "USE_TOTPKEY_C", length = 100)
private String totpKey;
/** /**
* Email address. * Email address.
*/ */
@ -82,48 +88,54 @@ public class User implements Loggable {
return id; return id;
} }
public void setId(String id) { public User setId(String id) {
this.id = id; this.id = id;
return this;
} }
public String getRoleId() { public String getRoleId() {
return roleId; return roleId;
} }
public void setRoleId(String roleId) { public User setRoleId(String roleId) {
this.roleId = roleId; this.roleId = roleId;
return this;
} }
public String getUsername() { public String getUsername() {
return username; return username;
} }
public void setUsername(String username) { public User setUsername(String username) {
this.username = username; this.username = username;
return this;
} }
public String getPassword() { public String getPassword() {
return password; return password;
} }
public void setPassword(String password) { public User setPassword(String password) {
this.password = password; this.password = password;
return this;
} }
public String getEmail() { public String getEmail() {
return email; return email;
} }
public void setEmail(String email) { public User setEmail(String email) {
this.email = email; this.email = email;
return this;
} }
public Date getCreateDate() { public Date getCreateDate() {
return createDate; return createDate;
} }
public void setCreateDate(Date createDate) { public User setCreateDate(Date createDate) {
this.createDate = createDate; this.createDate = createDate;
return this;
} }
@Override @Override
@ -131,32 +143,45 @@ public class User implements Loggable {
return deleteDate; return deleteDate;
} }
public void setDeleteDate(Date deleteDate) { public User setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate; this.deleteDate = deleteDate;
return this;
} }
public String getPrivateKey() { public String getPrivateKey() {
return privateKey; return privateKey;
} }
public void setPrivateKey(String privateKey) { public User setPrivateKey(String privateKey) {
this.privateKey = privateKey; this.privateKey = privateKey;
return this;
} }
public Long getStorageQuota() { public Long getStorageQuota() {
return storageQuota; return storageQuota;
} }
public void setStorageQuota(Long storageQuota) { public User setStorageQuota(Long storageQuota) {
this.storageQuota = storageQuota; this.storageQuota = storageQuota;
return this;
} }
public Long getStorageCurrent() { public Long getStorageCurrent() {
return storageCurrent; return storageCurrent;
} }
public void setStorageCurrent(Long storageCurrent) { public User setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent; this.storageCurrent = storageCurrent;
return this;
}
public String getTotpKey() {
return totpKey;
}
public User setTotpKey(String totpKey) {
this.totpKey = totpKey;
return this;
} }
@Override @Override

View File

@ -1 +1 @@
db.version=8 db.version=9

View File

@ -0,0 +1,2 @@
alter table T_USER add column USE_TOTPKEY_C varchar(100);
update T_CONFIG set CFG_VALUE_C = '9' where CFG_ID_C = 'DB_VERSION';

View File

@ -95,12 +95,11 @@ public class ClientUtil {
* @return Authentication token * @return Authentication token
*/ */
public String login(String username, String password, Boolean remember) { public String login(String username, String password, Boolean remember) {
Form form = new Form();
form.param("username", username);
form.param("password", password);
form.param("remember", remember.toString());
Response response = resource.path("/user/login").request() Response response = resource.path("/user/login").request()
.post(Entity.form(form)); .post(Entity.form(new Form()
.param("username", username)
.param("password", password)
.param("remember", remember.toString())));
return getAuthenticationCookie(response); return getAuthenticationCookie(response);
} }

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -255,6 +255,7 @@ public class UserResource extends BaseResource {
public Response login( public Response login(
@FormParam("username") String username, @FormParam("username") String username,
@FormParam("password") String password, @FormParam("password") String password,
@FormParam("code") String validationCodeStr,
@FormParam("remember") boolean longLasted) { @FormParam("remember") boolean longLasted) {
// Validate the input data // Validate the input data
username = StringUtils.strip(username); username = StringUtils.strip(username);
@ -262,11 +263,26 @@ public class UserResource extends BaseResource {
// Get the user // Get the user
UserDao userDao = new UserDao(); UserDao userDao = new UserDao();
String userId = userDao.authenticate(username, password); User user = userDao.authenticate(username, password);
if (userId == null) { if (user == null) {
throw new ForbiddenClientException(); throw new ForbiddenClientException();
} }
// Two factor authentication
if (user.getTotpKey() != null) {
// If TOTP is enabled, ask a validation code
if (Strings.isNullOrEmpty(validationCodeStr)) {
throw new ClientException("ValidationCodeRequired", "An OTP validation code is required");
}
// Check the validation code
int validationCode = ValidationUtil.validateInteger(validationCodeStr, "code");
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
if (!googleAuthenticator.authorize(user.getTotpKey(), validationCode)) {
throw new ForbiddenClientException();
}
}
// Get the remote IP // Get the remote IP
String ip = request.getHeader("x-forwarded-for"); String ip = request.getHeader("x-forwarded-for");
if (Strings.isNullOrEmpty(ip)) { if (Strings.isNullOrEmpty(ip)) {
@ -275,15 +291,15 @@ public class UserResource extends BaseResource {
// Create a new session token // Create a new session token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
AuthenticationToken authenticationToken = new AuthenticationToken(); AuthenticationToken authenticationToken = new AuthenticationToken()
authenticationToken.setUserId(userId); .setUserId(user.getId())
authenticationToken.setLongLasted(longLasted); .setLongLasted(longLasted)
authenticationToken.setIp(ip); .setIp(ip)
authenticationToken.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); .setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000));
String token = authenticationTokenDao.create(authenticationToken); String token = authenticationTokenDao.create(authenticationToken);
// Cleanup old session tokens // Cleanup old session tokens
authenticationTokenDao.deleteOldSessionToken(userId); authenticationTokenDao.deleteOldSessionToken(user.getId());
JsonObjectBuilder response = Json.createObjectBuilder(); JsonObjectBuilder response = Json.createObjectBuilder();
int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1; int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1;
@ -648,18 +664,18 @@ public class UserResource extends BaseResource {
throw new ForbiddenClientException(); throw new ForbiddenClientException();
} }
// Create a new TOTP key and scratch codes // Create a new TOTP key
GoogleAuthenticator gAuth = new GoogleAuthenticator(); GoogleAuthenticator gAuth = new GoogleAuthenticator();
final GoogleAuthenticatorKey key = gAuth.createCredentials(); final GoogleAuthenticatorKey key = gAuth.createCredentials();
JsonArrayBuilder scratchCodes = Json.createArrayBuilder(); // Save it
for (int scratchCode : key.getScratchCodes()) { UserDao userDao = new UserDao();
scratchCodes.add(scratchCode); User user = userDao.getActiveByUsername(principal.getName());
} user.setTotpKey(key.getKey());
user = userDao.update(user, principal.getId());
JsonObjectBuilder response = Json.createObjectBuilder() JsonObjectBuilder response = Json.createObjectBuilder()
.add("secret", key.getKey()) .add("secret", key.getKey());
.add("scratch_codes", scratchCodes);
return Response.ok().entity(response.build()).build(); return Response.ok().entity(response.build()).build();
} }

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -1,5 +1,6 @@
package com.sismics.docs.rest; package com.sismics.docs.rest;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import javax.json.JsonArray; import javax.json.JsonArray;
@ -13,6 +14,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.totp.GoogleAuthenticator;
/** /**
* Exhaustive test of the user resource. * Exhaustive test of the user resource.
@ -299,5 +301,27 @@ public class TestUserResource extends BaseJerseyTest {
.post(Entity.form(new Form()), JsonObject.class); .post(Entity.form(new Form()), JsonObject.class);
String secret = json.getString("secret"); String secret = json.getString("secret");
Assert.assertNotNull(secret); Assert.assertNotNull(secret);
// Try to login with totp1 without a validation code
Response response = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "totp1")
.param("password", "12345678")
.param("remember", "false")));
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationCodeRequired", json.getString("type"));
// Generate a OTP
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
int validationCode = googleAuthenticator.calculateCode(secret, new Date().getTime() / 30000);
// Login with totp1 with a validation code
json = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "totp1")
.param("password", "12345678")
.param("code", Integer.toString(validationCode))
.param("remember", "false")), JsonObject.class);
} }
} }