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 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 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 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 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; + +}