diff --git a/plume-admin-security/pom.xml b/plume-admin-security/pom.xml
index 5aa4f40..3e106ee 100644
--- a/plume-admin-security/pom.xml
+++ b/plume-admin-security/pom.xml
@@ -83,6 +83,23 @@
assertj-core
test
+
+
+
+ com.warrenstrange
+ googleauth
+ 1.5.0
+
+
+ com.google.zxing
+ core
+ 3.4.1
+
+
+ com.google.zxing
+ javase
+ 3.4.1
+
@@ -97,4 +114,4 @@
-
\ No newline at end of file
+
diff --git a/plume-admin-security/src/main/java/com/coreoz/plume/admin/services/configuration/AdminSecurityConfigurationService.java b/plume-admin-security/src/main/java/com/coreoz/plume/admin/services/configuration/AdminSecurityConfigurationService.java
index 80f242e..d1bb79c 100644
--- a/plume-admin-security/src/main/java/com/coreoz/plume/admin/services/configuration/AdminSecurityConfigurationService.java
+++ b/plume-admin-security/src/main/java/com/coreoz/plume/admin/services/configuration/AdminSecurityConfigurationService.java
@@ -24,6 +24,10 @@ public String jwtSecret() {
return config.getString("admin.jwt-secret");
}
+ public String mfaSecret() {
+ return config.getString("admin.mfa-secret");
+ }
+
public boolean sessionUseFingerprintCookie() {
return config.getBoolean("admin.session.use-fingerprint-cookie");
}
diff --git a/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryption.java b/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryption.java
new file mode 100644
index 0000000..92bca90
--- /dev/null
+++ b/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryption.java
@@ -0,0 +1,57 @@
+package com.coreoz.plume.admin.websession;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class MfaSecretKeyEncryption {
+
+ private static final String ALGORITHM = "AES/GCM/NoPadding";
+ private static final int IV_SIZE = 12; // 96 bits
+ private static final int TAG_SIZE = 128; // 128 bits
+ private final SecretKey secretKey;
+
+ public MfaSecretKeyEncryption(String base64SecretKey) {
+ byte[] decodedKey = Base64.getDecoder().decode(base64SecretKey);
+ this.secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
+ }
+
+ public String encrypt(String data) throws Exception {
+ Cipher cipher = Cipher.getInstance(ALGORITHM);
+ byte[] iv = new byte[IV_SIZE];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(iv);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_SIZE, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
+ byte[] encrypted = cipher.doFinal(data.getBytes());
+ byte[] encryptedWithIv = new byte[IV_SIZE + encrypted.length];
+ System.arraycopy(iv, 0, encryptedWithIv, 0, IV_SIZE);
+ System.arraycopy(encrypted, 0, encryptedWithIv, IV_SIZE, encrypted.length);
+ return Base64.getEncoder().encodeToString(encryptedWithIv);
+ }
+
+ public String decrypt(String encryptedData) throws Exception {
+ byte[] encryptedWithIv = Base64.getDecoder().decode(encryptedData);
+ byte[] iv = new byte[IV_SIZE];
+ byte[] encrypted = new byte[encryptedWithIv.length - IV_SIZE];
+ System.arraycopy(encryptedWithIv, 0, iv, 0, IV_SIZE);
+ System.arraycopy(encryptedWithIv, IV_SIZE, encrypted, 0, encrypted.length);
+ Cipher cipher = Cipher.getInstance(ALGORITHM);
+ GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_SIZE, iv);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
+ byte[] original = cipher.doFinal(encrypted);
+ return new String(original);
+ }
+
+ public static String generateSecretKey() throws Exception {
+ KeyGenerator keyGen = KeyGenerator.getInstance("AES");
+ keyGen.init(256); // Use 256 bits for strong encryption
+ SecretKey secretKey = keyGen.generateKey();
+ return Base64.getEncoder().encodeToString(secretKey.getEncoded());
+ }
+}
diff --git a/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryptionProvider.java b/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryptionProvider.java
new file mode 100644
index 0000000..2ae3dff
--- /dev/null
+++ b/plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryptionProvider.java
@@ -0,0 +1,23 @@
+package com.coreoz.plume.admin.websession;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import com.coreoz.plume.admin.services.configuration.AdminSecurityConfigurationService;
+
+@Singleton
+public class MfaSecretKeyEncryptionProvider implements Provider {
+
+ private final MfaSecretKeyEncryption mfaSecretKeyEncryption;
+
+ @Inject
+ private MfaSecretKeyEncryptionProvider(AdminSecurityConfigurationService conf) {
+ this.mfaSecretKeyEncryption = new MfaSecretKeyEncryption(conf.mfaSecret());
+ }
+
+ @Override
+ public MfaSecretKeyEncryption get() {
+ return mfaSecretKeyEncryption;
+ }
+}
diff --git a/plume-admin-ws/pom.xml b/plume-admin-ws/pom.xml
index 6e879b5..f691933 100644
--- a/plume-admin-ws/pom.xml
+++ b/plume-admin-ws/pom.xml
@@ -1,7 +1,7 @@
4.0.0
-
+
com.coreoz
plume-admin-parent
@@ -29,7 +29,7 @@
com.coreoz
plume-admin-security
-
+
com.coreoz
plume-services
@@ -101,6 +101,24 @@
assertj-core
test
+
+
+
+ com.yubico
+ webauthn-server-core
+ 2.5.3
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+ 2.14.1
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-cbor
+ 2.14.1
+
@@ -115,4 +133,4 @@
-
\ No newline at end of file
+
diff --git a/plume-admin-ws/sql/setup-mssql.sql b/plume-admin-ws/sql/setup-mssql.sql
index 08561be..e021b95 100644
--- a/plume-admin-ws/sql/setup-mssql.sql
+++ b/plume-admin-ws/sql/setup-mssql.sql
@@ -14,6 +14,7 @@ CREATE TABLE PLM_USER (
EMAIL varchar(255) NOT NULL,
USER_NAME varchar(255) NOT NULL,
PASSWORD varchar(255) NOT NULL,
+ SECRET_KEY varchar(255) NOT NULL,
CONSTRAINT plm_user_pk PRIMARY KEY (ID),
CONSTRAINT uniq_plm_user_email UNIQUE (EMAIL),
CONSTRAINT uniq_plm_user_username UNIQUE (USER_NAME),
@@ -34,4 +35,4 @@ INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_USERS');
INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_ROLES');
INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_SYSTEM');
-GO
\ No newline at end of file
+GO
diff --git a/plume-admin-ws/sql/setup-mysql.sql b/plume-admin-ws/sql/setup-mysql.sql
index b587bdf..0b4d93c 100644
--- a/plume-admin-ws/sql/setup-mysql.sql
+++ b/plume-admin-ws/sql/setup-mysql.sql
@@ -1,3 +1,6 @@
+DROP TABLE IF EXISTS `PLM_USER_MFA`;
+DROP TABLE IF EXISTS `PLM_ROLE_PERMISSION`;
+DROP TABLE IF EXISTS `PLM_USER`;
DROP TABLE IF EXISTS `PLM_ROLE`;
CREATE TABLE `PLM_ROLE` (
`id` bigint(20) NOT NULL,
@@ -6,7 +9,7 @@ CREATE TABLE `PLM_ROLE` (
UNIQUE KEY `uniq_plm_role_label` (`label`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-DROP TABLE IF EXISTS `PLM_USER`;
+
CREATE TABLE `PLM_USER` (
`id` bigint(20) NOT NULL,
`id_role` bigint(20) NOT NULL,
@@ -16,13 +19,14 @@ CREATE TABLE `PLM_USER` (
`email` varchar(255) NOT NULL,
`user_name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
+ `mfa_user_handle` BLOB DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_plm_user_email` (`email`),
UNIQUE KEY `uniq_plm_user_username` (`user_name`),
CONSTRAINT `plm_user_role` FOREIGN KEY (`id_role`) REFERENCES `PLM_ROLE` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-DROP TABLE IF EXISTS `PLM_ROLE_PERMISSION`;
+
CREATE TABLE `PLM_ROLE_PERMISSION` (
`id_role` bigint(20) NOT NULL,
`permission` varchar(255) NOT NULL,
@@ -31,8 +35,41 @@ CREATE TABLE `PLM_ROLE_PERMISSION` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+DROP TABLE IF EXISTS `PLM_MFA_AUTHENTICATOR`;
+CREATE TABLE `PLM_MFA_AUTHENTICATOR` (
+ `id` bigint(20) NOT NULL,
+ `secret_key` varchar(255) DEFAULT NULL,
+ `credential_id` BLOB DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `PLM_MFA_BROWSER`;
+CREATE TABLE `PLM_MFA_BROWSER` (
+ `id` bigint(20) NOT NULL,
+ `key_id` BLOB NOT NULL,
+ `public_key_cose` BLOB NOT NULL,
+ `attestation` BLOB NOT NULL,
+ `client_data_json` BLOB NOT NULL,
+ `is_discoverable` tinyint(1) DEFAULT NULL,
+ `signature_count` int(11) NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+CREATE TABLE `PLM_USER_MFA` (
+ `id` bigint(20) NOT NULL,
+ `type` ENUM('authenticator', 'browser') NOT NULL,
+ `id_user` bigint(20) NOT NULL,
+ `id_mfa_authenticator` bigint(20) DEFAULT NULL,
+ `id_mfa_browser` bigint(20) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ CONSTRAINT `plm_user_mfa_user` FOREIGN KEY (`id_user`) REFERENCES `PLM_USER` (`id`),
+ CONSTRAINT `plm_user_mfa_mfa_authenticator` FOREIGN KEY (`id_mfa_authenticator`) REFERENCES `PLM_MFA_AUTHENTICATOR` (`id`),
+ CONSTRAINT `plm_user_mfa_mfa_browser` FOREIGN KEY (`id_mfa_browser`) REFERENCES `PLM_MFA_BROWSER` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
INSERT INTO PLM_ROLE VALUES(1, 'Administrator');
-INSERT INTO PLM_USER VALUES(1, 1, NOW(), 'Admin', 'Admin', 'admin@admin', 'admin', '$2a$11$FfgtfoHeNo/m9jGj9D5rTO0zDDI4LkMXnXHai744Ee32P3CHoBVqm');
+INSERT INTO PLM_USER VALUES(1, 1, NOW(), 'Admin', 'Admin', 'admin@admin', 'admin', '$2a$11$FfgtfoHeNo/m9jGj9D5rTO0zDDI4LkMXnXHai744Ee32P3CHoBVqm', NULL);
INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_USERS');
INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_ROLES');
INSERT INTO PLM_ROLE_PERMISSION VALUES(1, 'MANAGE_SYSTEM');
diff --git a/plume-admin-ws/sql/setup-oracle.sql b/plume-admin-ws/sql/setup-oracle.sql
index 1a85bd4..1ec88d9 100644
--- a/plume-admin-ws/sql/setup-oracle.sql
+++ b/plume-admin-ws/sql/setup-oracle.sql
@@ -14,6 +14,7 @@ CREATE TABLE PLM_USER (
EMAIL varchar(255) NOT NULL,
USER_NAME varchar(255) NOT NULL,
PASSWORD varchar(255) NOT NULL,
+ SECRET_KEY varchar(255) NOT NULL,
CONSTRAINT plm_user_pk PRIMARY KEY (ID),
CONSTRAINT uniq_plm_user_email UNIQUE (EMAIL),
CONSTRAINT uniq_plm_user_username UNIQUE (USER_NAME),
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaAuthenticatorDao.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaAuthenticatorDao.java
new file mode 100644
index 0000000..bc825e2
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaAuthenticatorDao.java
@@ -0,0 +1,18 @@
+package com.coreoz.plume.admin.db.daos;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import com.coreoz.plume.admin.db.generated.AdminMfaAuthenticator;
+import com.coreoz.plume.admin.db.generated.QAdminMfaAuthenticator;
+import com.coreoz.plume.db.querydsl.crud.CrudDaoQuerydsl;
+import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl;
+
+@Singleton
+public class AdminMfaAuthenticatorDao extends CrudDaoQuerydsl {
+
+ @Inject
+ private AdminMfaAuthenticatorDao(TransactionManagerQuerydsl transactionManager) {
+ super(transactionManager, QAdminMfaAuthenticator.adminMfaAuthenticator);
+ }
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaBrowserCredentialDao.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaBrowserCredentialDao.java
new file mode 100644
index 0000000..1855582
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaBrowserCredentialDao.java
@@ -0,0 +1,168 @@
+package com.coreoz.plume.admin.db.daos;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import com.coreoz.plume.admin.db.generated.AdminMfaBrowser;
+import com.coreoz.plume.admin.db.generated.AdminUser;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
+import com.coreoz.plume.admin.db.generated.QAdminMfaBrowser;
+import com.coreoz.plume.admin.db.generated.QAdminUser;
+import com.coreoz.plume.admin.db.generated.QAdminUserMfa;
+import com.coreoz.plume.db.querydsl.crud.CrudDaoQuerydsl;
+import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl;
+import com.google.inject.Singleton;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.PublicKeyCredentialType;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor.PublicKeyCredentialDescriptorBuilder;
+
+@Singleton
+public class AdminMfaBrowserCredentialDao implements CredentialRepository {
+
+ private final TransactionManagerQuerydsl transactionManager;
+ private final CrudDaoQuerydsl adminMfaBrowserDao;
+ private final CrudDaoQuerydsl adminUserMfaDao;
+
+ @Inject
+ private AdminMfaBrowserCredentialDao(TransactionManagerQuerydsl transactionManager) {
+ this.transactionManager = transactionManager;
+ this.adminMfaBrowserDao = new CrudDaoQuerydsl<>(transactionManager, QAdminMfaBrowser.adminMfaBrowser);
+ this.adminUserMfaDao = new CrudDaoQuerydsl<>(transactionManager, QAdminUserMfa.adminUserMfa);
+ }
+
+ public void registerCredential(
+ AdminUser user,
+ RegistrationResult result,
+ PublicKeyCredential pkc
+ ) {
+ AdminMfaBrowser mfa = new AdminMfaBrowser();
+ mfa.setKeyId(result.getKeyId().getId().getBytes());
+ mfa.setPublicKeyCose(result.getPublicKeyCose().getBytes());
+ mfa.setSignatureCount((int)result.getSignatureCount());
+ mfa.setIsDiscoverable(result.isDiscoverable().orElse(null));
+ mfa.setAttestation(pkc.getResponse().getAttestationObject().getBytes());
+ mfa.setClientDataJson(pkc.getResponse().getClientDataJSON().getBytes());
+ adminMfaBrowserDao.save(mfa);
+
+ AdminUserMfa userMfa = new AdminUserMfa();
+ userMfa.setIdUser(user.getId());
+ userMfa.setIdMfaBrowser(mfa.getId());
+ userMfa.setType("Browser");
+ adminUserMfaDao.save(userMfa);
+ }
+
+ public void updateCredential(
+ AdminUser user,
+ AssertionResult result
+ ) {
+ AdminMfaBrowser mfa = transactionManager.selectQuery()
+ .select(QAdminMfaBrowser.adminMfaBrowser)
+ .from(QAdminMfaBrowser.adminMfaBrowser)
+ .join(QAdminUserMfa.adminUserMfa)
+ .on(QAdminUserMfa.adminUserMfa.idMfaBrowser.eq(QAdminMfaBrowser.adminMfaBrowser.id))
+ .join(QAdminUser.adminUser)
+ .on(QAdminUser.adminUser.id.eq(QAdminUserMfa.adminUserMfa.idUser))
+ .where(QAdminUser.adminUser.id.eq(user.getId())
+ .and(QAdminMfaBrowser.adminMfaBrowser.keyId.eq(result.getCredentialId().getBytes())))
+ .fetchOne();
+ mfa.setSignatureCount((int)result.getSignatureCount());
+ adminMfaBrowserDao.save(mfa);
+ }
+
+ @Override
+ public Set getCredentialIdsForUsername(String username) {
+ List results = transactionManager.selectQuery()
+ .select(QAdminMfaBrowser.adminMfaBrowser.publicKeyCose)
+ .from(QAdminMfaBrowser.adminMfaBrowser)
+ .join(QAdminUserMfa.adminUserMfa)
+ .on(QAdminUserMfa.adminUserMfa.idMfaBrowser.eq(QAdminMfaBrowser.adminMfaBrowser.id))
+ .join(QAdminUser.adminUser)
+ .on(QAdminUser.adminUser.id.eq(QAdminUserMfa.adminUserMfa.idUser))
+ .where(QAdminUser.adminUser.userName.eq(username))
+ .fetch();
+ // Transform the list of byte arrays into a set of PublicKeyCredentialDescriptors
+ return results.stream()
+ .map(bytes -> {
+ PublicKeyCredentialDescriptorBuilder builder = PublicKeyCredentialDescriptor.builder()
+ .id(new ByteArray(bytes))
+ // Todo: everything should come from the database
+ .type(PublicKeyCredentialType.PUBLIC_KEY);
+ return builder.build();
+ })
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Optional getUserHandleForUsername(String username) {
+ byte[] bytes = transactionManager.selectQuery()
+ .select(QAdminUser.adminUser.mfaUserHandle)
+ .from(QAdminUser.adminUser)
+ .where(QAdminUser.adminUser.userName.eq(username))
+ .fetchOne();
+ return Optional.ofNullable(bytes == null ? null : new ByteArray(bytes));
+ }
+
+ @Override
+ public Optional getUsernameForUserHandle(ByteArray userHandle) {
+ return Optional.ofNullable(transactionManager.selectQuery()
+ .select(QAdminUser.adminUser.userName)
+ .from(QAdminUser.adminUser)
+ .where(QAdminUser.adminUser.mfaUserHandle.eq(userHandle.getBytes()))
+ .fetchOne());
+ }
+
+ @Override
+ public Optional lookup(ByteArray credentialId, ByteArray userHandle) {
+ AdminMfaBrowser mfa = transactionManager.selectQuery()
+ .select(QAdminMfaBrowser.adminMfaBrowser)
+ .from(QAdminMfaBrowser.adminMfaBrowser)
+ .join(QAdminUserMfa.adminUserMfa)
+ .on(QAdminUserMfa.adminUserMfa.idMfaBrowser.eq(QAdminMfaBrowser.adminMfaBrowser.id))
+ .join(QAdminUser.adminUser)
+ .on(QAdminUser.adminUser.id.eq(QAdminUserMfa.adminUserMfa.idUser))
+ .where(QAdminUser.adminUser.mfaUserHandle.eq(userHandle.getBytes())
+ .and(QAdminMfaBrowser.adminMfaBrowser.keyId.eq(credentialId.getBytes())))
+ .fetchOne();
+ if (mfa == null) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ RegisteredCredential.builder()
+ .credentialId(new ByteArray(mfa.getKeyId()))
+ .userHandle(new ByteArray(userHandle.getBytes()))
+ .publicKeyCose(new ByteArray(mfa.getPublicKeyCose()))
+ .signatureCount(mfa.getSignatureCount())
+ .build());
+ }
+
+ @Override
+ public Set lookupAll(ByteArray credentialId) {
+ List enrollements = transactionManager.selectQuery()
+ .select(QAdminMfaBrowser.adminMfaBrowser)
+ .from(QAdminMfaBrowser.adminMfaBrowser)
+ .where(QAdminMfaBrowser.adminMfaBrowser.keyId.eq(credentialId.getBytes()))
+ .fetch();
+ // Convert to set
+ return enrollements.stream()
+ .map(mfa -> RegisteredCredential.builder()
+ .credentialId(new ByteArray(mfa.getKeyId()))
+ .userHandle(new ByteArray(mfa.getKeyId()))
+ .publicKeyCose(new ByteArray(mfa.getPublicKeyCose()))
+ .signatureCount(mfa.getSignatureCount())
+ .build())
+ .collect(Collectors.toSet());
+ }
+
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaDao.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaDao.java
new file mode 100644
index 0000000..f45de72
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaDao.java
@@ -0,0 +1,78 @@
+package com.coreoz.plume.admin.db.daos;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import com.coreoz.plume.admin.db.generated.AdminMfaAuthenticator;
+import com.coreoz.plume.admin.db.generated.AdminMfaBrowser;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
+import com.coreoz.plume.admin.db.generated.QAdminMfaAuthenticator;
+import com.coreoz.plume.admin.db.generated.QAdminMfaBrowser;
+import com.coreoz.plume.admin.db.generated.QAdminUserMfa;
+import com.coreoz.plume.admin.services.mfa.MfaTypeEnum;
+import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl;
+
+@Singleton
+public class AdminMfaDao {
+
+ private final TransactionManagerQuerydsl transactionManager;
+ private final AdminMfaAuthenticatorDao adminMfaAuthenticatorDao;
+ private final AdminUserMfaDao adminUserMfaDao;
+ private final AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao;
+
+ @Inject
+ private AdminMfaDao(
+ TransactionManagerQuerydsl transactionManager,
+ AdminMfaAuthenticatorDao adminMfaAuthenticatorDao,
+ AdminUserMfaDao adminUserMfaDao,
+ AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao
+ ) {
+ this.transactionManager = transactionManager;
+ this.adminMfaAuthenticatorDao = adminMfaAuthenticatorDao;
+ this.adminUserMfaDao = adminUserMfaDao;
+ this.adminMfaBrowserCredentialDao = adminMfaBrowserCredentialDao;
+ }
+
+ public List findAuthenticatorByUserId(long userId) {
+ return transactionManager.selectQuery()
+ .select(QAdminMfaAuthenticator.adminMfaAuthenticator)
+ .from(QAdminMfaAuthenticator.adminMfaAuthenticator)
+ .join(QAdminUserMfa.adminUserMfa)
+ .on(QAdminUserMfa.adminUserMfa.idMfaAuthenticator.eq(QAdminMfaAuthenticator.adminMfaAuthenticator.id))
+ .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId)
+ .and(QAdminUserMfa.adminUserMfa.type.eq(MfaTypeEnum.AUTHENTICATOR.getType())))
+ .fetch();
+ }
+
+ public List findMfaBrowserByUserId(long userId) {
+ return transactionManager.selectQuery()
+ .select(QAdminMfaBrowser.adminMfaBrowser)
+ .from(QAdminMfaBrowser.adminMfaBrowser)
+ .join(QAdminUserMfa.adminUserMfa)
+ .on(QAdminUserMfa.adminUserMfa.idMfaBrowser.eq(QAdminMfaBrowser.adminMfaBrowser.id))
+ .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId)
+ .and(QAdminUserMfa.adminUserMfa.type.eq(MfaTypeEnum.BROWSER.getType())))
+ .fetch();
+ }
+
+ public void addMfaAuthenticatorToUser(long userId, AdminMfaAuthenticator mfa) {
+ long mfaId = adminMfaAuthenticatorDao.save(mfa).getId();
+ AdminUserMfa userMfa = new AdminUserMfa();
+ userMfa.setIdUser(userId);
+ userMfa.setIdMfaAuthenticator(mfaId);
+ userMfa.setType(MfaTypeEnum.AUTHENTICATOR.getType());
+ adminUserMfaDao.save(userMfa);
+ }
+
+ public void removeMfaAuthenticatorFromUser(long userId, long mfaId) {
+ AdminMfaAuthenticator mfa = adminMfaAuthenticatorDao.findById(mfaId);
+ if (mfa == null) {
+ return;
+ }
+ AdminUserMfa userMfa = adminUserMfaDao.findByUserIdAndMfaId(userId, mfaId);
+ adminUserMfaDao.delete(userMfa.getId());
+ adminMfaAuthenticatorDao.delete(mfa.getId());
+ }
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminUserMfaDao.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminUserMfaDao.java
new file mode 100644
index 0000000..dde5e47
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminUserMfaDao.java
@@ -0,0 +1,28 @@
+package com.coreoz.plume.admin.db.daos;
+
+import javax.inject.Singleton;
+
+import com.coreoz.plume.admin.db.generated.QAdminUserMfa;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
+import com.coreoz.plume.db.querydsl.crud.CrudDaoQuerydsl;
+import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl;
+import com.google.inject.Inject;
+
+@Singleton
+public class AdminUserMfaDao extends CrudDaoQuerydsl {
+
+ @Inject
+ private AdminUserMfaDao(TransactionManagerQuerydsl transactionManager) {
+ super(transactionManager, QAdminUserMfa.adminUserMfa);
+ }
+
+ public AdminUserMfa findByUserIdAndMfaId(Long userId, Long mfaId) {
+ return transactionManager
+ .selectQuery()
+ .select(QAdminUserMfa.adminUserMfa)
+ .from(QAdminUserMfa.adminUserMfa)
+ .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId)
+ .and(QAdminUserMfa.adminUserMfa.id.eq(mfaId)))
+ .fetchOne();
+ }
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java
new file mode 100644
index 0000000..665650e
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java
@@ -0,0 +1,71 @@
+package com.coreoz.plume.admin.db.generated;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import javax.annotation.processing.Generated;
+import com.querydsl.sql.Column;
+
+/**
+ * AdminMfaAuthenticator is a Querydsl bean type
+ */
+@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer")
+public class AdminMfaAuthenticator extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl {
+
+ @Column("credential_id")
+ private byte[] credentialId;
+
+ @Column("id")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long id;
+
+ @Column("secret_key")
+ private String secretKey;
+
+ public byte[] getCredentialId() {
+ return credentialId;
+ }
+
+ public void setCredentialId(byte[] credentialId) {
+ this.credentialId = credentialId;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (id == null) {
+ return super.equals(o);
+ }
+ if (!(o instanceof AdminMfaAuthenticator)) {
+ return false;
+ }
+ AdminMfaAuthenticator obj = (AdminMfaAuthenticator) o;
+ return id.equals(obj.id);
+ }
+
+ @Override
+ public int hashCode() {
+ if (id == null) {
+ return super.hashCode();
+ }
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + id.hashCode();
+ return result;
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaBrowser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaBrowser.java
new file mode 100644
index 0000000..68148e1
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaBrowser.java
@@ -0,0 +1,115 @@
+package com.coreoz.plume.admin.db.generated;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import javax.annotation.processing.Generated;
+import com.querydsl.sql.Column;
+
+/**
+ * AdminMfaBrowser is a Querydsl bean type
+ */
+@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer")
+public class AdminMfaBrowser extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl {
+
+ @Column("attestation")
+ private byte[] attestation;
+
+ @Column("client_data_json")
+ private byte[] clientDataJson;
+
+ @Column("id")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long id;
+
+ @Column("is_discoverable")
+ private Boolean isDiscoverable;
+
+ @Column("key_id")
+ private byte[] keyId;
+
+ @Column("public_key_cose")
+ private byte[] publicKeyCose;
+
+ @Column("signature_count")
+ private Integer signatureCount;
+
+ public byte[] getAttestation() {
+ return attestation;
+ }
+
+ public void setAttestation(byte[] attestation) {
+ this.attestation = attestation;
+ }
+
+ public byte[] getClientDataJson() {
+ return clientDataJson;
+ }
+
+ public void setClientDataJson(byte[] clientDataJson) {
+ this.clientDataJson = clientDataJson;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Boolean getIsDiscoverable() {
+ return isDiscoverable;
+ }
+
+ public void setIsDiscoverable(Boolean isDiscoverable) {
+ this.isDiscoverable = isDiscoverable;
+ }
+
+ public byte[] getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(byte[] keyId) {
+ this.keyId = keyId;
+ }
+
+ public byte[] getPublicKeyCose() {
+ return publicKeyCose;
+ }
+
+ public void setPublicKeyCose(byte[] publicKeyCose) {
+ this.publicKeyCose = publicKeyCose;
+ }
+
+ public Integer getSignatureCount() {
+ return signatureCount;
+ }
+
+ public void setSignatureCount(Integer signatureCount) {
+ this.signatureCount = signatureCount;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (id == null) {
+ return super.equals(o);
+ }
+ if (!(o instanceof AdminMfaBrowser)) {
+ return false;
+ }
+ AdminMfaBrowser obj = (AdminMfaBrowser) o;
+ return id.equals(obj.id);
+ }
+
+ @Override
+ public int hashCode() {
+ if (id == null) {
+ return super.hashCode();
+ }
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + id.hashCode();
+ return result;
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUser.java
index 66b9b61..95f5892 100644
--- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUser.java
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUser.java
@@ -1,7 +1,7 @@
package com.coreoz.plume.admin.db.generated;
-import javax.annotation.Generated;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import javax.annotation.processing.Generated;
import com.querydsl.sql.Column;
/**
@@ -10,30 +10,33 @@
@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer")
public class AdminUser extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl {
- @Column("CREATION_DATE")
+ @Column("creation_date")
private java.time.LocalDateTime creationDate;
- @Column("EMAIL")
+ @Column("email")
private String email;
- @Column("FIRST_NAME")
+ @Column("first_name")
private String firstName;
- @Column("ID")
+ @Column("id")
@JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
private Long id;
- @Column("ID_ROLE")
+ @Column("id_role")
@JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
private Long idRole;
- @Column("LAST_NAME")
+ @Column("last_name")
private String lastName;
- @Column("PASSWORD")
+ @Column("mfa_user_handle")
+ private byte[] mfaUserHandle;
+
+ @Column("password")
private String password;
- @Column("USER_NAME")
+ @Column("user_name")
private String userName;
public java.time.LocalDateTime getCreationDate() {
@@ -84,6 +87,14 @@ public void setLastName(String lastName) {
this.lastName = lastName;
}
+ public byte[] getMfaUserHandle() {
+ return mfaUserHandle;
+ }
+
+ public void setMfaUserHandle(byte[] mfaUserHandle) {
+ this.mfaUserHandle = mfaUserHandle;
+ }
+
public String getPassword() {
return password;
}
@@ -123,10 +134,5 @@ public int hashCode() {
return result;
}
- @Override
- public String toString() {
- return "AdminUser#" + id;
- }
-
}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java
new file mode 100644
index 0000000..249fd24
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java
@@ -0,0 +1,96 @@
+package com.coreoz.plume.admin.db.generated;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import javax.annotation.processing.Generated;
+import com.querydsl.sql.Column;
+
+/**
+ * AdminUserMfa is a Querydsl bean type
+ */
+@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer")
+public class AdminUserMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl {
+
+ @Column("id")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long id;
+
+ @Column("id_mfa_authenticator")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long idMfaAuthenticator;
+
+ @Column("id_mfa_browser")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long idMfaBrowser;
+
+ @Column("id_user")
+ @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class)
+ private Long idUser;
+
+ @Column("type")
+ private String type;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getIdMfaAuthenticator() {
+ return idMfaAuthenticator;
+ }
+
+ public void setIdMfaAuthenticator(Long idMfaAuthenticator) {
+ this.idMfaAuthenticator = idMfaAuthenticator;
+ }
+
+ public Long getIdMfaBrowser() {
+ return idMfaBrowser;
+ }
+
+ public void setIdMfaBrowser(Long idMfaBrowser) {
+ this.idMfaBrowser = idMfaBrowser;
+ }
+
+ public Long getIdUser() {
+ return idUser;
+ }
+
+ public void setIdUser(Long idUser) {
+ this.idUser = idUser;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (id == null) {
+ return super.equals(o);
+ }
+ if (!(o instanceof AdminUserMfa)) {
+ return false;
+ }
+ AdminUserMfa obj = (AdminUserMfa) o;
+ return id.equals(obj.id);
+ }
+
+ @Override
+ public int hashCode() {
+ if (id == null) {
+ return super.hashCode();
+ }
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + id.hashCode();
+ return result;
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaAuthenticator.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaAuthenticator.java
new file mode 100644
index 0000000..b5ea09a
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaAuthenticator.java
@@ -0,0 +1,69 @@
+package com.coreoz.plume.admin.db.generated;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+import com.querydsl.sql.ColumnMetadata;
+import java.sql.Types;
+
+
+
+
+/**
+ * QAdminMfaAuthenticator is a Querydsl query type for AdminMfaAuthenticator
+ */
+@Generated("com.querydsl.sql.codegen.MetaDataSerializer")
+public class QAdminMfaAuthenticator extends com.querydsl.sql.RelationalPathBase {
+
+ private static final long serialVersionUID = 1997658142;
+
+ public static final QAdminMfaAuthenticator adminMfaAuthenticator = new QAdminMfaAuthenticator("PLM_MFA_AUTHENTICATOR");
+
+ public final SimplePath credentialId = createSimple("credentialId", byte[].class);
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final StringPath secretKey = createString("secretKey");
+
+ public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id);
+
+ public final com.querydsl.sql.ForeignKey _plmUserMfaMfaAuthenticator = createInvForeignKey(id, "id_mfa_authenticator");
+
+ public QAdminMfaAuthenticator(String variable) {
+ super(AdminMfaAuthenticator.class, forVariable(variable), "null", "PLM_MFA_AUTHENTICATOR");
+ addMetadata();
+ }
+
+ public QAdminMfaAuthenticator(String variable, String schema, String table) {
+ super(AdminMfaAuthenticator.class, forVariable(variable), schema, table);
+ addMetadata();
+ }
+
+ public QAdminMfaAuthenticator(String variable, String schema) {
+ super(AdminMfaAuthenticator.class, forVariable(variable), schema, "PLM_MFA_AUTHENTICATOR");
+ addMetadata();
+ }
+
+ public QAdminMfaAuthenticator(Path extends AdminMfaAuthenticator> path) {
+ super(path.getType(), path.getMetadata(), "null", "PLM_MFA_AUTHENTICATOR");
+ addMetadata();
+ }
+
+ public QAdminMfaAuthenticator(PathMetadata metadata) {
+ super(AdminMfaAuthenticator.class, metadata, "null", "PLM_MFA_AUTHENTICATOR");
+ addMetadata();
+ }
+
+ public void addMetadata() {
+ addMetadata(credentialId, ColumnMetadata.named("credential_id").withIndex(3).ofType(Types.LONGVARBINARY).withSize(65535));
+ addMetadata(id, ColumnMetadata.named("id").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(secretKey, ColumnMetadata.named("secret_key").withIndex(2).ofType(Types.VARCHAR).withSize(255));
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaBrowser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaBrowser.java
new file mode 100644
index 0000000..0b24ef8
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaBrowser.java
@@ -0,0 +1,81 @@
+package com.coreoz.plume.admin.db.generated;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+import com.querydsl.sql.ColumnMetadata;
+import java.sql.Types;
+
+
+
+
+/**
+ * QAdminMfaBrowser is a Querydsl query type for AdminMfaBrowser
+ */
+@Generated("com.querydsl.sql.codegen.MetaDataSerializer")
+public class QAdminMfaBrowser extends com.querydsl.sql.RelationalPathBase {
+
+ private static final long serialVersionUID = -1158649325;
+
+ public static final QAdminMfaBrowser adminMfaBrowser = new QAdminMfaBrowser("PLM_MFA_BROWSER");
+
+ public final SimplePath attestation = createSimple("attestation", byte[].class);
+
+ public final SimplePath clientDataJson = createSimple("clientDataJson", byte[].class);
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final BooleanPath isDiscoverable = createBoolean("isDiscoverable");
+
+ public final SimplePath keyId = createSimple("keyId", byte[].class);
+
+ public final SimplePath publicKeyCose = createSimple("publicKeyCose", byte[].class);
+
+ public final NumberPath signatureCount = createNumber("signatureCount", Integer.class);
+
+ public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id);
+
+ public final com.querydsl.sql.ForeignKey _plmUserMfaMfaBrowser = createInvForeignKey(id, "id_mfa_browser");
+
+ public QAdminMfaBrowser(String variable) {
+ super(AdminMfaBrowser.class, forVariable(variable), "null", "PLM_MFA_BROWSER");
+ addMetadata();
+ }
+
+ public QAdminMfaBrowser(String variable, String schema, String table) {
+ super(AdminMfaBrowser.class, forVariable(variable), schema, table);
+ addMetadata();
+ }
+
+ public QAdminMfaBrowser(String variable, String schema) {
+ super(AdminMfaBrowser.class, forVariable(variable), schema, "PLM_MFA_BROWSER");
+ addMetadata();
+ }
+
+ public QAdminMfaBrowser(Path extends AdminMfaBrowser> path) {
+ super(path.getType(), path.getMetadata(), "null", "PLM_MFA_BROWSER");
+ addMetadata();
+ }
+
+ public QAdminMfaBrowser(PathMetadata metadata) {
+ super(AdminMfaBrowser.class, metadata, "null", "PLM_MFA_BROWSER");
+ addMetadata();
+ }
+
+ public void addMetadata() {
+ addMetadata(attestation, ColumnMetadata.named("attestation").withIndex(4).ofType(Types.LONGVARBINARY).withSize(65535).notNull());
+ addMetadata(clientDataJson, ColumnMetadata.named("client_data_json").withIndex(5).ofType(Types.LONGVARBINARY).withSize(65535).notNull());
+ addMetadata(id, ColumnMetadata.named("id").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(isDiscoverable, ColumnMetadata.named("is_discoverable").withIndex(6).ofType(Types.BOOLEAN).withSize(3));
+ addMetadata(keyId, ColumnMetadata.named("key_id").withIndex(2).ofType(Types.LONGVARBINARY).withSize(65535).notNull());
+ addMetadata(publicKeyCose, ColumnMetadata.named("public_key_cose").withIndex(3).ofType(Types.LONGVARBINARY).withSize(65535).notNull());
+ addMetadata(signatureCount, ColumnMetadata.named("signature_count").withIndex(7).ofType(Types.INTEGER).withSize(10).notNull());
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUser.java
index a1a8f8f..eb44671 100644
--- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUser.java
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUser.java
@@ -5,7 +5,7 @@
import com.querydsl.core.types.dsl.*;
import com.querydsl.core.types.PathMetadata;
-import javax.annotation.Generated;
+import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
import com.querydsl.sql.ColumnMetadata;
@@ -36,16 +36,20 @@ public class QAdminUser extends com.querydsl.sql.RelationalPathBase {
public final StringPath lastName = createString("lastName");
+ public final SimplePath mfaUserHandle = createSimple("mfaUserHandle", byte[].class);
+
public final StringPath password = createString("password");
public final StringPath userName = createString("userName");
- public final com.querydsl.sql.PrimaryKey constraintB3 = createPrimaryKey(id);
+ public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id);
+
+ public final com.querydsl.sql.ForeignKey plmUserRole = createForeignKey(idRole, "id");
- public final com.querydsl.sql.ForeignKey plmUserRole = createForeignKey(idRole, "ID");
+ public final com.querydsl.sql.ForeignKey _plmUserMfaUser = createInvForeignKey(id, "id_user");
public QAdminUser(String variable) {
- super(AdminUser.class, forVariable(variable), "PUBLIC", "PLM_USER");
+ super(AdminUser.class, forVariable(variable), "null", "PLM_USER");
addMetadata();
}
@@ -60,24 +64,25 @@ public QAdminUser(String variable, String schema) {
}
public QAdminUser(Path extends AdminUser> path) {
- super(path.getType(), path.getMetadata(), "PUBLIC", "PLM_USER");
+ super(path.getType(), path.getMetadata(), "null", "PLM_USER");
addMetadata();
}
public QAdminUser(PathMetadata metadata) {
- super(AdminUser.class, metadata, "PUBLIC", "PLM_USER");
+ super(AdminUser.class, metadata, "null", "PLM_USER");
addMetadata();
}
public void addMetadata() {
- addMetadata(creationDate, ColumnMetadata.named("CREATION_DATE").withIndex(3).ofType(Types.TIMESTAMP).withSize(23).withDigits(10).notNull());
- addMetadata(email, ColumnMetadata.named("EMAIL").withIndex(6).ofType(Types.VARCHAR).withSize(255).notNull());
- addMetadata(firstName, ColumnMetadata.named("FIRST_NAME").withIndex(4).ofType(Types.VARCHAR).withSize(255).notNull());
- addMetadata(id, ColumnMetadata.named("ID").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull());
- addMetadata(idRole, ColumnMetadata.named("ID_ROLE").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull());
- addMetadata(lastName, ColumnMetadata.named("LAST_NAME").withIndex(5).ofType(Types.VARCHAR).withSize(255).notNull());
- addMetadata(password, ColumnMetadata.named("PASSWORD").withIndex(8).ofType(Types.VARCHAR).withSize(255).notNull());
- addMetadata(userName, ColumnMetadata.named("USER_NAME").withIndex(7).ofType(Types.VARCHAR).withSize(255).notNull());
+ addMetadata(creationDate, ColumnMetadata.named("creation_date").withIndex(3).ofType(Types.TIMESTAMP).withSize(19).notNull());
+ addMetadata(email, ColumnMetadata.named("email").withIndex(6).ofType(Types.VARCHAR).withSize(255).notNull());
+ addMetadata(firstName, ColumnMetadata.named("first_name").withIndex(4).ofType(Types.VARCHAR).withSize(255).notNull());
+ addMetadata(id, ColumnMetadata.named("id").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(idRole, ColumnMetadata.named("id_role").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(lastName, ColumnMetadata.named("last_name").withIndex(5).ofType(Types.VARCHAR).withSize(255).notNull());
+ addMetadata(mfaUserHandle, ColumnMetadata.named("mfa_user_handle").withIndex(9).ofType(Types.LONGVARBINARY).withSize(65535));
+ addMetadata(password, ColumnMetadata.named("password").withIndex(8).ofType(Types.VARCHAR).withSize(255).notNull());
+ addMetadata(userName, ColumnMetadata.named("user_name").withIndex(7).ofType(Types.VARCHAR).withSize(255).notNull());
}
}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java
new file mode 100644
index 0000000..cbc0fea
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java
@@ -0,0 +1,79 @@
+package com.coreoz.plume.admin.db.generated;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+import com.querydsl.sql.ColumnMetadata;
+import java.sql.Types;
+
+
+
+
+/**
+ * QAdminUserMfa is a Querydsl query type for AdminUserMfa
+ */
+@Generated("com.querydsl.sql.codegen.MetaDataSerializer")
+public class QAdminUserMfa extends com.querydsl.sql.RelationalPathBase {
+
+ private static final long serialVersionUID = -291052278;
+
+ public static final QAdminUserMfa adminUserMfa = new QAdminUserMfa("PLM_USER_MFA");
+
+ public final NumberPath id = createNumber("id", Long.class);
+
+ public final NumberPath idMfaAuthenticator = createNumber("idMfaAuthenticator", Long.class);
+
+ public final NumberPath idMfaBrowser = createNumber("idMfaBrowser", Long.class);
+
+ public final NumberPath idUser = createNumber("idUser", Long.class);
+
+ public final StringPath type = createString("type");
+
+ public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id);
+
+ public final com.querydsl.sql.ForeignKey plmUserMfaMfaAuthenticator = createForeignKey(idMfaAuthenticator, "id");
+
+ public final com.querydsl.sql.ForeignKey plmUserMfaMfaBrowser = createForeignKey(idMfaBrowser, "id");
+
+ public final com.querydsl.sql.ForeignKey plmUserMfaUser = createForeignKey(idUser, "id");
+
+ public QAdminUserMfa(String variable) {
+ super(AdminUserMfa.class, forVariable(variable), "null", "PLM_USER_MFA");
+ addMetadata();
+ }
+
+ public QAdminUserMfa(String variable, String schema, String table) {
+ super(AdminUserMfa.class, forVariable(variable), schema, table);
+ addMetadata();
+ }
+
+ public QAdminUserMfa(String variable, String schema) {
+ super(AdminUserMfa.class, forVariable(variable), schema, "PLM_USER_MFA");
+ addMetadata();
+ }
+
+ public QAdminUserMfa(Path extends AdminUserMfa> path) {
+ super(path.getType(), path.getMetadata(), "null", "PLM_USER_MFA");
+ addMetadata();
+ }
+
+ public QAdminUserMfa(PathMetadata metadata) {
+ super(AdminUserMfa.class, metadata, "null", "PLM_USER_MFA");
+ addMetadata();
+ }
+
+ public void addMetadata() {
+ addMetadata(id, ColumnMetadata.named("id").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(idMfaAuthenticator, ColumnMetadata.named("id_mfa_authenticator").withIndex(4).ofType(Types.BIGINT).withSize(19));
+ addMetadata(idMfaBrowser, ColumnMetadata.named("id_mfa_browser").withIndex(5).ofType(Types.BIGINT).withSize(19));
+ addMetadata(idUser, ColumnMetadata.named("id_user").withIndex(3).ofType(Types.BIGINT).withSize(19).notNull());
+ addMetadata(type, ColumnMetadata.named("type").withIndex(2).ofType(Types.VARCHAR).withSize(13).notNull());
+ }
+
+}
+
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/configuration/AdminConfigurationService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/configuration/AdminConfigurationService.java
index 9323cc4..78eb8ed 100644
--- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/configuration/AdminConfigurationService.java
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/configuration/AdminConfigurationService.java
@@ -27,6 +27,11 @@ public String jwtSecret() {
return config.getString("admin.jwt-secret");
}
+ // Can be used as an issue name to create QR Code for MFA
+ public String appName() {
+ return config.getString("admin.app-name");
+ }
+
public long sessionExpireDurationInMillis() {
return config.getDuration("admin.session.expire-duration", TimeUnit.MILLISECONDS);
}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java
new file mode 100644
index 0000000..df182bc
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java
@@ -0,0 +1,215 @@
+package com.coreoz.plume.admin.services.mfa;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.glassfish.jersey.internal.guava.Cache;
+import org.glassfish.jersey.internal.guava.CacheBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.coreoz.plume.admin.db.daos.AdminMfaBrowserCredentialDao;
+import com.coreoz.plume.admin.db.daos.AdminMfaDao;
+import com.coreoz.plume.admin.db.daos.AdminUserDao;
+import com.coreoz.plume.admin.db.generated.AdminMfaBrowser;
+import com.coreoz.plume.admin.db.generated.AdminUser;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
+import com.coreoz.plume.admin.services.configuration.AdminConfigurationService;
+import com.coreoz.plume.admin.webservices.validation.AdminWsError;
+import com.coreoz.plume.admin.websession.MfaSecretKeyEncryptionProvider;
+import com.coreoz.plume.jersey.errors.WsException;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.FinishAssertionOptions;
+import com.yubico.webauthn.FinishRegistrationOptions;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.StartAssertionOptions;
+import com.yubico.webauthn.StartRegistrationOptions;
+import com.yubico.webauthn.StartAssertionOptions.StartAssertionOptionsBuilder;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import com.yubico.webauthn.data.UserIdentity;
+import com.yubico.webauthn.exception.AssertionFailedException;
+import com.yubico.webauthn.exception.RegistrationFailedException;
+
+@Singleton
+public class MfaService {
+
+ private static final Logger logger = LoggerFactory.getLogger(MfaService.class);
+
+ private final GoogleAuthenticator authenticator = new GoogleAuthenticator();
+ private final AdminConfigurationService configurationService;
+ private final AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao;
+ private final AdminUserDao adminUserDao;
+ private final MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider;
+ private final RelyingParty relyingParty;
+ private final Random random = new Random();
+ private final Cache createOptionCache =
+ CacheBuilder.newBuilder().expireAfterAccess(2, TimeUnit.MINUTES).build();
+ private final Cache verifyOptionCache =
+ CacheBuilder.newBuilder().expireAfterAccess(2, TimeUnit.MINUTES).build();
+
+ @Inject
+ private MfaService(
+ AdminConfigurationService configurationService,
+ MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider,
+ AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao,
+ AdminUserDao adminUserDao
+ ) {
+ this.configurationService = configurationService;
+ // TODO: Avoir un conf une liste des mfa à activer
+ this.mfaSecretKeyEncryptionProvider = mfaSecretKeyEncryptionProvider;
+ this.adminMfaBrowserCredentialDao = adminMfaBrowserCredentialDao;
+ this.adminUserDao = adminUserDao;
+ RelyingPartyIdentity identity = RelyingPartyIdentity.builder()
+ .id("localhost") // TODO: Conf ?
+ .name(configurationService.appName())
+ .build();
+ this.relyingParty = RelyingParty.builder()
+ .identity(identity)
+ .credentialRepository(adminMfaBrowserCredentialDao)
+ .build();
+ }
+
+ // --------------------- Authenticator ---------------------
+
+ public String generateSecretKey() throws Exception {
+ GoogleAuthenticatorKey key = authenticator.createCredentials();
+ return key.getKey();
+ }
+
+ public String hashSecretKey(String secretKey) throws Exception {
+ return mfaSecretKeyEncryptionProvider.get().encrypt(secretKey);
+ }
+
+ public String getQRBarcodeURL(String user, String secret) {
+ final String issuer = configurationService.appName();
+ return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, user, secret, issuer);
+ }
+
+ public byte[] generateQRCode(String user, String secret) {
+ String qrBarcodeURL = getQRBarcodeURL(user, secret);
+ try {
+ return QRCodeGenerator.generateQRCodeImage(qrBarcodeURL, 200, 200);
+ } catch (WriterException | IOException e) {
+ logger.error("Error generating QR code", e);
+ return null;
+ }
+ }
+
+ public boolean verifyCode(String secret, int code) {
+ try {
+ return authenticator.authorize(mfaSecretKeyEncryptionProvider.get().decrypt(secret), code);
+ } catch (Exception e) {
+ logger.info("could not decrypt secret key", e);
+ return false;
+ }
+ }
+
+ private static class QRCodeGenerator {
+ public static byte[] generateQRCodeImage(String barcodeText, int width, int height) throws WriterException, IOException {
+ QRCodeWriter barcodeWriter = new QRCodeWriter();
+ BitMatrix bitMatrix = barcodeWriter.encode(barcodeText, BarcodeFormat.QR_CODE, width, height);
+
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
+ MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
+ return pngOutputStream.toByteArray();
+ }
+ }
+
+ // --------------------- Browser ---------------------
+
+ public PublicKeyCredentialCreationOptions startRegistration(AdminUser user) {
+ byte[] userHandle = new byte[64];
+ random.nextBytes(userHandle);
+ StartRegistrationOptions options = StartRegistrationOptions.builder()
+ .user(UserIdentity.builder()
+ .name(user.getUserName())
+ .displayName(user.getUserName())
+ .id(new ByteArray(userHandle))
+ .build())
+ .build();
+ PublicKeyCredentialCreationOptions createOptions = relyingParty.startRegistration(options);
+ createOptionCache.put(user.getId(), createOptions);
+ return createOptions;
+ }
+
+ public boolean finishRegistration(
+ AdminUser user,
+ PublicKeyCredential pkc
+ ) {
+ PublicKeyCredentialCreationOptions request = createOptionCache.getIfPresent(user.getId());
+ if (request == null) {
+ return false;
+ }
+ try {
+ RegistrationResult result = relyingParty.finishRegistration(
+ FinishRegistrationOptions.builder()
+ .request(request)
+ .response(pkc)
+ .build()
+ );
+ adminMfaBrowserCredentialDao.registerCredential(user, result, pkc);
+ return true;
+ } catch (RegistrationFailedException e) {
+ logger.error("Error finishing registration", e);
+ return false;
+ }
+ }
+
+ public AdminUser verifyWebauth(PublicKeyCredential pkc) {
+ if (pkc.getResponse().getUserHandle().isEmpty()) {
+ return null;
+ }
+ Optional username = adminMfaBrowserCredentialDao.getUsernameForUserHandle(pkc.getResponse().getUserHandle().get());
+ if (username.isEmpty()) {
+ return null;
+ }
+ AssertionRequest assertion = verifyOptionCache.getIfPresent(username.get());
+
+ try {
+ AssertionResult result = relyingParty.finishAssertion(FinishAssertionOptions.builder()
+ .request(assertion) // The PublicKeyCredentialRequestOptions from startAssertion above
+ .response(pkc)
+ .build());
+ if (result.isSuccess()) {
+ AdminUser user = adminUserDao.findByUserName(username.get()).get();
+ adminMfaBrowserCredentialDao.updateCredential(user, result);
+ return adminUserDao.findByUserName(result.getUsername()).get();
+ }
+ return null;
+ } catch (AssertionFailedException e) {
+ return null;
+ }
+ }
+
+ public AssertionRequest getAssertionRequest(String username) {
+ AssertionRequest request = relyingParty.startAssertion(StartAssertionOptions.builder()
+ .username(username)
+ .build());
+
+ verifyOptionCache.put(username, request);
+ return request;
+ }
+
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaTypeEnum.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaTypeEnum.java
new file mode 100644
index 0000000..df5663c
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaTypeEnum.java
@@ -0,0 +1,16 @@
+package com.coreoz.plume.admin.services.mfa;
+
+public enum MfaTypeEnum {
+ AUTHENTICATOR("authenticator"),
+ BROWSER("browser");
+
+ private final String type;
+
+ MfaTypeEnum(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/user/AdminUserService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/user/AdminUserService.java
index 7158bed..23cd9c5 100644
--- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/user/AdminUserService.java
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/user/AdminUserService.java
@@ -1,16 +1,23 @@
package com.coreoz.plume.admin.services.user;
+import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
+import com.coreoz.plume.admin.db.daos.AdminMfaDao;
import com.coreoz.plume.admin.db.daos.AdminUserDao;
+import com.coreoz.plume.admin.db.generated.AdminMfaAuthenticator;
import com.coreoz.plume.admin.db.generated.AdminUser;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
import com.coreoz.plume.admin.services.hash.HashService;
+import com.coreoz.plume.admin.services.mfa.MfaService;
import com.coreoz.plume.admin.services.role.AdminRoleService;
import com.coreoz.plume.admin.webservices.data.user.AdminUserParameters;
+import com.coreoz.plume.admin.webservices.validation.AdminWsError;
import com.coreoz.plume.db.crud.CrudService;
+import com.coreoz.plume.jersey.errors.WsException;
import com.coreoz.plume.services.time.TimeProvider;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
@@ -19,17 +26,22 @@
public class AdminUserService extends CrudService {
private final AdminUserDao adminUserDao;
+ private final AdminMfaDao adminMfaDao;
private final AdminRoleService adminRoleService;
private final HashService hashService;
+ private final MfaService mfaService;
private final TimeProvider timeProvider;
@Inject
public AdminUserService(AdminUserDao adminUserDao, AdminRoleService adminRoleService,
- HashService hashService, TimeProvider timeProvider) {
+ HashService hashService, TimeProvider timeProvider, MfaService mfaService,
+ AdminMfaDao adminMfaDao) {
super(adminUserDao);
this.adminUserDao = adminUserDao;
+ this.adminMfaDao = adminMfaDao;
this.adminRoleService = adminRoleService;
+ this.mfaService = mfaService;
this.hashService = hashService;
this.timeProvider = timeProvider;
}
@@ -44,6 +56,33 @@ public Optional authenticate(String userName, String password
));
}
+ public Optional authenticateWithAuthenticator(String userName, int code) {
+ return adminUserDao
+ .findByUserName(userName)
+ .filter(user -> {
+ List registeredAuthenticators = adminMfaDao.findAuthenticatorByUserId(user.getId());
+ // If any of the MFA is valid, then the user is valid
+ return registeredAuthenticators.stream().anyMatch(authenticator -> {
+ try {
+ return mfaService.verifyCode(authenticator.getSecretKey(), code);
+ } catch (Exception e) {
+ return false;
+ }
+ });
+ })
+ .map(user -> AuthenticatedUserAdmin.of(
+ user,
+ ImmutableSet.copyOf(adminRoleService.findRolePermissions(user.getIdRole()))
+ ));
+ }
+
+ public AuthenticatedUser authenticateWithMfa(AdminUser user) {
+ return AuthenticatedUserAdmin.of(
+ user,
+ ImmutableSet.copyOf(adminRoleService.findRolePermissions(user.getIdRole()))
+ );
+ }
+
public void update(AdminUserParameters parameters) {
String newPassword = Strings.emptyToNull(parameters.getPassword());
adminUserDao.update(
@@ -57,6 +96,17 @@ public void update(AdminUserParameters parameters) {
);
}
+ public String createMfaAuthenticatorSecretKey(Long idUser) throws Exception {
+ AdminUser user = adminUserDao.findById(idUser);
+ String secretKey = mfaService.generateSecretKey();
+ AdminMfaAuthenticator mfa = new AdminMfaAuthenticator();
+ mfa.setSecretKey(mfaService.hashSecretKey(secretKey));
+ adminMfaDao.addMfaAuthenticatorToUser(user.getId(), mfa);
+ adminUserDao.save(user);
+
+ return secretKey;
+ }
+
public AdminUser create(AdminUserParameters parameters) {
AdminUser adminUserToSave = new AdminUser();
adminUserToSave.setIdRole(parameters.getIdRole());
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/SessionWs.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/SessionWs.java
index ed8fbc5..99be742 100644
--- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/SessionWs.java
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/SessionWs.java
@@ -1,16 +1,25 @@
package com.coreoz.plume.admin.webservices;
+import java.io.IOException;
import java.security.SecureRandom;
import javax.inject.Inject;
import javax.inject.Singleton;
+import com.coreoz.plume.admin.db.generated.AdminUser;
+import com.coreoz.plume.admin.db.generated.AdminUserMfa;
import com.coreoz.plume.admin.security.login.LoginFailAttemptsManager;
import com.coreoz.plume.admin.services.configuration.AdminConfigurationService;
import com.coreoz.plume.admin.services.configuration.AdminSecurityConfigurationService;
+import com.coreoz.plume.admin.services.mfa.MfaService;
+import com.coreoz.plume.admin.services.mfa.MfaTypeEnum;
import com.coreoz.plume.admin.services.user.AdminUserService;
import com.coreoz.plume.admin.services.user.AuthenticatedUser;
+import com.coreoz.plume.admin.services.user.AuthenticatedUserAdmin;
+import com.coreoz.plume.admin.webservices.data.session.AdminAuthenticatorCredentials;
import com.coreoz.plume.admin.webservices.data.session.AdminCredentials;
+import com.coreoz.plume.admin.webservices.data.session.AdminMfaQrcode;
+import com.coreoz.plume.admin.webservices.data.session.AdminPublicKeyCredentials;
import com.coreoz.plume.admin.webservices.data.session.AdminSession;
import com.coreoz.plume.admin.webservices.validation.AdminWsError;
import com.coreoz.plume.admin.websession.JwtSessionSigner;
@@ -18,23 +27,43 @@
import com.coreoz.plume.admin.websession.WebSessionPermission;
import com.coreoz.plume.admin.websession.jersey.JerseySessionParser;
import com.coreoz.plume.jersey.errors.Validators;
+import com.coreoz.plume.jersey.errors.WsError;
import com.coreoz.plume.jersey.errors.WsException;
import com.coreoz.plume.jersey.security.permission.PublicApi;
import com.coreoz.plume.services.time.TimeProvider;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.common.net.HttpHeaders;
+import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.FinishAssertionOptions;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -47,10 +76,13 @@
@Singleton
public class SessionWs {
+ private static final Logger logger = LoggerFactory.getLogger(SessionWs.class);
+
public static final FingerprintWithHash NULL_FINGERPRINT = new FingerprintWithHash(null, null);
private final AdminUserService adminUserService;
private final JwtSessionSigner jwtSessionSigner;
+ private final MfaService mfaService;
private final TimeProvider timeProvider;
private final LoginFailAttemptsManager failAttemptsManager;
@@ -69,9 +101,11 @@ public SessionWs(AdminUserService adminUserService,
JwtSessionSigner jwtSessionSigner,
AdminConfigurationService configurationService,
AdminSecurityConfigurationService adminSecurityConfigurationService,
+ MfaService mfaService,
TimeProvider timeProvider) {
this.adminUserService = adminUserService;
this.jwtSessionSigner = jwtSessionSigner;
+ this.mfaService = mfaService;
this.timeProvider = timeProvider;
this.failAttemptsManager = new LoginFailAttemptsManager(
@@ -102,6 +136,145 @@ public Response authenticate(AdminCredentials credentials) {
.build();
}
+ // ------------------------ QR / Code Authenticator ------------------------
+
+ @POST
+ @Operation(description = "Generate a qrcode for MFA enrollment")
+ @Path("/auhenticator/qrcode-url")
+ public AdminMfaQrcode qrCodeUrl(AdminCredentials credentials) {
+ // First user needs to be authenticated (an exception will be raised otherwise)
+ AuthenticatedUser authenticatedUser = authenticateUser(credentials);
+
+ // Generate MFA secret key and QR code URL
+ try {
+ String secretKey = adminUserService.createMfaAuthenticatorSecretKey(authenticatedUser.getUser().getId());
+ String qrCodeUrl = mfaService.getQRBarcodeURL(authenticatedUser.getUser().getUserName(), secretKey);
+
+ // Return the QR code URL to the client
+ return new AdminMfaQrcode(qrCodeUrl);
+ } catch (Exception e) {
+ logger.debug("erreur lors de la génération du QR code", e);
+ throw new WsException(WsError.INTERNAL_ERROR);
+ }
+ }
+
+ @POST
+ @Operation(description = "Generate a qrcode for MFA enrollment")
+ @Path("/auhenticator/qrcode")
+ public Response qrCode(AdminCredentials credentials) {
+ // First user needs to be authenticated (an exception will be raised otherwise)
+ AuthenticatedUser authenticatedUser = authenticateUser(credentials);
+
+ // Generate MFA secret key and QR code URL
+ try {
+ String secretKey = adminUserService.createMfaAuthenticatorSecretKey(authenticatedUser.getUser().getId());
+ byte[] qrCode = mfaService.generateQRCode(secretKey, secretKey);
+
+ // Return the QR code image to the client
+ ResponseBuilder response = Response.ok(qrCode);
+ response.header("Content-Disposition", "attachment; filename=qrcode.png");
+ response.header("Content-Type", "image/png");
+ return response.build();
+ } catch (Exception e) {
+ logger.debug("erreur lors de la génération du QR code", e);
+ throw new WsException(WsError.INTERNAL_ERROR);
+ }
+ }
+
+ @POST
+ @Path("/auhenticator/verify")
+ @Operation(description = "Verify MFA code for authentication")
+ public Response verifyMfa(AdminAuthenticatorCredentials credentials) {
+ // first user needs to be authenticated (an exception will be raised otherwise)
+ AuthenticatedUser authenticatedUser = authenticateUserWithAuthenticator(credentials);
+ // if the client is authenticated, the fingerprint can be generated if needed
+ FingerprintWithHash fingerprintWithHash = sessionUseFingerprintCookie ? generateFingerprint() : NULL_FINGERPRINT;
+ return withFingerprintCookie(
+ Response.ok(toAdminSession(toWebSession(authenticatedUser, fingerprintWithHash.getHash()))),
+ fingerprintWithHash.getFingerprint()
+ )
+ .build();
+ }
+
+ // ------------------------ Browser Authenticator ------------------------
+
+ @POST
+ @Operation(description = "Start the registration of a new MFA credential with WebAuthn")
+ @Path("/webauth/start-registration")
+ public String getWebAuthentCreationOptions(AdminCredentials credentials) {
+ // First user needs to be authenticated (an exception will be raised otherwise)
+ AuthenticatedUser authenticatedUser = authenticateUser(credentials);
+
+ // Generate the PublicKeyCredentialCreationOptions
+ PublicKeyCredentialCreationOptions options = mfaService.startRegistration(authenticatedUser.getUser());
+ try {
+ return options.toCredentialsCreateJson();
+ } catch (JsonProcessingException e) {
+ logger.debug("erreur lors de la génération du PublicKeyCredentialCreationOptions", e);
+ throw new WsException(WsError.INTERNAL_ERROR);
+ }
+ }
+
+ @POST
+ @Operation(description = "Register public key of a new MFA credential")
+ @Path("/webauth/register-credential")
+ public Response registerCredential(AdminPublicKeyCredentials credentials) {
+ // First user needs to be authenticated (an exception will be raised otherwise)
+ AuthenticatedUser authenticatedUser = authenticateUser(credentials.getCredentials());
+
+ // Finish the registration of the new MFA credential
+ String publicKeyCredentialJson = credentials.getPublicKeyCredentialJson();
+ try {
+ PublicKeyCredential pkc =
+ PublicKeyCredential.parseRegistrationResponseJson(publicKeyCredentialJson);
+ boolean success = mfaService.finishRegistration(authenticatedUser.getUser(), pkc);
+ if (!success) {
+ throw new WsException(WsError.INTERNAL_ERROR);
+ }
+ return Response.ok().build();
+ } catch (IOException e) {
+ logger.error("publicKeyCredentialJson parsing error", e);
+ return Response.serverError().build();
+ }
+ }
+
+ @GET
+ @Operation(description = "Get an assertion for the user")
+ @Path("/webauth/assertion/{username}")
+ public String getAssertion(@PathParam("username") String userName) {
+ // Generate the PublicKeyCredentialRequestOptions
+ AssertionRequest options = mfaService.getAssertionRequest(userName);
+ try {
+ return options.toCredentialsGetJson();
+ } catch (JsonProcessingException e) {
+ logger.debug("erreur lors de la génération du credentialGetOptions", e);
+ throw new WsException(WsError.INTERNAL_ERROR);
+ }
+ }
+
+ @POST
+ @Operation(description = "Start the authentication with WebAuthn")
+ @Path("/webauth/verify")
+ public Response verifyCredential(String publicKeyCredentialJson) {
+ try {
+ PublicKeyCredential pkc =
+ PublicKeyCredential.parseAssertionResponseJson(publicKeyCredentialJson);
+
+ // Verify the assertion
+ AdminUser user = mfaService.verifyWebauth(pkc);
+ FingerprintWithHash fingerprintWithHash = sessionUseFingerprintCookie ? generateFingerprint() : NULL_FINGERPRINT;
+ return withFingerprintCookie(
+ Response.ok(toAdminSession(toWebSession(adminUserService.authenticateWithMfa(user), fingerprintWithHash.getHash()))),
+ fingerprintWithHash.getFingerprint()
+ )
+ .build();
+
+ } catch (IOException e) {
+ throw new WsException(AdminWsError.WRONG_LOGIN_OR_PASSWORD);
+ }
+ }
+
+ // ------------------------ Sessions ------------------------
@PUT
@Consumes(MediaType.TEXT_PLAIN)
@Operation(description = "Renew a valid session token")
@@ -120,6 +293,26 @@ public AdminSession renew(String webSessionSerialized) {
return toAdminSession(parsedSession);
}
+ public AuthenticatedUser authenticateUserWithAuthenticator(AdminAuthenticatorCredentials credentials) {
+ Validators.checkRequired("Json creadentials", credentials);
+ Validators.checkRequired("users.USERNAME", credentials.getUserName());
+ Validators.checkRequired("users.CODE", credentials.getCode());
+
+ if(credentials.getUserName() != null && failAttemptsManager.isBlocked(credentials.getUserName())) {
+ throw new WsException(
+ AdminWsError.TOO_MANY_WRONG_ATTEMPS,
+ ImmutableList.of(String.valueOf(blockedDurationInSeconds))
+ );
+ }
+
+ return adminUserService
+ .authenticateWithAuthenticator(credentials.getUserName(), credentials.getCode())
+ .orElseThrow(() -> {
+ failAttemptsManager.addAttempt(credentials.getUserName());
+ return new WsException(AdminWsError.WRONG_LOGIN_OR_PASSWORD);
+ });
+ }
+
public AuthenticatedUser authenticateUser(AdminCredentials credentials) {
Validators.checkRequired("Json creadentials", credentials);
Validators.checkRequired("users.USERNAME", credentials.getUserName());
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java
new file mode 100644
index 0000000..4806cdf
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java
@@ -0,0 +1,13 @@
+package com.coreoz.plume.admin.webservices.data.session;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class AdminAuthenticatorCredentials {
+
+ private String userName;
+ private int code;
+
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaQrcode.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaQrcode.java
new file mode 100644
index 0000000..b168354
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaQrcode.java
@@ -0,0 +1,10 @@
+package com.coreoz.plume.admin.webservices.data.session;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class AdminMfaQrcode {
+ private String qrcode;
+}
diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminPublicKeyCredentials.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminPublicKeyCredentials.java
new file mode 100644
index 0000000..f62a5e8
--- /dev/null
+++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminPublicKeyCredentials.java
@@ -0,0 +1,13 @@
+package com.coreoz.plume.admin.webservices.data.session;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class AdminPublicKeyCredentials {
+
+ private AdminCredentials credentials;
+ private String publicKeyCredentialJson;
+
+}