mirror of
https://github.com/sismics/docs.git
synced 2024-11-22 22:07:56 +01:00
commit
767099b7ea
@ -23,11 +23,11 @@ Features
|
|||||||
- Optical character recognition
|
- Optical character recognition
|
||||||
- Support image, PDF, ODT and DOCX files
|
- Support image, PDF, ODT and DOCX files
|
||||||
- Flexible search engine
|
- Flexible search engine
|
||||||
- Full text search in image and PDF
|
- Full text search in all supported files
|
||||||
- All [Dublin Core](http://dublincore.org/) metadata
|
- All [Dublin Core](http://dublincore.org/) metadata
|
||||||
- 256-bit AES encryption
|
- 256-bit AES encryption of stored files
|
||||||
- Tag system with relations
|
- Tag system with nesting
|
||||||
- Multi-users ACL system
|
- User/group permission system
|
||||||
- Hierarchical groups
|
- Hierarchical groups
|
||||||
- Audit log
|
- Audit log
|
||||||
- Comments
|
- Comments
|
||||||
|
@ -77,7 +77,6 @@
|
|||||||
<artifactId>jbcrypt</artifactId>
|
<artifactId>jbcrypt</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.lucene</groupId>
|
<groupId>org.apache.lucene</groupId>
|
||||||
<artifactId>lucene-core</artifactId>
|
<artifactId>lucene-core</artifactId>
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,456 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2016 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base32;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements the functionality described in RFC 6238 (TOTP: Time
|
||||||
|
* based one-time password algorithm) and has been tested again Google's
|
||||||
|
* implementation of such algorithm in its Google Authenticator application.
|
||||||
|
* <p/>
|
||||||
|
* This class lets users create a new 16-bit base32-encoded secret key with the
|
||||||
|
* validation code calculated at {@code time = 0} (the UNIX epoch) and the URL
|
||||||
|
* of a Google-provided QR barcode to let an user load the generated information
|
||||||
|
* into Google Authenticator.
|
||||||
|
* <p/>
|
||||||
|
* The random number generator used by this class uses the default algorithm and
|
||||||
|
* provider. Users can override them by setting the following system properties
|
||||||
|
* to the algorithm and provider name of their choice:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #RNG_ALGORITHM}.</li>
|
||||||
|
* <li>{@link #RNG_ALGORITHM_PROVIDER}.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p/>
|
||||||
|
* This class does not store in any way either the generated keys nor the keys
|
||||||
|
* passed during the authorization process.
|
||||||
|
* <p/>
|
||||||
|
* Java Server side class for Google Authenticator's TOTP generator was inspired
|
||||||
|
* by an author's blog post.
|
||||||
|
*
|
||||||
|
* @author Enrico M. Crisostomo
|
||||||
|
* @author Warren Strange
|
||||||
|
* @version 0.5.0
|
||||||
|
* @see <a href=
|
||||||
|
* "http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html"
|
||||||
|
* />
|
||||||
|
* @see <a href="http://code.google.com/p/google-authenticator" />
|
||||||
|
* @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt" />
|
||||||
|
* @since 0.3.0
|
||||||
|
*/
|
||||||
|
public final class GoogleAuthenticator {
|
||||||
|
/**
|
||||||
|
* The system property to specify the random number generator algorithm to
|
||||||
|
* use.
|
||||||
|
*
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
public static final String RNG_ALGORITHM = "com.warrenstrange.googleauth.rng.algorithm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The system property to specify the random number generator provider to
|
||||||
|
* use.
|
||||||
|
*
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
public static final String RNG_ALGORITHM_PROVIDER = "com.warrenstrange.googleauth.rng.algorithmProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bits of a secret key in binary form. Since the Base32
|
||||||
|
* encoding with 8 bit characters introduces an 160% overhead, we just need
|
||||||
|
* 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
|
||||||
|
*/
|
||||||
|
private static final int SECRET_BITS = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of scratch codes to generate during the key generation. We are
|
||||||
|
* using Google's default of providing 5 scratch codes.
|
||||||
|
*/
|
||||||
|
private static final int SCRATCH_CODES = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of digits of a scratch code represented as a decimal integer.
|
||||||
|
*/
|
||||||
|
private static final int SCRATCH_CODE_LENGTH = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modulus used to truncate the scratch code.
|
||||||
|
*/
|
||||||
|
public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic number representing an invalid scratch code.
|
||||||
|
*/
|
||||||
|
private static final int SCRATCH_CODE_INVALID = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length in bytes of each scratch code. We're using Google's default of
|
||||||
|
* using 4 bytes per scratch code.
|
||||||
|
*/
|
||||||
|
private static final int BYTES_PER_SCRATCH_CODE = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default SecureRandom algorithm to use if none is specified.
|
||||||
|
*
|
||||||
|
* @see java.security.SecureRandom#getInstance(String)
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default random number algorithm provider to use if none is specified.
|
||||||
|
*
|
||||||
|
* @see java.security.SecureRandom#getInstance(String)
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptographic hash function used to calculate the HMAC (Hash-based
|
||||||
|
* Message Authentication Code). This implementation uses the SHA1 hash
|
||||||
|
* function.
|
||||||
|
*/
|
||||||
|
private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration used by the current instance.
|
||||||
|
*/
|
||||||
|
private final GoogleAuthenticatorConfig config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal SecureRandom instance used by this class. Since Java 7
|
||||||
|
* {@link Random} instances are required to be thread-safe, no
|
||||||
|
* synchronisation is required in the methods of this class using this
|
||||||
|
* instance. Thread-safety of this class was a de-facto standard in previous
|
||||||
|
* versions of Java so that it is expected to work correctly in previous
|
||||||
|
* versions of the Java platform as well.
|
||||||
|
*/
|
||||||
|
private ReseedingSecureRandom secureRandom = new ReseedingSecureRandom(getRandomNumberAlgorithm(), getRandomNumberAlgorithmProvider());
|
||||||
|
|
||||||
|
public GoogleAuthenticator() {
|
||||||
|
config = new GoogleAuthenticatorConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
|
||||||
|
if (config == null) {
|
||||||
|
throw new IllegalArgumentException("Configuration cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the random number generator algorithm.
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
private String getRandomNumberAlgorithm() {
|
||||||
|
return System.getProperty(RNG_ALGORITHM, DEFAULT_RANDOM_NUMBER_ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the random number generator algorithm provider.
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
private String getRandomNumberAlgorithmProvider() {
|
||||||
|
return System.getProperty(RNG_ALGORITHM_PROVIDER, DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int calculateCode(String secret, long tm) {
|
||||||
|
return calculateCode(decodeKey(secret), tm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a secret key in raw bytes.
|
||||||
|
*
|
||||||
|
* @param secret Secret key
|
||||||
|
* @return Raw bytes
|
||||||
|
*/
|
||||||
|
private byte[] decodeKey(String secret) {
|
||||||
|
switch (config.getKeyRepresentation()) {
|
||||||
|
case BASE32:
|
||||||
|
Base32 codec32 = new Base32();
|
||||||
|
return codec32.decode(secret);
|
||||||
|
case BASE64:
|
||||||
|
Base64 codec64 = new Base64();
|
||||||
|
return codec64.decode(secret);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown key representation type.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the verification code of the provided key at the specified
|
||||||
|
* instant of time using the algorithm specified in RFC 6238.
|
||||||
|
*
|
||||||
|
* @param key the secret key in binary format.
|
||||||
|
* @param tm the instant of time.
|
||||||
|
* @return the validation code for the provided key at the specified instant
|
||||||
|
* of time.
|
||||||
|
*/
|
||||||
|
int calculateCode(byte[] key, long tm) {
|
||||||
|
// Allocating an array of bytes to represent the specified instant
|
||||||
|
// of time.
|
||||||
|
byte[] data = new byte[8];
|
||||||
|
long value = tm;
|
||||||
|
|
||||||
|
// Converting the instant of time from the long representation to a
|
||||||
|
// big-endian array of bytes (RFC4226, 5.2. Description).
|
||||||
|
for (int i = 8; i-- > 0; value >>>= 8) {
|
||||||
|
data[i] = (byte) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Building the secret key specification for the HmacSHA1 algorithm.
|
||||||
|
SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Getting an HmacSHA1 algorithm implementation from the JCE.
|
||||||
|
Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
|
||||||
|
|
||||||
|
// Initializing the MAC algorithm.
|
||||||
|
mac.init(signKey);
|
||||||
|
|
||||||
|
// Processing the instant of time and getting the encrypted data.
|
||||||
|
byte[] hash = mac.doFinal(data);
|
||||||
|
|
||||||
|
// Building the validation code performing dynamic truncation
|
||||||
|
// (RFC4226, 5.3. Generating an HOTP value)
|
||||||
|
int offset = hash[hash.length - 1] & 0xF;
|
||||||
|
|
||||||
|
// We are using a long because Java hasn't got an unsigned integer
|
||||||
|
// type
|
||||||
|
// and we need 32 unsigned bits).
|
||||||
|
long truncatedHash = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
truncatedHash <<= 8;
|
||||||
|
|
||||||
|
// Java bytes are signed but we need an unsigned integer:
|
||||||
|
// cleaning off all but the LSB.
|
||||||
|
truncatedHash |= (hash[offset + i] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean bits higher than the 32nd (inclusive) and calculate the
|
||||||
|
// module with the maximum validation code value.
|
||||||
|
truncatedHash &= 0x7FFFFFFF;
|
||||||
|
truncatedHash %= config.getKeyModulus();
|
||||||
|
|
||||||
|
// Returning the validation code to the caller.
|
||||||
|
return (int) truncatedHash;
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
|
||||||
|
// We're not disclosing internal error details to our clients.
|
||||||
|
throw new GoogleAuthenticatorException("The operation cannot be performed now.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method implements the algorithm specified in RFC 6238 to check if a
|
||||||
|
* validation code is valid in a given instant of time for the given secret
|
||||||
|
* key.
|
||||||
|
*
|
||||||
|
* @param secret the Base32 encoded secret key.
|
||||||
|
* @param code the code to validate.
|
||||||
|
* @param timestamp the instant of time to use during the validation process.
|
||||||
|
* @param window the window size to use during the validation process.
|
||||||
|
* @return <code>true</code> if the validation code is valid,
|
||||||
|
* <code>false</code> otherwise.
|
||||||
|
*/
|
||||||
|
private boolean checkCode(String secret, long code, long timestamp, int window) {
|
||||||
|
// Decoding the secret key to get its raw byte representation.
|
||||||
|
byte[] decodedKey = decodeKey(secret);
|
||||||
|
|
||||||
|
// convert unix time into a 30 second "window" as specified by the
|
||||||
|
// TOTP specification. Using Google's default interval of 30 seconds.
|
||||||
|
final long timeWindow = timestamp / this.config.getTimeStepSizeInMillis();
|
||||||
|
|
||||||
|
// Calculating the verification code of the given key in each of the
|
||||||
|
// time intervals and returning true if the provided code is equal to
|
||||||
|
// one of them.
|
||||||
|
for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
|
||||||
|
// Calculating the verification code for the current time interval.
|
||||||
|
long hash = calculateCode(decodedKey, timeWindow + i);
|
||||||
|
|
||||||
|
// Checking if the provided code is equal to the calculated one.
|
||||||
|
if (hash == code) {
|
||||||
|
// The verification code is valid.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The verification code is invalid.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticatorKey createCredentials() {
|
||||||
|
// Allocating a buffer sufficiently large to hold the bytes required by
|
||||||
|
// the secret key and the scratch codes.
|
||||||
|
byte[] buffer = new byte[SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
|
||||||
|
|
||||||
|
secureRandom.nextBytes(buffer);
|
||||||
|
|
||||||
|
// Extracting the bytes making up the secret key.
|
||||||
|
byte[] secretKey = Arrays.copyOf(buffer, SECRET_BITS / 8);
|
||||||
|
String generatedKey = calculateSecretKey(secretKey);
|
||||||
|
|
||||||
|
// Generating the verification code at time = 0.
|
||||||
|
int validationCode = calculateValidationCode(secretKey);
|
||||||
|
|
||||||
|
// Calculate scratch codes
|
||||||
|
List<Integer> scratchCodes = calculateScratchCodes(buffer);
|
||||||
|
|
||||||
|
return new GoogleAuthenticatorKey(generatedKey, validationCode, scratchCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> calculateScratchCodes(byte[] buffer) {
|
||||||
|
List<Integer> scratchCodes = new ArrayList<>();
|
||||||
|
|
||||||
|
while (scratchCodes.size() < SCRATCH_CODES) {
|
||||||
|
byte[] scratchCodeBuffer = Arrays.copyOfRange(buffer, SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size(), SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size() + BYTES_PER_SCRATCH_CODE);
|
||||||
|
|
||||||
|
int scratchCode = calculateScratchCode(scratchCodeBuffer);
|
||||||
|
|
||||||
|
if (scratchCode != SCRATCH_CODE_INVALID) {
|
||||||
|
scratchCodes.add(scratchCode);
|
||||||
|
} else {
|
||||||
|
scratchCodes.add(generateScratchCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scratchCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method calculates a scratch code from a random byte buffer of
|
||||||
|
* suitable size <code>#BYTES_PER_SCRATCH_CODE</code>.
|
||||||
|
*
|
||||||
|
* @param scratchCodeBuffer a random byte buffer whose minimum size is <code>#BYTES_PER_SCRATCH_CODE</code>.
|
||||||
|
* @return the scratch code.
|
||||||
|
*/
|
||||||
|
private int calculateScratchCode(byte[] scratchCodeBuffer) {
|
||||||
|
if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE) {
|
||||||
|
throw new IllegalArgumentException(String.format("The provided random byte buffer is too small: %d.", scratchCodeBuffer.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
int scratchCode = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i) {
|
||||||
|
scratchCode = (scratchCode << 8) + (scratchCodeBuffer[i] & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
scratchCode = (scratchCode & 0x7FFFFFFF) % SCRATCH_CODE_MODULUS;
|
||||||
|
|
||||||
|
// Accept the scratch code only if it has exactly
|
||||||
|
// SCRATCH_CODE_LENGTH digits.
|
||||||
|
if (scratchCode >= SCRATCH_CODE_MODULUS / 10) {
|
||||||
|
return scratchCode;
|
||||||
|
} else {
|
||||||
|
return SCRATCH_CODE_INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates a new random byte buffer from which a new scratch
|
||||||
|
* code is generated. This function is invoked if a scratch code generated
|
||||||
|
* from the main buffer is invalid because it does not satisfy the scratch
|
||||||
|
* code restrictions.
|
||||||
|
*
|
||||||
|
* @return A valid scratch code.
|
||||||
|
*/
|
||||||
|
private int generateScratchCode() {
|
||||||
|
while (true) {
|
||||||
|
byte[] scratchCodeBuffer = new byte[BYTES_PER_SCRATCH_CODE];
|
||||||
|
secureRandom.nextBytes(scratchCodeBuffer);
|
||||||
|
|
||||||
|
int scratchCode = calculateScratchCode(scratchCodeBuffer);
|
||||||
|
|
||||||
|
if (scratchCode != SCRATCH_CODE_INVALID) {
|
||||||
|
return scratchCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method calculates the validation code at time 0.
|
||||||
|
*
|
||||||
|
* @param secretKey The secret key to use.
|
||||||
|
* @return the validation code at time 0.
|
||||||
|
*/
|
||||||
|
private int calculateValidationCode(byte[] secretKey) {
|
||||||
|
return calculateCode(secretKey, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method calculates the secret key given a random byte buffer.
|
||||||
|
*
|
||||||
|
* @param secretKey a random byte buffer.
|
||||||
|
* @return the secret key.
|
||||||
|
*/
|
||||||
|
private String calculateSecretKey(byte[] secretKey) {
|
||||||
|
switch (config.getKeyRepresentation()) {
|
||||||
|
case BASE32:
|
||||||
|
return new Base32().encodeToString(secretKey);
|
||||||
|
case BASE64:
|
||||||
|
return new Base64().encodeToString(secretKey);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown key representation type.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorize(String secret, int verificationCode) throws GoogleAuthenticatorException {
|
||||||
|
return authorize(secret, verificationCode, new Date().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorize(String secret, int verificationCode, long time) throws GoogleAuthenticatorException {
|
||||||
|
// Checking user input and failing if the secret key was not provided.
|
||||||
|
if (secret == null) {
|
||||||
|
throw new IllegalArgumentException("Secret cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if the verification code is between the legal bounds.
|
||||||
|
if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking the validation code using the current UNIX time.
|
||||||
|
return checkCode(secret, verificationCode, time, this.config.getWindowSize());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2015 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class GoogleAuthenticatorConfig {
|
||||||
|
private long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30);
|
||||||
|
private int windowSize = 3;
|
||||||
|
private int codeDigits = 6;
|
||||||
|
private int keyModulus = (int) Math.pow(10, codeDigits);
|
||||||
|
private KeyRepresentation keyRepresentation = KeyRepresentation.BASE32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key module.
|
||||||
|
*
|
||||||
|
* @return the key module.
|
||||||
|
*/
|
||||||
|
public int getKeyModulus() {
|
||||||
|
return keyModulus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key representation.
|
||||||
|
*
|
||||||
|
* @return the key representation.
|
||||||
|
*/
|
||||||
|
public KeyRepresentation getKeyRepresentation() {
|
||||||
|
return keyRepresentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of digits in the generated code.
|
||||||
|
*
|
||||||
|
* @return the number of digits in the generated code.
|
||||||
|
*/
|
||||||
|
public int getCodeDigits() {
|
||||||
|
return codeDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time step size, in milliseconds, as specified by RFC 6238.
|
||||||
|
* The default value is 30.000.
|
||||||
|
*
|
||||||
|
* @return the time step size in milliseconds.
|
||||||
|
*/
|
||||||
|
public long getTimeStepSizeInMillis() {
|
||||||
|
return timeStepSizeInMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an integer value representing the number of windows of size
|
||||||
|
* timeStepSizeInMillis that are checked during the validation process, to
|
||||||
|
* account for differences between the server and the client clocks. The
|
||||||
|
* bigger the window, the more tolerant the library code is about clock
|
||||||
|
* skews.
|
||||||
|
* <p/>
|
||||||
|
* We are using Google's default behaviour of using a window size equal to
|
||||||
|
* 3. The limit on the maximum window size, present in older versions of
|
||||||
|
* this library, has been removed.
|
||||||
|
*
|
||||||
|
* @return the window size.
|
||||||
|
* @see #timeStepSizeInMillis
|
||||||
|
*/
|
||||||
|
public int getWindowSize() {
|
||||||
|
return windowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GoogleAuthenticatorConfigBuilder {
|
||||||
|
private GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig();
|
||||||
|
|
||||||
|
public GoogleAuthenticatorConfig build() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticatorConfigBuilder setCodeDigits(int codeDigits) {
|
||||||
|
if (codeDigits <= 0) {
|
||||||
|
throw new IllegalArgumentException("Code digits must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeDigits < 6) {
|
||||||
|
throw new IllegalArgumentException("The minimum number of digits is 6.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeDigits > 8) {
|
||||||
|
throw new IllegalArgumentException("The maximum number of digits is 8.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.codeDigits = codeDigits;
|
||||||
|
config.keyModulus = (int) Math.pow(10, codeDigits);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticatorConfigBuilder setTimeStepSizeInMillis(long timeStepSizeInMillis) {
|
||||||
|
if (timeStepSizeInMillis <= 0) {
|
||||||
|
throw new IllegalArgumentException("Time step size must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.timeStepSizeInMillis = timeStepSizeInMillis;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticatorConfigBuilder setWindowSize(int windowSize) {
|
||||||
|
if (windowSize <= 0) {
|
||||||
|
throw new IllegalArgumentException("Window number must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.windowSize = windowSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleAuthenticatorConfigBuilder setKeyRepresentation(KeyRepresentation keyRepresentation) {
|
||||||
|
if (keyRepresentation == null) {
|
||||||
|
throw new IllegalArgumentException("Key representation cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.keyRepresentation = keyRepresentation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2015 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date: 12/02/14
|
||||||
|
* Time: 13:36
|
||||||
|
*
|
||||||
|
* @author Enrico M. Crisostomo
|
||||||
|
*/
|
||||||
|
public class GoogleAuthenticatorException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an exception with the provided error message.
|
||||||
|
*
|
||||||
|
* @param message the error message.
|
||||||
|
*/
|
||||||
|
public GoogleAuthenticatorException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an exception with the provided error mesasge and
|
||||||
|
* the provided cuase.
|
||||||
|
*
|
||||||
|
* @param message the error message.
|
||||||
|
* @param cause the cause.
|
||||||
|
*/
|
||||||
|
public GoogleAuthenticatorException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2015 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a JavaBean used by the GoogleAuthenticator library to represent
|
||||||
|
* a secret key.
|
||||||
|
* <p/>
|
||||||
|
* This class is immutable.
|
||||||
|
* <p/>
|
||||||
|
* Instance of this class should only be constructed by the GoogleAuthenticator
|
||||||
|
* library.
|
||||||
|
*
|
||||||
|
* @author Enrico M. Crisostomo
|
||||||
|
* @version 1.0
|
||||||
|
* @see GoogleAuthenticator
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class GoogleAuthenticatorKey {
|
||||||
|
/**
|
||||||
|
* The secret key in Base32 encoding.
|
||||||
|
*/
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verification code at time = 0 (the UNIX epoch).
|
||||||
|
*/
|
||||||
|
private final int verificationCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of scratch codes.
|
||||||
|
*/
|
||||||
|
private final List<Integer> scratchCodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The constructor with package visibility.
|
||||||
|
*
|
||||||
|
* @param secretKey the secret key in Base32 encoding.
|
||||||
|
* @param code the verification code at time = 0 (the UNIX epoch).
|
||||||
|
* @param scratchCodes the list of scratch codes.
|
||||||
|
*/
|
||||||
|
GoogleAuthenticatorKey(String secretKey, int code, List<Integer> scratchCodes) {
|
||||||
|
key = secretKey;
|
||||||
|
verificationCode = code;
|
||||||
|
this.scratchCodes = new ArrayList<>(scratchCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of scratch codes.
|
||||||
|
*
|
||||||
|
* @return the list of scratch codes.
|
||||||
|
*/
|
||||||
|
public List<Integer> getScratchCodes() {
|
||||||
|
return scratchCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the secret key in Base32 encoding.
|
||||||
|
*
|
||||||
|
* @return the secret key in Base32 encoding.
|
||||||
|
*/
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the verification code at time = 0 (the UNIX epoch).
|
||||||
|
*
|
||||||
|
* @return the verificationCode at time = 0 (the UNIX epoch).
|
||||||
|
*/
|
||||||
|
public int getVerificationCode() {
|
||||||
|
return verificationCode;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2015 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
public enum KeyRepresentation {
|
||||||
|
BASE32,
|
||||||
|
BASE64
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2014-2015 Enrico M. Crisostomo
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the author nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.sismics.util.totp;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date: 08/04/14 Time: 15:21
|
||||||
|
*
|
||||||
|
* @author Enrico M. Crisostomo
|
||||||
|
*/
|
||||||
|
class ReseedingSecureRandom {
|
||||||
|
private static final int MAX_OPERATIONS = 1_000_000;
|
||||||
|
private final String provider;
|
||||||
|
private final String algorithm;
|
||||||
|
private final AtomicInteger count = new AtomicInteger(0);
|
||||||
|
private SecureRandom secureRandom;
|
||||||
|
|
||||||
|
ReseedingSecureRandom() {
|
||||||
|
this.algorithm = null;
|
||||||
|
this.provider = null;
|
||||||
|
|
||||||
|
buildSecureRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReseedingSecureRandom(String algorithm) {
|
||||||
|
if (algorithm == null) {
|
||||||
|
throw new IllegalArgumentException("Algorithm cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.provider = null;
|
||||||
|
|
||||||
|
buildSecureRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReseedingSecureRandom(String algorithm, String provider) {
|
||||||
|
if (algorithm == null) {
|
||||||
|
throw new IllegalArgumentException("Algorithm cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("Provider cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.provider = provider;
|
||||||
|
|
||||||
|
buildSecureRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSecureRandom() {
|
||||||
|
try {
|
||||||
|
if (this.algorithm == null && this.provider == null) {
|
||||||
|
this.secureRandom = new SecureRandom();
|
||||||
|
} else if (this.provider == null) {
|
||||||
|
this.secureRandom = SecureRandom.getInstance(this.algorithm);
|
||||||
|
} else {
|
||||||
|
this.secureRandom = SecureRandom.getInstance(this.algorithm, this.provider);
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified algorithm: %s. "
|
||||||
|
+ "Another provider can be chosen setting the %s system property.", this.algorithm, GoogleAuthenticator.RNG_ALGORITHM), e);
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified provider: %s. "
|
||||||
|
+ "Another provider can be chosen setting the %s system property.", this.provider, GoogleAuthenticator.RNG_ALGORITHM_PROVIDER), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextBytes(byte[] bytes) {
|
||||||
|
if (count.incrementAndGet() > MAX_OPERATIONS) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (count.get() > MAX_OPERATIONS) {
|
||||||
|
buildSecureRandom();
|
||||||
|
count.set(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.secureRandom.nextBytes(bytes);
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
db.version=8
|
db.version=9
|
@ -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';
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.sismics.util;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.sismics.util.totp.GoogleAuthenticator;
|
||||||
|
import com.sismics.util.totp.GoogleAuthenticatorKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test of {@link GoogleAuthenticator}
|
||||||
|
*
|
||||||
|
* @author bgamard
|
||||||
|
*/
|
||||||
|
public class TestGoogleAuthenticator {
|
||||||
|
@Test
|
||||||
|
public void testGoogleAuthenticator() {
|
||||||
|
GoogleAuthenticator gAuth = new GoogleAuthenticator();
|
||||||
|
GoogleAuthenticatorKey key = gAuth.createCredentials();
|
||||||
|
Assert.assertNotNull(key.getVerificationCode());
|
||||||
|
Assert.assertEquals(5, key.getScratchCodes().size());
|
||||||
|
int validationCode = gAuth.calculateCode(key.getKey(), new Date().getTime() / 30000);
|
||||||
|
Assert.assertTrue(gAuth.authorize(key.getKey(), validationCode));
|
||||||
|
}
|
||||||
|
}
|
@ -395,13 +395,15 @@
|
|||||||
<version>${com.twelvemonkeys.imageio.version}</version>
|
<version>${com.twelvemonkeys.imageio.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency><!-- Only JBIG2 -->
|
<!-- Only JBIG2 -->
|
||||||
|
<dependency>
|
||||||
<groupId>com.levigo.jbig2</groupId>
|
<groupId>com.levigo.jbig2</groupId>
|
||||||
<artifactId>levigo-jbig2-imageio</artifactId>
|
<artifactId>levigo-jbig2-imageio</artifactId>
|
||||||
<version>${com.levigo.jbig2.levigo-jbig2-imageio.version}</version>
|
<version>${com.levigo.jbig2.levigo-jbig2-imageio.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency><!-- Essentially TIFF (for OCR) -->
|
<!-- Essentially TIFF (for OCR) -->
|
||||||
|
<dependency>
|
||||||
<groupId>com.github.jai-imageio</groupId>
|
<groupId>com.github.jai-imageio</groupId>
|
||||||
<artifactId>jai-imageio-core</artifactId>
|
<artifactId>jai-imageio-core</artifactId>
|
||||||
<version>${com.github.jai-imageio.jai-imageio-core.version}</version>
|
<version>${com.github.jai-imageio.jai-imageio-core.version}</version>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -55,6 +55,8 @@ import com.sismics.rest.util.JsonUtil;
|
|||||||
import com.sismics.rest.util.ValidationUtil;
|
import com.sismics.rest.util.ValidationUtil;
|
||||||
import com.sismics.security.UserPrincipal;
|
import com.sismics.security.UserPrincipal;
|
||||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||||
|
import com.sismics.util.totp.GoogleAuthenticator;
|
||||||
|
import com.sismics.util.totp.GoogleAuthenticatorKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User REST resources.
|
* User REST resources.
|
||||||
@ -253,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);
|
||||||
@ -260,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)) {
|
||||||
@ -273,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;
|
||||||
@ -470,7 +488,8 @@ public class UserResource extends BaseResource {
|
|||||||
response.add("username", user.getUsername())
|
response.add("username", user.getUsername())
|
||||||
.add("email", user.getEmail())
|
.add("email", user.getEmail())
|
||||||
.add("storage_quota", user.getStorageQuota())
|
.add("storage_quota", user.getStorageQuota())
|
||||||
.add("storage_current", user.getStorageCurrent());
|
.add("storage_current", user.getStorageCurrent())
|
||||||
|
.add("totp_enabled", user.getTotpKey() != null);
|
||||||
|
|
||||||
// Base functions
|
// Base functions
|
||||||
JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
|
JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
|
||||||
@ -639,6 +658,66 @@ public class UserResource extends BaseResource {
|
|||||||
return Response.ok().entity(response.build()).build();
|
return Response.ok().entity(response.build()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable time-based one-time password.
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("enable_totp")
|
||||||
|
public Response enableTotp() {
|
||||||
|
if (!authenticate()) {
|
||||||
|
throw new ForbiddenClientException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new TOTP key
|
||||||
|
GoogleAuthenticator gAuth = new GoogleAuthenticator();
|
||||||
|
final GoogleAuthenticatorKey key = gAuth.createCredentials();
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
return Response.ok().entity(response.build()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable time-based one-time password.
|
||||||
|
*
|
||||||
|
* @param password Password
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("disable_totp")
|
||||||
|
public Response disableTotp(@FormParam("password") String password) {
|
||||||
|
if (!authenticate()) {
|
||||||
|
throw new ForbiddenClientException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the input data
|
||||||
|
password = ValidationUtil.validateLength(password, "password", 1, 100, false);
|
||||||
|
|
||||||
|
// Check the password and get the user
|
||||||
|
UserDao userDao = new UserDao();
|
||||||
|
User user = userDao.authenticate(principal.getName(), password);
|
||||||
|
if (user == null) {
|
||||||
|
throw new ForbiddenClientException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the TOTP key
|
||||||
|
user.setTotpKey(null);
|
||||||
|
userDao.update(user, principal.getId());
|
||||||
|
|
||||||
|
// Always return OK
|
||||||
|
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||||
|
.add("status", "ok");
|
||||||
|
return Response.ok().entity(response.build()).build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication token value.
|
* Returns the authentication token value.
|
||||||
*
|
*
|
||||||
|
@ -22,7 +22,7 @@ module.exports = function(grunt) {
|
|||||||
separator: ';'
|
separator: ';'
|
||||||
},
|
},
|
||||||
src: ['src/lib/jquery.js','src/lib/jquery.ui.js','src/lib/underscore.js','src/lib/colorpicker.js', 'src/lib/angular.js', 'src/lib/angular.*.js',
|
src: ['src/lib/jquery.js','src/lib/jquery.ui.js','src/lib/underscore.js','src/lib/colorpicker.js', 'src/lib/angular.js', 'src/lib/angular.*.js',
|
||||||
'dist/app/docs/app.js', 'dist/app/docs/controller/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'],
|
'dist/app/docs/app.js', 'dist/app/docs/controller/**/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'],
|
||||||
dest: 'dist/docs.js'
|
dest: 'dist/docs.js'
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
angular.module('docs',
|
angular.module('docs',
|
||||||
// Dependencies
|
// Dependencies
|
||||||
['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog',
|
['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', 'ngProgress', 'monospaced.qrcode',
|
||||||
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload']
|
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload']
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,6 +61,15 @@ angular.module('docs',
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.state('settings.security', {
|
||||||
|
url: '/security',
|
||||||
|
views: {
|
||||||
|
'settings': {
|
||||||
|
templateUrl: 'partial/docs/settings.security.html',
|
||||||
|
controller: 'SettingsSecurity'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('settings.session', {
|
.state('settings.session', {
|
||||||
url: '/session',
|
url: '/session',
|
||||||
views: {
|
views: {
|
||||||
@ -345,4 +354,21 @@ angular.module('docs',
|
|||||||
$state.go(redirect, toParams);
|
$state.go(redirect, toParams);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Initialize ngProgress.
|
||||||
|
*/
|
||||||
|
.run(function($rootScope, ngProgressFactory, $http) {
|
||||||
|
$rootScope.ngProgress = ngProgressFactory.createInstance();
|
||||||
|
|
||||||
|
// Watch for the number of XHR running
|
||||||
|
$rootScope.$watch(function() {
|
||||||
|
return $http.pendingRequests.length > 0
|
||||||
|
}, function(count) {
|
||||||
|
if (count == 0) {
|
||||||
|
$rootScope.ngProgress.complete();
|
||||||
|
} else {
|
||||||
|
$rootScope.ngProgress.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
@ -4,18 +4,28 @@
|
|||||||
* Login controller.
|
* Login controller.
|
||||||
*/
|
*/
|
||||||
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
|
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
|
||||||
|
$scope.codeRequired = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login.
|
||||||
|
*/
|
||||||
$scope.login = function() {
|
$scope.login = function() {
|
||||||
User.login($scope.user).then(function() {
|
User.login($scope.user).then(function() {
|
||||||
User.userInfo(true).then(function(data) {
|
User.userInfo(true).then(function(data) {
|
||||||
$rootScope.userInfo = data;
|
$rootScope.userInfo = data;
|
||||||
});
|
});
|
||||||
$state.go('document.default');
|
$state.go('document.default');
|
||||||
}, function() {
|
}, function(data) {
|
||||||
|
if (data.data.type == 'ValidationCodeRequired') {
|
||||||
|
// A TOTP validation code is required to login
|
||||||
|
$scope.codeRequired = true;
|
||||||
|
} else {
|
||||||
|
// Login truly failed
|
||||||
var title = 'Login failed';
|
var title = 'Login failed';
|
||||||
var msg = 'Username or password invalid';
|
var msg = 'Username or password invalid';
|
||||||
var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}];
|
var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}];
|
||||||
|
|
||||||
$dialog.messageBox(title, msg, btns);
|
$dialog.messageBox(title, msg, btns);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
@ -56,11 +56,4 @@ angular.module('docs').controller('Navigation', function($scope, $http, $state,
|
|||||||
});
|
});
|
||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if at least an asynchronous request is in progress.
|
|
||||||
*/
|
|
||||||
$scope.isLoading = function() {
|
|
||||||
return $http.pendingRequests.length > 0;
|
|
||||||
};
|
|
||||||
});
|
});
|
@ -0,0 +1,53 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings security controller.
|
||||||
|
*/
|
||||||
|
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular) {
|
||||||
|
User.userInfo().then(function(data) {
|
||||||
|
$scope.user = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable TOTP.
|
||||||
|
*/
|
||||||
|
$scope.enableTotp = function() {
|
||||||
|
var title = 'Enable two-factor authentication';
|
||||||
|
var msg = 'Make sure you have a TOTP-compatible application on your phone ready to add a new account';
|
||||||
|
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
|
||||||
|
|
||||||
|
$dialog.messageBox(title, msg, btns, function(result) {
|
||||||
|
if (result == 'ok') {
|
||||||
|
Restangular.one('user/enable_totp').post().then(function(data) {
|
||||||
|
$scope.secret = data.secret;
|
||||||
|
User.userInfo(true).then(function(data) {
|
||||||
|
$scope.user = data;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable TOTP.
|
||||||
|
*/
|
||||||
|
$scope.disableTotp = function() {
|
||||||
|
$modal.open({
|
||||||
|
templateUrl: 'partial/docs/settings.security.disabletotp.html',
|
||||||
|
controller: 'SettingsSecurityModalDisableTotp'
|
||||||
|
}).result.then(function (password) {
|
||||||
|
if (password == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable TOTP
|
||||||
|
Restangular.one('user/disable_totp').post('', {
|
||||||
|
password: password
|
||||||
|
}).then(function() {
|
||||||
|
User.userInfo(true).then(function(data) {
|
||||||
|
$scope.user = data;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,11 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings modal disable TOTP controller.
|
||||||
|
*/
|
||||||
|
angular.module('docs').controller('SettingsSecurityModalDisableTotp', function ($scope, $modalInstance) {
|
||||||
|
$scope.password = '';
|
||||||
|
$scope.close = function(password) {
|
||||||
|
$modalInstance.close(password);
|
||||||
|
}
|
||||||
|
});
|
@ -34,8 +34,8 @@ angular.module('docs').controller('SettingsVocabulary', function($scope, Restang
|
|||||||
// Add an entry
|
// Add an entry
|
||||||
$scope.addEntry = function(entry) {
|
$scope.addEntry = function(entry) {
|
||||||
entry.name = $scope.vocabulary;
|
entry.name = $scope.vocabulary;
|
||||||
Restangular.one('vocabulary').put(entry).then(function() {
|
Restangular.one('vocabulary').put(entry).then(function(data) {
|
||||||
$scope.entries.push(entry);
|
$scope.entries.push(data);
|
||||||
$scope.entry = {};
|
$scope.entry = {};
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -37,35 +37,39 @@
|
|||||||
<script src="lib/angular.restangular.js" type="text/javascript"></script>
|
<script src="lib/angular.restangular.js" type="text/javascript"></script>
|
||||||
<script src="lib/angular.colorpicker.js" type="text/javascript"></script>
|
<script src="lib/angular.colorpicker.js" type="text/javascript"></script>
|
||||||
<script src="lib/angular.file-upload.js" type="text/javascript"></script>
|
<script src="lib/angular.file-upload.js" type="text/javascript"></script>
|
||||||
|
<script src="lib/angular.ngprogress.js" type="text/javascript"></script>
|
||||||
|
<script src="lib/angular.qrcode.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/app.js" type="text/javascript"></script>
|
<script src="app/docs/app.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Main.js" type="text/javascript"></script>
|
<script src="app/docs/controller/Main.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Document.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/Document.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentDefault.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentDefault.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentEdit.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentEdit.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentView.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentView.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentViewContent.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentViewContent.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentViewPermissions.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentViewPermissions.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentViewActivity.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentViewActivity.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentModalShare.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentModalShare.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/DocumentModalPdf.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/DocumentModalPdf.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/FileView.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/FileView.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/FileModalView.js" type="text/javascript"></script>
|
<script src="app/docs/controller/document/FileModalView.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Login.js" type="text/javascript"></script>
|
<script src="app/docs/controller/Login.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Tag.js" type="text/javascript"></script>
|
<script src="app/docs/controller/tag/Tag.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Navigation.js" type="text/javascript"></script>
|
<script src="app/docs/controller/Navigation.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/Settings.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsDefault.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsDefault.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsAccount.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsAccount.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsSession.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsSecurity.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsLog.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsSecurityModalDisableTotp.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsUser.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsSession.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsUserEdit.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsLog.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsGroup.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsUser.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsGroupEdit.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsUserEdit.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/SettingsVocabulary.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsGroup.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/UserGroup.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsGroupEdit.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/UserProfile.js" type="text/javascript"></script>
|
<script src="app/docs/controller/settings/SettingsVocabulary.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/controller/GroupProfile.js" type="text/javascript"></script>
|
<script src="app/docs/controller/usergroup/UserGroup.js" type="text/javascript"></script>
|
||||||
|
<script src="app/docs/controller/usergroup/UserProfile.js" type="text/javascript"></script>
|
||||||
|
<script src="app/docs/controller/usergroup/GroupProfile.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/service/User.js" type="text/javascript"></script>
|
<script src="app/docs/service/User.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/service/Tag.js" type="text/javascript"></script>
|
<script src="app/docs/service/Tag.js" type="text/javascript"></script>
|
||||||
<script src="app/docs/filter/Newline.js" type="text/javascript"></script>
|
<script src="app/docs/filter/Newline.js" type="text/javascript"></script>
|
||||||
@ -92,10 +96,6 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="navbar-brand loader" ng-class="{'loader-hide': !isLoading() }">
|
|
||||||
<img src="img/loader.gif" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden-xs navbar-text navbar-logo">
|
<div class="hidden-xs navbar-text navbar-logo">
|
||||||
<img src="favicon.png" />
|
<img src="favicon.png" />
|
||||||
</div>
|
</div>
|
||||||
|
220
docs-web/src/main/webapp/src/lib/angular.ngprogress.js
Normal file
220
docs-web/src/main/webapp/src/lib/angular.ngprogress.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
ngprogress 1.1.2 - slim, site-wide progressbar for AngularJS
|
||||||
|
(C) 2013 - Victor Bjelkholm
|
||||||
|
License: MIT
|
||||||
|
Source: https://github.com/VictorBjelkholm/ngProgress
|
||||||
|
Date Compiled: 2015-07-27
|
||||||
|
*/
|
||||||
|
angular.module('ngProgress.provider', ['ngProgress.directive'])
|
||||||
|
.service('ngProgress', function () {
|
||||||
|
'use strict';
|
||||||
|
return ['$document', '$window', '$compile', '$rootScope', '$timeout', function($document, $window, $compile, $rootScope, $timeout) {
|
||||||
|
this.autoStyle = true;
|
||||||
|
this.count = 0;
|
||||||
|
this.height = '2px';
|
||||||
|
this.$scope = $rootScope.$new();
|
||||||
|
this.color = 'white';
|
||||||
|
this.parent = $document.find('body')[0];
|
||||||
|
this.count = 0;
|
||||||
|
|
||||||
|
// Compile the directive
|
||||||
|
this.progressbarEl = $compile('<ng-progress></ng-progress>')(this.$scope);
|
||||||
|
// Add the element to body
|
||||||
|
this.parent.appendChild(this.progressbarEl[0]);
|
||||||
|
// Set the initial height
|
||||||
|
this.$scope.count = this.count;
|
||||||
|
// If height or color isn't undefined, set the height, background-color and color.
|
||||||
|
if (this.height !== undefined) {
|
||||||
|
this.progressbarEl.eq(0).children().css('height', this.height);
|
||||||
|
}
|
||||||
|
if (this.color !== undefined) {
|
||||||
|
this.progressbarEl.eq(0).children().css('background-color', this.color);
|
||||||
|
this.progressbarEl.eq(0).children().css('color', this.color);
|
||||||
|
}
|
||||||
|
// The ID for the interval controlling start()
|
||||||
|
this.intervalCounterId = 0;
|
||||||
|
|
||||||
|
// Starts the animation and adds between 0 - 5 percent to loading
|
||||||
|
// each 400 milliseconds. Should always be finished with progressbar.complete()
|
||||||
|
// to hide it
|
||||||
|
this.start = function () {
|
||||||
|
// TODO Use requestAnimationFrame instead of setInterval
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
|
||||||
|
this.show();
|
||||||
|
var self = this;
|
||||||
|
clearInterval(this.intervalCounterId);
|
||||||
|
this.intervalCounterId = setInterval(function () {
|
||||||
|
if (isNaN(self.count)) {
|
||||||
|
clearInterval(self.intervalCounterId);
|
||||||
|
self.count = 0;
|
||||||
|
self.hide();
|
||||||
|
} else {
|
||||||
|
self.remaining = 100 - self.count;
|
||||||
|
self.count = self.count + (0.15 * Math.pow(1 - Math.sqrt(self.remaining), 2));
|
||||||
|
self.updateCount(self.count);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
this.updateCount = function (new_count) {
|
||||||
|
this.$scope.count = new_count;
|
||||||
|
if(!this.$scope.$$phase) {
|
||||||
|
this.$scope.$apply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Sets the height of the progressbar. Use any valid CSS value
|
||||||
|
// Eg '10px', '1em' or '1%'
|
||||||
|
this.setHeight = function (new_height) {
|
||||||
|
if (new_height !== undefined) {
|
||||||
|
this.height = new_height;
|
||||||
|
this.$scope.height = this.height;
|
||||||
|
if(!this.$scope.$$phase) {
|
||||||
|
this.$scope.$apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.height;
|
||||||
|
};
|
||||||
|
// Sets the color of the progressbar and it's shadow. Use any valid HTML
|
||||||
|
// color
|
||||||
|
this.setColor = function(new_color) {
|
||||||
|
if (new_color !== undefined) {
|
||||||
|
this.color = new_color;
|
||||||
|
this.$scope.color = this.color;
|
||||||
|
if(!this.$scope.$$phase) {
|
||||||
|
this.$scope.$apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.color;
|
||||||
|
};
|
||||||
|
this.hide = function() {
|
||||||
|
this.progressbarEl.children().css('opacity', '0');
|
||||||
|
var self = this;
|
||||||
|
self.animate(function () {
|
||||||
|
self.progressbarEl.children().css('width', '0%');
|
||||||
|
self.animate(function () {
|
||||||
|
self.show();
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
this.show = function () {
|
||||||
|
var self = this;
|
||||||
|
self.animate(function () {
|
||||||
|
self.progressbarEl.children().css('opacity', '1');
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
// Cancel any prior animations before running new ones.
|
||||||
|
// Multiple simultaneous animations just look weird.
|
||||||
|
this.animate = function(fn, time) {
|
||||||
|
if(this.animation !== undefined) { $timeout.cancel(this.animation); }
|
||||||
|
this.animation = $timeout(fn, time);
|
||||||
|
};
|
||||||
|
// Returns on how many percent the progressbar is at. Should'nt be needed
|
||||||
|
this.status = function () {
|
||||||
|
return this.count;
|
||||||
|
};
|
||||||
|
// Stops the progressbar at it's current location
|
||||||
|
this.stop = function () {
|
||||||
|
clearInterval(this.intervalCounterId);
|
||||||
|
};
|
||||||
|
// Set's the progressbar percentage. Use a number between 0 - 100.
|
||||||
|
// If 100 is provided, complete will be called.
|
||||||
|
this.set = function (new_count) {
|
||||||
|
this.show();
|
||||||
|
this.updateCount(new_count);
|
||||||
|
this.count = new_count;
|
||||||
|
clearInterval(this.intervalCounterId);
|
||||||
|
return this.count;
|
||||||
|
};
|
||||||
|
this.css = function (args) {
|
||||||
|
return this.progressbarEl.children().css(args);
|
||||||
|
};
|
||||||
|
// Resets the progressbar to percetage 0 and therefore will be hided after
|
||||||
|
// it's rollbacked
|
||||||
|
this.reset = function () {
|
||||||
|
clearInterval(this.intervalCounterId);
|
||||||
|
this.count = 0;
|
||||||
|
this.updateCount(this.count);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
// Jumps to 100% progress and fades away progressbar.
|
||||||
|
this.complete = function () {
|
||||||
|
this.count = 100;
|
||||||
|
this.updateCount(this.count);
|
||||||
|
var self = this;
|
||||||
|
clearInterval(this.intervalCounterId);
|
||||||
|
$timeout(function () {
|
||||||
|
self.hide();
|
||||||
|
$timeout(function () {
|
||||||
|
self.count = 0;
|
||||||
|
self.updateCount(self.count);
|
||||||
|
}, 200);
|
||||||
|
}, 500);
|
||||||
|
return this.count;
|
||||||
|
};
|
||||||
|
// Set the parent of the directive, sometimes body is not sufficient
|
||||||
|
this.setParent = function(newParent) {
|
||||||
|
if(newParent === null || newParent === undefined) {
|
||||||
|
throw new Error('Provide a valid parent of type HTMLElement');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.parent !== null && this.parent !== undefined) {
|
||||||
|
this.parent.removeChild(this.progressbarEl[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parent = newParent;
|
||||||
|
this.parent.appendChild(this.progressbarEl[0]);
|
||||||
|
};
|
||||||
|
// Gets the current element the progressbar is attached to
|
||||||
|
this.getDomElement = function () {
|
||||||
|
return this.progressbarEl;
|
||||||
|
};
|
||||||
|
this.setAbsolute = function() {
|
||||||
|
this.progressbarEl.css('position', 'absolute');
|
||||||
|
};
|
||||||
|
}];
|
||||||
|
})
|
||||||
|
.factory('ngProgressFactory', ['$injector', 'ngProgress', function($injector, ngProgress) {
|
||||||
|
var service = {
|
||||||
|
createInstance: function () {
|
||||||
|
return $injector.instantiate(ngProgress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return service;
|
||||||
|
}]);
|
||||||
|
angular.module('ngProgress.directive', [])
|
||||||
|
.directive('ngProgress', ["$window", "$rootScope", function ($window, $rootScope) {
|
||||||
|
var directiveObj = {
|
||||||
|
// Replace the directive
|
||||||
|
replace: true,
|
||||||
|
// Only use as a element
|
||||||
|
restrict: 'E',
|
||||||
|
link: function ($scope, $element, $attrs, $controller) {
|
||||||
|
// Watch the count on the $rootScope. As soon as count changes to something that
|
||||||
|
// isn't undefined or null, change the counter on $scope and also the width of
|
||||||
|
// the progressbar. The same goes for color and height on the $rootScope
|
||||||
|
$scope.$watch('count', function (newVal) {
|
||||||
|
if (newVal !== undefined || newVal !== null) {
|
||||||
|
$scope.counter = newVal;
|
||||||
|
$element.eq(0).children().css('width', newVal + '%');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.$watch('color', function (newVal) {
|
||||||
|
if (newVal !== undefined || newVal !== null) {
|
||||||
|
$scope.color = newVal;
|
||||||
|
$element.eq(0).children().css('background-color', newVal);
|
||||||
|
$element.eq(0).children().css('color', newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.$watch('height', function (newVal) {
|
||||||
|
if (newVal !== undefined || newVal !== null) {
|
||||||
|
$scope.height = newVal;
|
||||||
|
$element.eq(0).children().css('height', newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// The actual html that will be used
|
||||||
|
template: '<div id="ngProgress-container"><div id="ngProgress"></div></div>'
|
||||||
|
};
|
||||||
|
return directiveObj;
|
||||||
|
}]);
|
||||||
|
|
||||||
|
angular.module('ngProgress', ['ngProgress.directive', 'ngProgress.provider']);
|
2015
docs-web/src/main/webapp/src/lib/angular.qrcode.js
Normal file
2015
docs-web/src/main/webapp/src/lib/angular.qrcode.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,15 @@
|
|||||||
<input class="form-control" type="password" id="inputPassword" placeholder="Password" ng-model="user.password" />
|
<input class="form-control" type="password" id="inputPassword" placeholder="Password" ng-model="user.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span class="help-block" ng-if="codeRequired">
|
||||||
|
A validation code is required
|
||||||
|
<span class="glyphicon glyphicon-question-sign" title="You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app your configured."></span>
|
||||||
|
</span>
|
||||||
|
<div class="form-group" ng-if="codeRequired">
|
||||||
|
<label class="sr-only" for="inputCode">Validation code</label>
|
||||||
|
<input class="form-control" type="text" id="inputCode" placeholder="Validation code" ng-model="user.code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ng-model="user.remember" /> Remember me
|
<input type="checkbox" ng-model="user.remember" /> Remember me
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Personal settings</strong></div>
|
<div class="panel-heading"><strong>Personal settings</strong></div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/account" href="#/settings/account">User account</a>
|
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/account" href="#/settings/account">User account</a>
|
||||||
|
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/security" href="#/settings/security">Two-factor authentication</a>
|
||||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/session" href="#/settings/session">Opened sessions</a>
|
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/session" href="#/settings/session">Opened sessions</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -19,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-9">
|
||||||
<div ui-view="settings"></div>
|
<div ui-view="settings"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,18 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h3>Disable two-factor authentication</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-danger">
|
||||||
|
Your account will not be protected by the two-factor authentication anymore.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="password">Confirm your password</label>
|
||||||
|
<input class="form-control" type="password" id="password" ng-model="password" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button ng-click="close(password)" class="btn btn-warning">
|
||||||
|
Disable two-factor authentication
|
||||||
|
</button>
|
||||||
|
<button ng-click="close(null)" class="btn btn-default">Cancel</button>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
<h1>
|
||||||
|
Two-factor <small>authentication</small>
|
||||||
|
<span class="label" ng-class="{ 'label-success': user.totp_enabled, 'label-danger': !user.totp_enabled }">
|
||||||
|
{{ user.totp_enabled ? 'Enabled' : 'Disabled' }}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div ng-if="!user.totp_enabled">
|
||||||
|
<p>
|
||||||
|
Two-factor authentication allows you to add a layer of security on your <strong>Sismics Docs</strong> account.<br/>
|
||||||
|
Before activating this feature, make sure you have a TOTP-compatible app on your phone:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>For Android, iOS, and Blackberry: <a href="https://support.google.com/accounts/answer/1066447" target="_blank">Google Authenticator</a></li>
|
||||||
|
<li>For Android and iOS: <a href="https://guide.duo.com/third-party-accounts" target="_blank">Duo Mobile</a></li>
|
||||||
|
<li>For Windows Phone: <a href="https://www.microsoft.com/en-US/store/apps/Authenticator/9WZDNCRFJ3RJ" target="_blank">Authenticator</a></li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Those applications automatically generate a validation code that changes after a certain period of time.<br/>
|
||||||
|
You will be required to enter this validation code each time you login on <strong>Sismics Docs</strong>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-primary" ng-click="enableTotp()">Enable two-factor authentication</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="user.totp_enabled">
|
||||||
|
<div ng-if="secret">
|
||||||
|
<p>Your secret key is: <strong>{{ secret }}</strong></p>
|
||||||
|
<qrcode data="otpauth://totp/Sismics%20Docs?secret={{ secret }}" size="200"></qrcode>
|
||||||
|
<p class="text-danger">
|
||||||
|
<strong>Configure your TOTP app on your phone with this secret key now, you will not be able to access it later.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Two-factor authentication is enabled on your account.<br/>
|
||||||
|
Each time you login on <strong>Sismics Docs</strong>, you will be asked a validation code from your configured phone app.<br/>
|
||||||
|
If you loose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-warning" ng-click="disableTotp()">Disable two-factor authentication</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
@ -240,5 +240,38 @@ input[readonly].share-link {
|
|||||||
.login-box {
|
.login-box {
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: rgba(255, 255, 255, 0.5);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 4px
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.help-block, .checkbox {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for the ngProgress itself */
|
||||||
|
#ngProgress {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 99998;
|
||||||
|
background-color: green;
|
||||||
|
color: green;
|
||||||
|
box-shadow: 0 0 10px 0; /* Inherits the font color */
|
||||||
|
height: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
/* Add CSS3 styles for transition smoothing */
|
||||||
|
-webkit-transition: all 0.5s ease-in-out;
|
||||||
|
-moz-transition: all 0.5s ease-in-out;
|
||||||
|
-o-transition: all 0.5s ease-in-out;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for the ngProgress-container */
|
||||||
|
#ngProgress-container {
|
||||||
|
position: fixed;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99999;
|
||||||
}
|
}
|
@ -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
|
@ -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
|
@ -23,8 +23,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestAclResource extends BaseJerseyTest {
|
public class TestAclResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the ACL resource.
|
* Test the ACL resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testAclResource() {
|
public void testAclResource() {
|
||||||
|
@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestAppResource extends BaseJerseyTest {
|
public class TestAppResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the API resource.
|
* Test the API resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testAppResource() {
|
public void testAppResource() {
|
||||||
@ -63,8 +61,6 @@ public class TestAppResource extends BaseJerseyTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the log resource.
|
* Test the log resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testLogResource() {
|
public void testLogResource() {
|
||||||
|
@ -20,8 +20,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestAuditLogResource extends BaseJerseyTest {
|
public class TestAuditLogResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the audit log resource.
|
* Test the audit log resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testAuditLogResource() {
|
public void testAuditLogResource() {
|
||||||
|
@ -21,11 +21,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestCommentResource extends BaseJerseyTest {
|
public class TestCommentResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the comment resource.
|
* Test the comment resource.
|
||||||
*
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testCommentResource() throws Exception {
|
public void testCommentResource() {
|
||||||
// Login comment1
|
// Login comment1
|
||||||
clientUtil.createUser("comment1");
|
clientUtil.createUser("comment1");
|
||||||
String comment1Token = clientUtil.login("comment1");
|
String comment1Token = clientUtil.login("comment1");
|
||||||
|
@ -359,9 +359,8 @@ public class TestDocumentResource extends BaseJerseyTest {
|
|||||||
* @param query Search query
|
* @param query Search query
|
||||||
* @param token Authentication token
|
* @param token Authentication token
|
||||||
* @return Number of documents found
|
* @return Number of documents found
|
||||||
* @throws Exception
|
|
||||||
*/
|
*/
|
||||||
private int searchDocuments(String query, String token) throws Exception {
|
private int searchDocuments(String query, String token) {
|
||||||
JsonObject json = target().path("/document/list")
|
JsonObject json = target().path("/document/list")
|
||||||
.queryParam("search", query)
|
.queryParam("search", query)
|
||||||
.request()
|
.request()
|
||||||
|
@ -198,6 +198,11 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals(1, files.size());
|
Assert.assertEquals(1, files.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test orphan files (without linked document).
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testOrphanFile() throws Exception {
|
public void testOrphanFile() throws Exception {
|
||||||
// Login file2
|
// Login file2
|
||||||
@ -283,6 +288,11 @@ public class TestFileResource extends BaseJerseyTest {
|
|||||||
Assert.assertEquals("ok", json.getString("status"));
|
Assert.assertEquals("ok", json.getString("status"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test user quota.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testQuota() throws Exception {
|
public void testQuota() throws Exception {
|
||||||
// Login file_quota
|
// Login file_quota
|
||||||
|
@ -22,8 +22,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestGroupResource extends BaseJerseyTest {
|
public class TestGroupResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the group resource.
|
* Test the group resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testGroupResource() {
|
public void testGroupResource() {
|
||||||
|
@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestSecurity extends BaseJerseyTest {
|
public class TestSecurity extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test of the security layer.
|
* Test of the security layer.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testSecurity() {
|
public void testSecurity() {
|
||||||
|
@ -28,7 +28,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestShareResource extends BaseJerseyTest {
|
public class TestShareResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the share resource.
|
* Test the share resource.
|
||||||
* @throws Exception
|
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestTagResource extends BaseJerseyTest {
|
public class TestTagResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the tag resource.
|
* Test the tag resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testTagResource() {
|
public void testTagResource() {
|
||||||
|
@ -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.
|
||||||
@ -22,8 +24,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestUserResource extends BaseJerseyTest {
|
public class TestUserResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the user resource.
|
* Test the user resource.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testUserResource() {
|
public void testUserResource() {
|
||||||
@ -229,8 +229,6 @@ public class TestUserResource extends BaseJerseyTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the user resource admin functions.
|
* Test the user resource admin functions.
|
||||||
*
|
|
||||||
* @throws JSONException
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testUserResourceAdmin() {
|
public void testUserResourceAdmin() {
|
||||||
@ -290,4 +288,71 @@ public class TestUserResource extends BaseJerseyTest {
|
|||||||
json = response.readEntity(JsonObject.class);
|
json = response.readEntity(JsonObject.class);
|
||||||
Assert.assertEquals("UserNotFound", json.getString("type"));
|
Assert.assertEquals("UserNotFound", json.getString("type"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTotp() {
|
||||||
|
// Create totp1 user
|
||||||
|
clientUtil.createUser("totp1");
|
||||||
|
String totp1Token = clientUtil.login("totp1");
|
||||||
|
|
||||||
|
// Check TOTP enablement
|
||||||
|
JsonObject json = target().path("/user").request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
Assert.assertFalse(json.getBoolean("totp_enabled"));
|
||||||
|
|
||||||
|
// Enable TOTP for totp1
|
||||||
|
json = target().path("/user/enable_totp").request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
// Check TOTP enablement
|
||||||
|
json = target().path("/user").request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
Assert.assertTrue(json.getBoolean("totp_enabled"));
|
||||||
|
|
||||||
|
// Disable TOTP for totp1
|
||||||
|
json = target().path("/user/disable_totp").request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
|
||||||
|
.post(Entity.form(new Form()
|
||||||
|
.param("password", "12345678")), JsonObject.class);
|
||||||
|
|
||||||
|
// Login with totp1 without a validation code
|
||||||
|
json = target().path("/user/login").request()
|
||||||
|
.post(Entity.form(new Form()
|
||||||
|
.param("username", "totp1")
|
||||||
|
.param("password", "12345678")
|
||||||
|
.param("remember", "false")), JsonObject.class);
|
||||||
|
|
||||||
|
// Check TOTP enablement
|
||||||
|
json = target().path("/user").request()
|
||||||
|
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
|
||||||
|
.get(JsonObject.class);
|
||||||
|
Assert.assertFalse(json.getBoolean("totp_enabled"));
|
||||||
|
}
|
||||||
}
|
}
|
@ -19,11 +19,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
|
|||||||
public class TestVocabularyResource extends BaseJerseyTest {
|
public class TestVocabularyResource extends BaseJerseyTest {
|
||||||
/**
|
/**
|
||||||
* Test the vocabulary resource.
|
* Test the vocabulary resource.
|
||||||
*
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testVocabularyResource() throws Exception {
|
public void testVocabularyResource() {
|
||||||
// Login vocabulary1
|
// Login vocabulary1
|
||||||
clientUtil.createUser("vocabulary1");
|
clientUtil.createUser("vocabulary1");
|
||||||
String vocabulary1Token = clientUtil.login("vocabulary1");
|
String vocabulary1Token = clientUtil.login("vocabulary1");
|
||||||
|
Loading…
Reference in New Issue
Block a user