diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java index 92cb1718..bc392947 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java @@ -36,9 +36,9 @@ public class UserDao { * * @param username User login * @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(); Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null"); q.setParameter("username", username); @@ -47,7 +47,7 @@ public class UserDao { if (!BCrypt.checkpw(password, user.getPassword())) { return null; } - return user.getId(); + return user; } catch (NoResultException e) { return null; } @@ -104,6 +104,7 @@ public class UserDao { userFromDb.setEmail(user.getEmail()); userFromDb.setStorageQuota(user.getStorageQuota()); userFromDb.setStorageCurrent(user.getStorageCurrent()); + userFromDb.setTotpKey(user.getTotpKey()); // Create audit log AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId); diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java index c5030e0d..3b592bf2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java @@ -64,56 +64,63 @@ public class AuthenticationToken { return id; } - public void setId(String id) { + public AuthenticationToken setId(String id) { this.id = id; + return this; } public String getUserId() { return userId; } - public void setUserId(String userId) { + public AuthenticationToken setUserId(String userId) { this.userId = userId; + return this; } public boolean isLongLasted() { return longLasted; } - public void setLongLasted(boolean longLasted) { + public AuthenticationToken setLongLasted(boolean longLasted) { this.longLasted = longLasted; + return this; } public String getIp() { return ip; } - public void setIp(String ip) { + public AuthenticationToken setIp(String ip) { this.ip = ip; + return this; } public String getUserAgent() { return userAgent; } - public void setUserAgent(String userAgent) { + public AuthenticationToken setUserAgent(String userAgent) { this.userAgent = userAgent; + return this; } public Date getCreationDate() { return creationDate; } - public void setCreationDate(Date creationDate) { + public AuthenticationToken setCreationDate(Date creationDate) { this.creationDate = creationDate; + return this; } public Date getLastConnectionDate() { return lastConnectionDate; } - public void setLastConnectionDate(Date lastConnectionDate) { + public AuthenticationToken setLastConnectionDate(Date lastConnectionDate) { this.lastConnectionDate = lastConnectionDate; + return this; } @Override diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index 5476a4ab..0f37b50c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -48,6 +48,12 @@ public class User implements Loggable { @Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100) private String privateKey; + /** + * TOTP secret key. + */ + @Column(name = "USE_TOTPKEY_C", length = 100) + private String totpKey; + /** * Email address. */ @@ -82,48 +88,54 @@ public class User implements Loggable { return id; } - public void setId(String id) { + public User setId(String id) { this.id = id; + return this; } public String getRoleId() { return roleId; } - public void setRoleId(String roleId) { + public User setRoleId(String roleId) { this.roleId = roleId; + return this; } public String getUsername() { return username; } - public void setUsername(String username) { + public User setUsername(String username) { this.username = username; + return this; } public String getPassword() { return password; } - public void setPassword(String password) { + public User setPassword(String password) { this.password = password; + return this; } public String getEmail() { return email; } - public void setEmail(String email) { + public User setEmail(String email) { this.email = email; + return this; } public Date getCreateDate() { return createDate; } - public void setCreateDate(Date createDate) { + public User setCreateDate(Date createDate) { this.createDate = createDate; + return this; } @Override @@ -131,32 +143,45 @@ public class User implements Loggable { return deleteDate; } - public void setDeleteDate(Date deleteDate) { + public User setDeleteDate(Date deleteDate) { this.deleteDate = deleteDate; + return this; } public String getPrivateKey() { return privateKey; } - public void setPrivateKey(String privateKey) { + public User setPrivateKey(String privateKey) { this.privateKey = privateKey; + return this; } public Long getStorageQuota() { return storageQuota; } - public void setStorageQuota(Long storageQuota) { + public User setStorageQuota(Long storageQuota) { this.storageQuota = storageQuota; + return this; } public Long getStorageCurrent() { return storageCurrent; } - public void setStorageCurrent(Long storageCurrent) { + public User setStorageCurrent(Long storageCurrent) { this.storageCurrent = storageCurrent; + return this; + } + + public String getTotpKey() { + return totpKey; + } + + public User setTotpKey(String totpKey) { + this.totpKey = totpKey; + return this; } @Override diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index fda5dfd0..edf8e6a4 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=8 \ No newline at end of file +db.version=9 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-009-0.sql b/docs-core/src/main/resources/db/update/dbupdate-009-0.sql new file mode 100644 index 00000000..2b5e8ea1 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-009-0.sql @@ -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'; diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java index 4c476265..968f9adb 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java @@ -95,12 +95,11 @@ public class ClientUtil { * @return Authentication token */ 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() - .post(Entity.form(form)); + .post(Entity.form(new Form() + .param("username", username) + .param("password", password) + .param("remember", remember.toString()))); return getAuthenticationCookie(response); } diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 44ddb414..04b5153a 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=8 \ No newline at end of file +db.version=9 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 16cf7593..697b8a62 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -255,6 +255,7 @@ public class UserResource extends BaseResource { public Response login( @FormParam("username") String username, @FormParam("password") String password, + @FormParam("code") String validationCodeStr, @FormParam("remember") boolean longLasted) { // Validate the input data username = StringUtils.strip(username); @@ -262,10 +263,25 @@ public class UserResource extends BaseResource { // Get the user UserDao userDao = new UserDao(); - String userId = userDao.authenticate(username, password); - if (userId == null) { + User user = userDao.authenticate(username, password); + if (user == null) { 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 String ip = request.getHeader("x-forwarded-for"); @@ -275,15 +291,15 @@ public class UserResource extends BaseResource { // Create a new session token AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); - AuthenticationToken authenticationToken = new AuthenticationToken(); - authenticationToken.setUserId(userId); - authenticationToken.setLongLasted(longLasted); - authenticationToken.setIp(ip); - authenticationToken.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); + AuthenticationToken authenticationToken = new AuthenticationToken() + .setUserId(user.getId()) + .setLongLasted(longLasted) + .setIp(ip) + .setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); String token = authenticationTokenDao.create(authenticationToken); // Cleanup old session tokens - authenticationTokenDao.deleteOldSessionToken(userId); + authenticationTokenDao.deleteOldSessionToken(user.getId()); JsonObjectBuilder response = Json.createObjectBuilder(); int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1; @@ -648,18 +664,18 @@ public class UserResource extends BaseResource { throw new ForbiddenClientException(); } - // Create a new TOTP key and scratch codes + // Create a new TOTP key GoogleAuthenticator gAuth = new GoogleAuthenticator(); final GoogleAuthenticatorKey key = gAuth.createCredentials(); - JsonArrayBuilder scratchCodes = Json.createArrayBuilder(); - for (int scratchCode : key.getScratchCodes()) { - scratchCodes.add(scratchCode); - } + // Save it + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(principal.getName()); + user.setTotpKey(key.getKey()); + user = userDao.update(user, principal.getId()); JsonObjectBuilder response = Json.createObjectBuilder() - .add("secret", key.getKey()) - .add("scratch_codes", scratchCodes); + .add("secret", key.getKey()); return Response.ok().entity(response.build()).build(); } diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 44ddb414..04b5153a 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=8 \ No newline at end of file +db.version=9 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 44ddb414..04b5153a 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=8 \ No newline at end of file +db.version=9 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 241990e6..e338ca3e 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -1,5 +1,6 @@ package com.sismics.docs.rest; +import java.util.Date; import java.util.Locale; import javax.json.JsonArray; @@ -13,6 +14,7 @@ import org.junit.Assert; import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; +import com.sismics.util.totp.GoogleAuthenticator; /** * Exhaustive test of the user resource. @@ -299,5 +301,27 @@ public class TestUserResource extends BaseJerseyTest { .post(Entity.form(new Form()), JsonObject.class); String secret = json.getString("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); } } \ No newline at end of file