From bbf8a8915d76ce579534187f7ac024a5c10ff9bb Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Fri, 6 Sep 2024 16:02:48 +0200 Subject: [PATCH 1/6] Add a qrcode MFA, webservice, service and encryption for the secret key --- plume-admin-security/pom.xml | 18 +++- .../AdminSecurityConfigurationService.java | 4 + .../websession/MfaSecretKeyEncryption.java | 57 ++++++++++++ .../MfaSecretKeyEncryptionProvider.java | 23 +++++ plume-admin-ws/sql/setup-mssql.sql | 3 +- plume-admin-ws/sql/setup-mysql.sql | 3 +- plume-admin-ws/sql/setup-oracle.sql | 1 + .../plume/admin/db/generated/AdminUser.java | 11 +++ .../plume/admin/db/generated/QAdminUser.java | 3 + .../AdminConfigurationService.java | 5 + .../plume/admin/services/mfa/MfaService.java | 84 +++++++++++++++++ .../admin/services/user/AdminUserService.java | 24 ++++- .../plume/admin/webservices/SessionWs.java | 92 +++++++++++++++++++ .../data/session/AdminMfaCredentials.java | 13 +++ .../data/session/AdminMfaQrcode.java | 10 ++ 15 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryption.java create mode 100644 plume-admin-security/src/main/java/com/coreoz/plume/admin/websession/MfaSecretKeyEncryptionProvider.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaQrcode.java diff --git a/plume-admin-security/pom.xml b/plume-admin-security/pom.xml index 5aa4f40..eaecc99 100644 --- a/plume-admin-security/pom.xml +++ b/plume-admin-security/pom.xml @@ -83,6 +83,22 @@ 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 +113,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/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..e1180b0 100644 --- a/plume-admin-ws/sql/setup-mysql.sql +++ b/plume-admin-ws/sql/setup-mysql.sql @@ -16,6 +16,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) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_plm_user_email` (`email`), UNIQUE KEY `uniq_plm_user_username` (`user_name`), @@ -32,7 +33,7 @@ CREATE TABLE `PLM_ROLE_PERMISSION` ( 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/generated/AdminUser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUser.java index 66b9b61..c7f56fa 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 @@ -33,6 +33,9 @@ public class AdminUser extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuery @Column("PASSWORD") private String password; + @Column("SECRET_KEY") + private String secretKey; + @Column("USER_NAME") private String userName; @@ -92,6 +95,14 @@ public void setPassword(String password) { this.password = password; } + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + public String getUserName() { return userName; } 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..2ee340d 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 @@ -38,6 +38,8 @@ public class QAdminUser extends com.querydsl.sql.RelationalPathBase { public final StringPath password = createString("password"); + public final StringPath secretKey = createString("secretKey"); + public final StringPath userName = createString("userName"); public final com.querydsl.sql.PrimaryKey constraintB3 = createPrimaryKey(id); @@ -77,6 +79,7 @@ public void addMetadata() { 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(secretKey, ColumnMetadata.named("SECRET_KEY").withIndex(9).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/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..8888f37 --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java @@ -0,0 +1,84 @@ +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.services.configuration.AdminConfigurationService; +import com.coreoz.plume.admin.websession.MfaSecretKeyEncryptionProvider; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; + +@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 MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider; + + @Inject + private MfaService( + AdminConfigurationService configurationService, + MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider + ) { + this.configurationService = configurationService; + this.mfaSecretKeyEncryptionProvider = mfaSecretKeyEncryptionProvider; + } + + 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(); + } + } +} 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..8988aaa 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 @@ -8,6 +8,7 @@ import com.coreoz.plume.admin.db.daos.AdminUserDao; import com.coreoz.plume.admin.db.generated.AdminUser; 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.db.crud.CrudService; @@ -21,15 +22,17 @@ public class AdminUserService extends CrudService { private final AdminUserDao adminUserDao; 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) { super(adminUserDao); this.adminUserDao = adminUserDao; this.adminRoleService = adminRoleService; + this.mfaService = mfaService; this.hashService = hashService; this.timeProvider = timeProvider; } @@ -44,6 +47,16 @@ public Optional authenticate(String userName, String password )); } + public Optional authenticateMfa(String userName, int code) { + return adminUserDao + .findByUserName(userName) + .filter(user -> mfaService.verifyCode(user.getSecretKey(), code)) + .map(user -> AuthenticatedUserAdmin.of( + user, + ImmutableSet.copyOf(adminRoleService.findRolePermissions(user.getIdRole())) + )); + } + public void update(AdminUserParameters parameters) { String newPassword = Strings.emptyToNull(parameters.getPassword()); adminUserDao.update( @@ -57,6 +70,15 @@ public void update(AdminUserParameters parameters) { ); } + public String createMfaSecretKey(Long idUser) throws Exception { + AdminUser user = adminUserDao.findById(idUser); + String secretKey = mfaService.generateSecretKey(); + user.setSecretKey(mfaService.hashSecretKey(secretKey)); + 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..0091753 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 @@ -8,9 +8,12 @@ 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.user.AdminUserService; import com.coreoz.plume.admin.services.user.AuthenticatedUser; import com.coreoz.plume.admin.webservices.data.session.AdminCredentials; +import com.coreoz.plume.admin.webservices.data.session.AdminMfaCredentials; +import com.coreoz.plume.admin.webservices.data.session.AdminMfaQrcode; 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,6 +21,7 @@ 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; @@ -32,9 +36,14 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; 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 +56,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 +81,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 +116,64 @@ public Response authenticate(AdminCredentials credentials) { .build(); } + @POST + @Operation(description = "Generate a qrcode for MFA enrollment") + @Path("/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.createMfaSecretKey(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("/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.createMfaSecretKey(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("/verify-mfa") + @Operation(description = "Verify MFA code") + public Response verifyMfa(AdminMfaCredentials credentials) { + // first user needs to be authenticated (an exception will be raised otherwise) + AuthenticatedUser authenticatedUser = authenticateUserMfa(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(); + } + @PUT @Consumes(MediaType.TEXT_PLAIN) @Operation(description = "Renew a valid session token") @@ -120,6 +192,26 @@ public AdminSession renew(String webSessionSerialized) { return toAdminSession(parsedSession); } + public AuthenticatedUser authenticateUserMfa(AdminMfaCredentials 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 + .authenticateMfa(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/AdminMfaCredentials.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java new file mode 100644 index 0000000..d323737 --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java @@ -0,0 +1,13 @@ +package com.coreoz.plume.admin.webservices.data.session; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class AdminMfaCredentials { + + 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; +} From d1aea4d0eca9cc0258ef9303f479f0a39102121c Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Fri, 6 Sep 2024 17:55:36 +0200 Subject: [PATCH 2/6] Handles multi mfa for a user --- plume-admin-ws/sql/setup-mysql.sql | 27 ++++++- .../plume/admin/db/daos/AdminMfaDao.java | 56 ++++++++++++++ .../plume/admin/db/generated/AdminMfa.java | 76 +++++++++++++++++++ .../plume/admin/db/generated/AdminUser.java | 11 --- .../admin/db/generated/AdminUserMfa.java | 67 ++++++++++++++++ .../plume/admin/db/generated/QAdminMfa.java | 69 +++++++++++++++++ .../plume/admin/db/generated/QAdminUser.java | 3 - .../admin/db/generated/QAdminUserMfa.java | 68 +++++++++++++++++ .../plume/admin/services/mfa/MfaTypeEnum.java | 16 ++++ .../admin/services/user/AdminUserService.java | 30 ++++++-- .../plume/admin/webservices/SessionWs.java | 19 ++--- ...ava => AdminAuthenticatorCredentials.java} | 2 +- 12 files changed, 411 insertions(+), 33 deletions(-) create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaDao.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaTypeEnum.java rename plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/{AdminMfaCredentials.java => AdminAuthenticatorCredentials.java} (78%) diff --git a/plume-admin-ws/sql/setup-mysql.sql b/plume-admin-ws/sql/setup-mysql.sql index e1180b0..b163c86 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,14 +19,13 @@ CREATE TABLE `PLM_USER` ( `email` varchar(255) NOT NULL, `user_name` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, - `secret_key` varchar(255) 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, @@ -32,8 +34,25 @@ CREATE TABLE `PLM_ROLE_PERMISSION` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `PLM_MFA`; +CREATE TABLE `PLM_MFA` ( + `id` bigint(20) NOT NULL, + `type` ENUM('authenticator', 'browser') NOT NULL, + `secret_key` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `PLM_USER_MFA` ( + `id_user` bigint(20) NOT NULL, + `id_mfa` bigint(20) NOT NULL, + PRIMARY KEY (`id_user`, `id_mfa`), + CONSTRAINT `plm_user_mfa_user` FOREIGN KEY (`id_user`) REFERENCES `PLM_USER` (`id`), + CONSTRAINT `plm_user_mfa_mfa` FOREIGN KEY (`id_mfa`) REFERENCES `PLM_MFA` (`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', NULL); +INSERT INTO PLM_USER VALUES(1, 1, NOW(), 'Admin', 'Admin', 'admin@admin', 'admin', '$2a$11$FfgtfoHeNo/m9jGj9D5rTO0zDDI4LkMXnXHai744Ee32P3CHoBVqm'); 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/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..705f43b --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaDao.java @@ -0,0 +1,56 @@ +package com.coreoz.plume.admin.db.daos; + +import java.util.List; + +import javax.inject.Inject; + +import com.coreoz.plume.admin.db.generated.AdminMfa; +import com.coreoz.plume.admin.db.generated.QAdminMfa; +import com.coreoz.plume.admin.db.generated.QAdminUserMfa; +import com.coreoz.plume.admin.services.mfa.MfaTypeEnum; +import com.coreoz.plume.db.querydsl.crud.CrudDaoQuerydsl; +import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; + +public class AdminMfaDao extends CrudDaoQuerydsl { + @Inject + public AdminMfaDao(TransactionManagerQuerydsl transactionManager) { + super(transactionManager, QAdminMfa.adminMfa); + } + + public List findByUserId(long userId) { + return selectFrom() + .join(QAdminUserMfa.adminUserMfa) + .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) + .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId)) + .fetch(); + } + + public List findMfaByUserIdAndType(long userId, MfaTypeEnum type) { + return selectFrom() + .join(QAdminUserMfa.adminUserMfa) + .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) + .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId) + .and(QAdminMfa.adminMfa.type.eq(type.getType()))) + .fetch(); + } + + public void addMfaToUser(long userId, AdminMfa mfa) { + long mfaId = save(mfa).getId(); + transactionManager.insert(QAdminUserMfa.adminUserMfa) + .set(QAdminUserMfa.adminUserMfa.idMfa, mfaId) + .set(QAdminUserMfa.adminUserMfa.idUser, userId) + .execute(); + } + + public void removeMfaFromUser(long userId, long mfaId) { + AdminMfa mfa = findById(mfaId); + if (mfa == null) { + return; + } + transactionManager.delete(QAdminUserMfa.adminUserMfa) + .where(QAdminUserMfa.adminUserMfa.idMfa.eq(mfaId) + .and(QAdminUserMfa.adminUserMfa.idUser.eq(userId))) + .execute(); + delete(mfaId); + } +} diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java new file mode 100644 index 0000000..e6539e2 --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java @@ -0,0 +1,76 @@ +package com.coreoz.plume.admin.db.generated; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.annotation.processing.Generated; +import com.querydsl.sql.Column; + +/** + * AdminMfa is a Querydsl bean type + */ +@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") +public class AdminMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl { + + @Column("ID") + @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) + private Long id; + + @Column("SECRET_KEY") + private String secretKey; + + @Column("TYPE") + private String type; + + 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; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public String toString() { + return "AdminMfa#" + id; + } + + @Override + public boolean equals(Object o) { + if (id == null) { + return super.equals(o); + } + if (!(o instanceof AdminMfa)) { + return false; + } + AdminMfa obj = (AdminMfa) 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 c7f56fa..66b9b61 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 @@ -33,9 +33,6 @@ public class AdminUser extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuery @Column("PASSWORD") private String password; - @Column("SECRET_KEY") - private String secretKey; - @Column("USER_NAME") private String userName; @@ -95,14 +92,6 @@ public void setPassword(String password) { this.password = password; } - public String getSecretKey() { - return secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - public String getUserName() { return userName; } 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..8a4a5de --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java @@ -0,0 +1,67 @@ +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 { + + @Column("ID_MFA") + @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) + private Long idMfa; + + @Column("ID_USER") + @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) + private Long idUser; + + public Long getIdMfa() { + return idMfa; + } + + public void setIdMfa(Long idMfa) { + this.idMfa = idMfa; + } + + public Long getIdUser() { + return idUser; + } + + public void setIdUser(Long idUser) { + this.idUser = idUser; + } + + @Override + public String toString() { + return "AdminUserMfa#" + idMfa+ ";" + idUser; + } + + @Override + public boolean equals(Object o) { + if (idMfa == null || idUser == null) { + return super.equals(o); + } + if (!(o instanceof AdminUserMfa)) { + return false; + } + AdminUserMfa obj = (AdminUserMfa) o; + return idMfa.equals(obj.idMfa) && idUser.equals(obj.idUser); + } + + @Override + public int hashCode() { + if (idMfa == null || idUser == null) { + return super.hashCode(); + } + final int prime = 31; + int result = 1; + result = prime * result + idMfa.hashCode(); + result = prime * result + idUser.hashCode(); + return result; + } + +} + diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java new file mode 100644 index 0000000..b13d5a3 --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.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; + + + + +/** + * QAdminMfa is a Querydsl query type for AdminMfa + */ +@Generated("com.querydsl.sql.codegen.MetaDataSerializer") +public class QAdminMfa extends com.querydsl.sql.RelationalPathBase { + + private static final long serialVersionUID = 320633169; + + public static final QAdminMfa adminMfa = new QAdminMfa("PLM_MFA"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath secretKey = createString("secretKey"); + + public final StringPath type = createString("type"); + + public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id); + + public final com.querydsl.sql.ForeignKey _plmUserMfaMfa = createInvForeignKey(id, "ID_MFA"); + + public QAdminMfa(String variable) { + super(AdminMfa.class, forVariable(variable), "null", "PLM_MFA"); + addMetadata(); + } + + public QAdminMfa(String variable, String schema, String table) { + super(AdminMfa.class, forVariable(variable), schema, table); + addMetadata(); + } + + public QAdminMfa(String variable, String schema) { + super(AdminMfa.class, forVariable(variable), schema, "PLM_MFA"); + addMetadata(); + } + + public QAdminMfa(Path path) { + super(path.getType(), path.getMetadata(), "null", "PLM_MFA"); + addMetadata(); + } + + public QAdminMfa(PathMetadata metadata) { + super(AdminMfa.class, metadata, "null", "PLM_MFA"); + addMetadata(); + } + + public void addMetadata() { + addMetadata(id, ColumnMetadata.named("ID").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); + addMetadata(secretKey, ColumnMetadata.named("SECRET_KEY").withIndex(3).ofType(Types.VARCHAR).withSize(255)); + 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/db/generated/QAdminUser.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUser.java index 2ee340d..a1a8f8f 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 @@ -38,8 +38,6 @@ public class QAdminUser extends com.querydsl.sql.RelationalPathBase { public final StringPath password = createString("password"); - public final StringPath secretKey = createString("secretKey"); - public final StringPath userName = createString("userName"); public final com.querydsl.sql.PrimaryKey constraintB3 = createPrimaryKey(id); @@ -79,7 +77,6 @@ public void addMetadata() { 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(secretKey, ColumnMetadata.named("SECRET_KEY").withIndex(9).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..d7f8b9f --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java @@ -0,0 +1,68 @@ +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 = -1870946042; + + public static final QAdminUserMfa adminUserMfa = new QAdminUserMfa("PLM_USER_MFA"); + + public final NumberPath idMfa = createNumber("idMfa", Long.class); + + public final NumberPath idUser = createNumber("idUser", Long.class); + + public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(idMfa, idUser); + + public final com.querydsl.sql.ForeignKey plmUserMfaMfa = createForeignKey(idMfa, "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(idMfa, ColumnMetadata.named("ID_MFA").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull()); + addMetadata(idUser, ColumnMetadata.named("ID_USER").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); + } + +} + 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 8988aaa..22afc4d 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,14 +1,18 @@ 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.AdminMfa; import com.coreoz.plume.admin.db.generated.AdminUser; import com.coreoz.plume.admin.services.hash.HashService; import com.coreoz.plume.admin.services.mfa.MfaService; +import com.coreoz.plume.admin.services.mfa.MfaTypeEnum; import com.coreoz.plume.admin.services.role.AdminRoleService; import com.coreoz.plume.admin.webservices.data.user.AdminUserParameters; import com.coreoz.plume.db.crud.CrudService; @@ -20,6 +24,7 @@ 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; @@ -27,10 +32,12 @@ public class AdminUserService extends CrudService { @Inject public AdminUserService(AdminUserDao adminUserDao, AdminRoleService adminRoleService, - HashService hashService, TimeProvider timeProvider, MfaService mfaService) { + 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; @@ -47,10 +54,20 @@ public Optional authenticate(String userName, String password )); } - public Optional authenticateMfa(String userName, int code) { + public Optional authenticateWithAuthenticator(String userName, int code) { return adminUserDao .findByUserName(userName) - .filter(user -> mfaService.verifyCode(user.getSecretKey(), code)) + .filter(user -> { + List registeredAuthenticators = adminMfaDao.findMfaByUserIdAndType(user.getId(), MfaTypeEnum.AUTHENTICATOR); + // 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())) @@ -70,10 +87,13 @@ public void update(AdminUserParameters parameters) { ); } - public String createMfaSecretKey(Long idUser) throws Exception { + public String createMfaSecretKey(Long idUser, MfaTypeEnum type) throws Exception { AdminUser user = adminUserDao.findById(idUser); String secretKey = mfaService.generateSecretKey(); - user.setSecretKey(mfaService.hashSecretKey(secretKey)); + AdminMfa mfa = new AdminMfa(); + mfa.setSecretKey(mfaService.hashSecretKey(secretKey)); + mfa.setType(type.getType()); + adminMfaDao.addMfaToUser(user.getId(), mfa); adminUserDao.save(user); return secretKey; 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 0091753..bd5ab11 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 @@ -9,10 +9,11 @@ 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.webservices.data.session.AdminAuthenticatorCredentials; import com.coreoz.plume.admin.webservices.data.session.AdminCredentials; -import com.coreoz.plume.admin.webservices.data.session.AdminMfaCredentials; import com.coreoz.plume.admin.webservices.data.session.AdminMfaQrcode; import com.coreoz.plume.admin.webservices.data.session.AdminSession; import com.coreoz.plume.admin.webservices.validation.AdminWsError; @@ -56,13 +57,13 @@ @Singleton public class SessionWs { - private static final Logger logger = LoggerFactory.getLogger(SessionWs.class); + 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 MfaService mfaService; private final TimeProvider timeProvider; private final LoginFailAttemptsManager failAttemptsManager; @@ -125,7 +126,7 @@ public AdminMfaQrcode qrCodeUrl(AdminCredentials credentials) { // Generate MFA secret key and QR code URL try { - String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId()); + String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId(), MfaTypeEnum.AUTHENTICATOR); String qrCodeUrl = mfaService.getQRBarcodeURL(authenticatedUser.getUser().getUserName(), secretKey); // Return the QR code URL to the client @@ -145,7 +146,7 @@ public Response qrCode(AdminCredentials credentials) { // Generate MFA secret key and QR code URL try { - String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId()); + String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId(), MfaTypeEnum.AUTHENTICATOR); byte[] qrCode = mfaService.generateQRCode(secretKey, secretKey); // Return the QR code image to the client @@ -162,9 +163,9 @@ public Response qrCode(AdminCredentials credentials) { @POST @Path("/verify-mfa") @Operation(description = "Verify MFA code") - public Response verifyMfa(AdminMfaCredentials credentials) { + public Response verifyMfa(AdminAuthenticatorCredentials credentials) { // first user needs to be authenticated (an exception will be raised otherwise) - AuthenticatedUser authenticatedUser = authenticateUserMfa(credentials); + AuthenticatedUser authenticatedUser = authenticateUserWithAuthenticator(credentials); // if the client is authenticated, the fingerprint can be generated if needed FingerprintWithHash fingerprintWithHash = sessionUseFingerprintCookie ? generateFingerprint() : NULL_FINGERPRINT; return withFingerprintCookie( @@ -192,7 +193,7 @@ public AdminSession renew(String webSessionSerialized) { return toAdminSession(parsedSession); } - public AuthenticatedUser authenticateUserMfa(AdminMfaCredentials credentials) { + public AuthenticatedUser authenticateUserWithAuthenticator(AdminAuthenticatorCredentials credentials) { Validators.checkRequired("Json creadentials", credentials); Validators.checkRequired("users.USERNAME", credentials.getUserName()); Validators.checkRequired("users.CODE", credentials.getCode()); @@ -205,7 +206,7 @@ public AuthenticatedUser authenticateUserMfa(AdminMfaCredentials credentials) { } return adminUserService - .authenticateMfa(credentials.getUserName(), credentials.getCode()) + .authenticateWithAuthenticator(credentials.getUserName(), credentials.getCode()) .orElseThrow(() -> { failAttemptsManager.addAttempt(credentials.getUserName()); return new WsException(AdminWsError.WRONG_LOGIN_OR_PASSWORD); diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java similarity index 78% rename from plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java rename to plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java index d323737..4806cdf 100644 --- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminMfaCredentials.java +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminAuthenticatorCredentials.java @@ -5,7 +5,7 @@ @Setter @Getter -public class AdminMfaCredentials { +public class AdminAuthenticatorCredentials { private String userName; private int code; From 9b16f1adbe7bd31f83bc80fbe0588dddee3012be Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Mon, 9 Sep 2024 17:38:37 +0200 Subject: [PATCH 3/6] Start of implementation of browser authent with yubico --- plume-admin-security/pom.xml | 11 ++- plume-admin-ws/sql/setup-mysql.sql | 4 +- .../db/daos/AdminMfaBrowserCredentialDao.java | 99 +++++++++++++++++++ .../plume/admin/db/generated/AdminMfa.java | 22 +++-- .../plume/admin/db/generated/AdminUser.java | 34 ++++--- .../admin/db/generated/AdminUserMfa.java | 9 +- .../plume/admin/db/generated/QAdminMfa.java | 13 ++- .../plume/admin/db/generated/QAdminUser.java | 33 ++++--- .../admin/db/generated/QAdminUserMfa.java | 10 +- .../plume/admin/services/mfa/MfaService.java | 46 ++++++++- .../plume/admin/webservices/SessionWs.java | 18 ++++ 11 files changed, 242 insertions(+), 57 deletions(-) create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaBrowserCredentialDao.java diff --git a/plume-admin-security/pom.xml b/plume-admin-security/pom.xml index eaecc99..7650f25 100644 --- a/plume-admin-security/pom.xml +++ b/plume-admin-security/pom.xml @@ -83,12 +83,13 @@ assertj-core test + + com.warrenstrange googleauth 1.5.0 - com.google.zxing core @@ -99,6 +100,14 @@ javase 3.4.1 + + + + com.yubico + webauthn-server-core + 2.5.3 + diff --git a/plume-admin-ws/sql/setup-mysql.sql b/plume-admin-ws/sql/setup-mysql.sql index b163c86..472c2b6 100644 --- a/plume-admin-ws/sql/setup-mysql.sql +++ b/plume-admin-ws/sql/setup-mysql.sql @@ -19,6 +19,7 @@ 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`), @@ -39,6 +40,7 @@ CREATE TABLE `PLM_MFA` ( `id` bigint(20) NOT NULL, `type` ENUM('authenticator', 'browser') NOT NULL, `secret_key` varchar(255) DEFAULT NULL, + `credential_id` BLOB DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -52,7 +54,7 @@ CREATE TABLE `PLM_USER_MFA` ( ) 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/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..9dea5c7 --- /dev/null +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaBrowserCredentialDao.java @@ -0,0 +1,99 @@ +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.AdminMfa; +import com.coreoz.plume.admin.db.generated.QAdminMfa; +import com.coreoz.plume.admin.db.generated.QAdminUser; +import com.coreoz.plume.admin.db.generated.QAdminUserMfa; +import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; +import com.yubico.webauthn.CredentialRepository; +import com.yubico.webauthn.RegisteredCredential; +import com.yubico.webauthn.data.AuthenticatorTransport; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; +import com.yubico.webauthn.data.PublicKeyCredentialType; +import com.yubico.webauthn.data.PublicKeyCredentialDescriptor.PublicKeyCredentialDescriptorBuilder; + +public class AdminMfaBrowserCredentialDao implements CredentialRepository { + + private final TransactionManagerQuerydsl transactionManager; + + @Inject + public AdminMfaBrowserCredentialDao(TransactionManagerQuerydsl transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public Set getCredentialIdsForUsername(String username) { + List results = transactionManager.selectQuery() + .select(QAdminMfa.adminMfa.credentialId) + .from(QAdminMfa.adminMfa) + .join(QAdminUserMfa.adminUserMfa) + .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.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) { + String username = getUsernameForUserHandle(userHandle).orElse(null); + if (username == null) { + return Optional.empty(); + } + AdminMfa mfa = transactionManager.selectQuery() + .select(QAdminMfa.adminMfa) + .from(QAdminMfa.adminMfa) + .join(QAdminUserMfa.adminUserMfa) + .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) + .join(QAdminUser.adminUser) + .on(QAdminUser.adminUser.id.eq(QAdminUserMfa.adminUserMfa.idUser)) + .where(QAdminMfa.adminMfa.credentialId.eq(credentialId.getBytes()) + .and(QAdminUser.adminUser.userName.eq(username))) + .fetchOne(); + throw new UnsupportedOperationException("Unimplemented method 'lookup'"); + } + + @Override + public Set lookupAll(ByteArray credentialId) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'lookupAll'"); + } + +} diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java index e6539e2..c0093df 100644 --- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java @@ -10,16 +10,27 @@ @Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") public class AdminMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl { - @Column("ID") + @Column("credential_id") + private byte[] credentialId; + + @Column("id") @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) private Long id; - @Column("SECRET_KEY") + @Column("secret_key") private String secretKey; - @Column("TYPE") + @Column("type") private String type; + public byte[] getCredentialId() { + return credentialId; + } + + public void setCredentialId(byte[] credentialId) { + this.credentialId = credentialId; + } + public Long getId() { return id; } @@ -44,11 +55,6 @@ public void setType(String type) { this.type = type; } - @Override - public String toString() { - return "AdminMfa#" + id; - } - @Override public boolean equals(Object o) { if (id == null) { 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 index 8a4a5de..00dd02a 100644 --- 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 @@ -10,11 +10,11 @@ @Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") public class AdminUserMfa { - @Column("ID_MFA") + @Column("id_mfa") @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) private Long idMfa; - @Column("ID_USER") + @Column("id_user") @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) private Long idUser; @@ -34,11 +34,6 @@ public void setIdUser(Long idUser) { this.idUser = idUser; } - @Override - public String toString() { - return "AdminUserMfa#" + idMfa+ ";" + idUser; - } - @Override public boolean equals(Object o) { if (idMfa == null || idUser == null) { diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java index b13d5a3..c6dcb2e 100644 --- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java @@ -20,10 +20,12 @@ @Generated("com.querydsl.sql.codegen.MetaDataSerializer") public class QAdminMfa extends com.querydsl.sql.RelationalPathBase { - private static final long serialVersionUID = 320633169; + private static final long serialVersionUID = 2054315861; public static final QAdminMfa adminMfa = new QAdminMfa("PLM_MFA"); + public final SimplePath credentialId = createSimple("credentialId", byte[].class); + public final NumberPath id = createNumber("id", Long.class); public final StringPath secretKey = createString("secretKey"); @@ -32,7 +34,7 @@ public class QAdminMfa extends com.querydsl.sql.RelationalPathBase { public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id); - public final com.querydsl.sql.ForeignKey _plmUserMfaMfa = createInvForeignKey(id, "ID_MFA"); + public final com.querydsl.sql.ForeignKey _plmUserMfaMfa = createInvForeignKey(id, "id_mfa"); public QAdminMfa(String variable) { super(AdminMfa.class, forVariable(variable), "null", "PLM_MFA"); @@ -60,9 +62,10 @@ public QAdminMfa(PathMetadata metadata) { } public void addMetadata() { - addMetadata(id, ColumnMetadata.named("ID").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); - addMetadata(secretKey, ColumnMetadata.named("SECRET_KEY").withIndex(3).ofType(Types.VARCHAR).withSize(255)); - addMetadata(type, ColumnMetadata.named("TYPE").withIndex(2).ofType(Types.VARCHAR).withSize(13).notNull()); + addMetadata(credentialId, ColumnMetadata.named("credential_id").withIndex(4).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(3).ofType(Types.VARCHAR).withSize(255)); + 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/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 index d7f8b9f..96fa5f1 100644 --- 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 @@ -20,7 +20,7 @@ @Generated("com.querydsl.sql.codegen.MetaDataSerializer") public class QAdminUserMfa extends com.querydsl.sql.RelationalPathBase { - private static final long serialVersionUID = -1870946042; + private static final long serialVersionUID = -291052278; public static final QAdminUserMfa adminUserMfa = new QAdminUserMfa("PLM_USER_MFA"); @@ -30,9 +30,9 @@ public class QAdminUserMfa extends com.querydsl.sql.RelationalPathBase primary = createPrimaryKey(idMfa, idUser); - public final com.querydsl.sql.ForeignKey plmUserMfaMfa = createForeignKey(idMfa, "ID"); + public final com.querydsl.sql.ForeignKey plmUserMfaMfa = createForeignKey(idMfa, "id"); - public final com.querydsl.sql.ForeignKey plmUserMfaUser = createForeignKey(idUser, "ID"); + public final com.querydsl.sql.ForeignKey plmUserMfaUser = createForeignKey(idUser, "id"); public QAdminUserMfa(String variable) { super(AdminUserMfa.class, forVariable(variable), "null", "PLM_USER_MFA"); @@ -60,8 +60,8 @@ public QAdminUserMfa(PathMetadata metadata) { } public void addMetadata() { - addMetadata(idMfa, ColumnMetadata.named("ID_MFA").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull()); - addMetadata(idUser, ColumnMetadata.named("ID_USER").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); + addMetadata(idMfa, ColumnMetadata.named("id_mfa").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull()); + addMetadata(idUser, ColumnMetadata.named("id_user").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); } } 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 index 8888f37..633a328 100644 --- 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 @@ -9,16 +9,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; +import java.util.Random; 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.services.configuration.AdminConfigurationService; import com.coreoz.plume.admin.websession.MfaSecretKeyEncryptionProvider; import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.yubico.webauthn.RelyingParty; +import com.yubico.webauthn.StartRegistrationOptions; +import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; +import com.yubico.webauthn.data.RelyingPartyIdentity; +import com.yubico.webauthn.data.UserIdentity; @Singleton public class MfaService { @@ -28,16 +36,30 @@ public class MfaService { private final GoogleAuthenticator authenticator = new GoogleAuthenticator(); private final AdminConfigurationService configurationService; private final MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider; + private final RelyingParty relyingParty; + private final Random random = new Random(); @Inject private MfaService( AdminConfigurationService configurationService, - MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider + MfaSecretKeyEncryptionProvider mfaSecretKeyEncryptionProvider, + AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao ) { this.configurationService = configurationService; + // TODO: Avoir un conf une liste des mfa à activer this.mfaSecretKeyEncryptionProvider = mfaSecretKeyEncryptionProvider; + RelyingPartyIdentity identity = RelyingPartyIdentity.builder() + .id("com.coreoz") // 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(); @@ -81,4 +103,24 @@ public static byte[] generateQRCodeImage(String barcodeText, int width, int heig return pngOutputStream.toByteArray(); } } + + // --------------------- Browser --------------------- + + public PublicKeyCredentialCreationOptions startRegistration(String userName) { + byte[] userHandle = new byte[64]; + random.nextBytes(userHandle); + StartRegistrationOptions options = StartRegistrationOptions.builder() + .user(UserIdentity.builder() + .name(userName) + .displayName(userName) + .id(new ByteArray(userHandle)) + .build()) + .build(); + return relyingParty.startRegistration(options); + } + + private Optional findExistingUser(String username) { + return Optional.empty(); + } + } 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 bd5ab11..fe452f6 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 @@ -29,10 +29,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; import com.google.common.net.HttpHeaders; +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; @@ -117,6 +119,8 @@ public Response authenticate(AdminCredentials credentials) { .build(); } + // ------------------------ QR / Code Authenticator ------------------------ + @POST @Operation(description = "Generate a qrcode for MFA enrollment") @Path("/qrcode-url") @@ -175,6 +179,20 @@ public Response verifyMfa(AdminAuthenticatorCredentials credentials) { .build(); } + // ------------------------ Browser Authenticator ------------------------ + + @POST + @Operation(description = "Get the list of MFA credentials for the user") + @Path("/start-registration") + public PublicKeyCredentialCreationOptions getWebAuthentCreationOptions(AdminCredentials credentials) { + // First user needs to be authenticated (an exception will be raised otherwise) + AuthenticatedUser authenticatedUser = authenticateUser(credentials); + + // Generate the PublicKeyCredentialCreationOptions + return mfaService.startRegistration(authenticatedUser.getUser().getUserName()); + } + + // ------------------------ Sessions ------------------------ @PUT @Consumes(MediaType.TEXT_PLAIN) @Operation(description = "Renew a valid session token") From c3c4459c6cfb80155bb61bcc3e1cb91e1ccad0b1 Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Mon, 9 Sep 2024 17:53:46 +0200 Subject: [PATCH 4/6] Ajout d'un bout de code pour finir l'enregistrement web authent --- .../db/daos/AdminMfaBrowserCredentialDao.java | 1 - .../plume/admin/services/mfa/MfaService.java | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 index 9dea5c7..a48397c 100644 --- 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 @@ -14,7 +14,6 @@ import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; import com.yubico.webauthn.CredentialRepository; import com.yubico.webauthn.RegisteredCredential; -import com.yubico.webauthn.data.AuthenticatorTransport; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.PublicKeyCredentialType; 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 index 633a328..a8c5665 100644 --- 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 @@ -21,12 +21,17 @@ import com.coreoz.plume.admin.websession.MfaSecretKeyEncryptionProvider; import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.yubico.webauthn.FinishRegistrationOptions; import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartRegistrationOptions; +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.PublicKeyCredentialCreationOptions; import com.yubico.webauthn.data.RelyingPartyIdentity; import com.yubico.webauthn.data.UserIdentity; +import com.yubico.webauthn.exception.RegistrationFailedException; @Singleton public class MfaService { @@ -119,6 +124,27 @@ public PublicKeyCredentialCreationOptions startRegistration(String userName) { return relyingParty.startRegistration(options); } + public boolean finishRegistration(String responseJson, PublicKeyCredentialCreationOptions request) { + try { + PublicKeyCredential response = + PublicKeyCredential.parseRegistrationResponseJson(responseJson); + + relyingParty.finishRegistration( + FinishRegistrationOptions.builder() + .request(request) + .response(response) + .build() + ); + return true; + } catch (IOException e) { + logger.error("Error parsing registration response", e); + return false; + } catch (RegistrationFailedException e) { + logger.error("Error finishing registration", e); + return false; + } + } + private Optional findExistingUser(String username) { return Optional.empty(); } From 836263418d1f7aa1d81a250d39bdeb4eb7856539 Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Fri, 13 Sep 2024 18:32:52 +0200 Subject: [PATCH 5/6] continue webauthn with registered of the public key --- plume-admin-security/pom.xml | 8 -- plume-admin-ws/pom.xml | 24 +++- plume-admin-ws/sql/setup-mysql.sql | 27 +++- .../db/daos/AdminMfaAuthenticatorDao.java | 18 +++ .../db/daos/AdminMfaBrowserCredentialDao.java | 56 ++++++--- .../plume/admin/db/daos/AdminMfaDao.java | 76 ++++++++---- .../plume/admin/db/daos/AdminUserMfaDao.java | 28 +++++ ...minMfa.java => AdminMfaAuthenticator.java} | 19 +-- .../admin/db/generated/AdminMfaBrowser.java | 115 ++++++++++++++++++ .../admin/db/generated/AdminUserMfa.java | 58 +++++++-- .../plume/admin/db/generated/QAdminMfa.java | 72 ----------- .../db/generated/QAdminMfaAuthenticator.java | 69 +++++++++++ .../admin/db/generated/QAdminMfaBrowser.java | 81 ++++++++++++ .../admin/db/generated/QAdminUserMfa.java | 21 +++- .../plume/admin/services/mfa/MfaService.java | 40 ++++-- .../admin/services/user/AdminUserService.java | 12 +- .../plume/admin/webservices/SessionWs.java | 44 ++++++- .../session/AdminPublicKeyCredentials.java | 13 ++ 18 files changed, 593 insertions(+), 188 deletions(-) create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminMfaAuthenticatorDao.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/daos/AdminUserMfaDao.java rename plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/{AdminMfa.java => AdminMfaAuthenticator.java} (79%) create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaBrowser.java delete mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaAuthenticator.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfaBrowser.java create mode 100644 plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/data/session/AdminPublicKeyCredentials.java diff --git a/plume-admin-security/pom.xml b/plume-admin-security/pom.xml index 7650f25..3e106ee 100644 --- a/plume-admin-security/pom.xml +++ b/plume-admin-security/pom.xml @@ -100,14 +100,6 @@ javase 3.4.1 - - - - com.yubico - webauthn-server-core - 2.5.3 - 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-mysql.sql b/plume-admin-ws/sql/setup-mysql.sql index 472c2b6..0b4d93c 100644 --- a/plume-admin-ws/sql/setup-mysql.sql +++ b/plume-admin-ws/sql/setup-mysql.sql @@ -35,22 +35,37 @@ CREATE TABLE `PLM_ROLE_PERMISSION` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `PLM_MFA`; -CREATE TABLE `PLM_MFA` ( +DROP TABLE IF EXISTS `PLM_MFA_AUTHENTICATOR`; +CREATE TABLE `PLM_MFA_AUTHENTICATOR` ( `id` bigint(20) NOT NULL, - `type` ENUM('authenticator', 'browser') 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` bigint(20) NOT NULL, - PRIMARY KEY (`id_user`, `id_mfa`), + `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` FOREIGN KEY (`id_mfa`) REFERENCES `PLM_MFA` (`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'); 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 index a48397c..12a2a14 100644 --- 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 @@ -7,34 +7,68 @@ import javax.inject.Inject; -import com.coreoz.plume.admin.db.generated.AdminMfa; -import com.coreoz.plume.admin.db.generated.QAdminMfa; +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.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 - public AdminMfaBrowserCredentialDao(TransactionManagerQuerydsl transactionManager) { + 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); + } + @Override public Set getCredentialIdsForUsername(String username) { List results = transactionManager.selectQuery() - .select(QAdminMfa.adminMfa.credentialId) - .from(QAdminMfa.adminMfa) + .select(QAdminMfaBrowser.adminMfaBrowser.publicKeyCose) + .from(QAdminMfaBrowser.adminMfaBrowser) .join(QAdminUserMfa.adminUserMfa) - .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) + .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)) @@ -76,16 +110,6 @@ public Optional lookup(ByteArray credentialId, ByteArray u if (username == null) { return Optional.empty(); } - AdminMfa mfa = transactionManager.selectQuery() - .select(QAdminMfa.adminMfa) - .from(QAdminMfa.adminMfa) - .join(QAdminUserMfa.adminUserMfa) - .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) - .join(QAdminUser.adminUser) - .on(QAdminUser.adminUser.id.eq(QAdminUserMfa.adminUserMfa.idUser)) - .where(QAdminMfa.adminMfa.credentialId.eq(credentialId.getBytes()) - .and(QAdminUser.adminUser.userName.eq(username))) - .fetchOne(); throw new UnsupportedOperationException("Unimplemented method 'lookup'"); } 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 index 705f43b..f45de72 100644 --- 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 @@ -3,54 +3,76 @@ import java.util.List; import javax.inject.Inject; +import javax.inject.Singleton; -import com.coreoz.plume.admin.db.generated.AdminMfa; -import com.coreoz.plume.admin.db.generated.QAdminMfa; +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.crud.CrudDaoQuerydsl; import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; -public class AdminMfaDao extends CrudDaoQuerydsl { +@Singleton +public class AdminMfaDao { + + private final TransactionManagerQuerydsl transactionManager; + private final AdminMfaAuthenticatorDao adminMfaAuthenticatorDao; + private final AdminUserMfaDao adminUserMfaDao; + private final AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao; + @Inject - public AdminMfaDao(TransactionManagerQuerydsl transactionManager) { - super(transactionManager, QAdminMfa.adminMfa); + private AdminMfaDao( + TransactionManagerQuerydsl transactionManager, + AdminMfaAuthenticatorDao adminMfaAuthenticatorDao, + AdminUserMfaDao adminUserMfaDao, + AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao + ) { + this.transactionManager = transactionManager; + this.adminMfaAuthenticatorDao = adminMfaAuthenticatorDao; + this.adminUserMfaDao = adminUserMfaDao; + this.adminMfaBrowserCredentialDao = adminMfaBrowserCredentialDao; } - public List findByUserId(long userId) { - return selectFrom() + public List findAuthenticatorByUserId(long userId) { + return transactionManager.selectQuery() + .select(QAdminMfaAuthenticator.adminMfaAuthenticator) + .from(QAdminMfaAuthenticator.adminMfaAuthenticator) .join(QAdminUserMfa.adminUserMfa) - .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) - .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId)) + .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 findMfaByUserIdAndType(long userId, MfaTypeEnum type) { - return selectFrom() + public List findMfaBrowserByUserId(long userId) { + return transactionManager.selectQuery() + .select(QAdminMfaBrowser.adminMfaBrowser) + .from(QAdminMfaBrowser.adminMfaBrowser) .join(QAdminUserMfa.adminUserMfa) - .on(QAdminUserMfa.adminUserMfa.idMfa.eq(QAdminMfa.adminMfa.id)) + .on(QAdminUserMfa.adminUserMfa.idMfaBrowser.eq(QAdminMfaBrowser.adminMfaBrowser.id)) .where(QAdminUserMfa.adminUserMfa.idUser.eq(userId) - .and(QAdminMfa.adminMfa.type.eq(type.getType()))) + .and(QAdminUserMfa.adminUserMfa.type.eq(MfaTypeEnum.BROWSER.getType()))) .fetch(); } - public void addMfaToUser(long userId, AdminMfa mfa) { - long mfaId = save(mfa).getId(); - transactionManager.insert(QAdminUserMfa.adminUserMfa) - .set(QAdminUserMfa.adminUserMfa.idMfa, mfaId) - .set(QAdminUserMfa.adminUserMfa.idUser, userId) - .execute(); + 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 removeMfaFromUser(long userId, long mfaId) { - AdminMfa mfa = findById(mfaId); + public void removeMfaAuthenticatorFromUser(long userId, long mfaId) { + AdminMfaAuthenticator mfa = adminMfaAuthenticatorDao.findById(mfaId); if (mfa == null) { return; } - transactionManager.delete(QAdminUserMfa.adminUserMfa) - .where(QAdminUserMfa.adminUserMfa.idMfa.eq(mfaId) - .and(QAdminUserMfa.adminUserMfa.idUser.eq(userId))) - .execute(); - delete(mfaId); + 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/AdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java similarity index 79% rename from plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java rename to plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java index c0093df..665650e 100644 --- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfa.java +++ b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminMfaAuthenticator.java @@ -5,10 +5,10 @@ import com.querydsl.sql.Column; /** - * AdminMfa is a Querydsl bean type + * AdminMfaAuthenticator is a Querydsl bean type */ @Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") -public class AdminMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl { +public class AdminMfaAuthenticator extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl { @Column("credential_id") private byte[] credentialId; @@ -20,9 +20,6 @@ public class AdminMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQueryd @Column("secret_key") private String secretKey; - @Column("type") - private String type; - public byte[] getCredentialId() { return credentialId; } @@ -47,23 +44,15 @@ public void setSecretKey(String secretKey) { this.secretKey = secretKey; } - 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 AdminMfa)) { + if (!(o instanceof AdminMfaAuthenticator)) { return false; } - AdminMfa obj = (AdminMfa) o; + AdminMfaAuthenticator obj = (AdminMfaAuthenticator) o; return id.equals(obj.id); } 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/AdminUserMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/AdminUserMfa.java index 00dd02a..249fd24 100644 --- 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 @@ -8,22 +8,49 @@ * AdminUserMfa is a Querydsl bean type */ @Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") -public class AdminUserMfa { +public class AdminUserMfa extends com.coreoz.plume.db.querydsl.crud.CrudEntityQuerydsl { - @Column("id_mfa") + @Column("id") @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) - private Long idMfa; + 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; - public Long getIdMfa() { - return idMfa; + @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 setIdMfa(Long idMfa) { - this.idMfa = idMfa; + public void setIdMfaAuthenticator(Long idMfaAuthenticator) { + this.idMfaAuthenticator = idMfaAuthenticator; + } + + public Long getIdMfaBrowser() { + return idMfaBrowser; + } + + public void setIdMfaBrowser(Long idMfaBrowser) { + this.idMfaBrowser = idMfaBrowser; } public Long getIdUser() { @@ -34,27 +61,34 @@ 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 (idMfa == null || idUser == null) { + if (id == null) { return super.equals(o); } if (!(o instanceof AdminUserMfa)) { return false; } AdminUserMfa obj = (AdminUserMfa) o; - return idMfa.equals(obj.idMfa) && idUser.equals(obj.idUser); + return id.equals(obj.id); } @Override public int hashCode() { - if (idMfa == null || idUser == null) { + if (id == null) { return super.hashCode(); } final int prime = 31; int result = 1; - result = prime * result + idMfa.hashCode(); - result = prime * result + idUser.hashCode(); + result = prime * result + id.hashCode(); return result; } diff --git a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java deleted file mode 100644 index c6dcb2e..0000000 --- a/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminMfa.java +++ /dev/null @@ -1,72 +0,0 @@ -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; - - - - -/** - * QAdminMfa is a Querydsl query type for AdminMfa - */ -@Generated("com.querydsl.sql.codegen.MetaDataSerializer") -public class QAdminMfa extends com.querydsl.sql.RelationalPathBase { - - private static final long serialVersionUID = 2054315861; - - public static final QAdminMfa adminMfa = new QAdminMfa("PLM_MFA"); - - public final SimplePath credentialId = createSimple("credentialId", byte[].class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath secretKey = createString("secretKey"); - - public final StringPath type = createString("type"); - - public final com.querydsl.sql.PrimaryKey primary = createPrimaryKey(id); - - public final com.querydsl.sql.ForeignKey _plmUserMfaMfa = createInvForeignKey(id, "id_mfa"); - - public QAdminMfa(String variable) { - super(AdminMfa.class, forVariable(variable), "null", "PLM_MFA"); - addMetadata(); - } - - public QAdminMfa(String variable, String schema, String table) { - super(AdminMfa.class, forVariable(variable), schema, table); - addMetadata(); - } - - public QAdminMfa(String variable, String schema) { - super(AdminMfa.class, forVariable(variable), schema, "PLM_MFA"); - addMetadata(); - } - - public QAdminMfa(Path path) { - super(path.getType(), path.getMetadata(), "null", "PLM_MFA"); - addMetadata(); - } - - public QAdminMfa(PathMetadata metadata) { - super(AdminMfa.class, metadata, "null", "PLM_MFA"); - addMetadata(); - } - - public void addMetadata() { - addMetadata(credentialId, ColumnMetadata.named("credential_id").withIndex(4).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(3).ofType(Types.VARCHAR).withSize(255)); - 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/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/QAdminUserMfa.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/db/generated/QAdminUserMfa.java index 96fa5f1..cbc0fea 100644 --- 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 @@ -24,13 +24,21 @@ public class QAdminUserMfa extends com.querydsl.sql.RelationalPathBase idMfa = createNumber("idMfa", Long.class); + 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 com.querydsl.sql.PrimaryKey primary = createPrimaryKey(idMfa, idUser); + 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 plmUserMfaMfa = createForeignKey(idMfa, "id"); + public final com.querydsl.sql.ForeignKey plmUserMfaMfaBrowser = createForeignKey(idMfaBrowser, "id"); public final com.querydsl.sql.ForeignKey plmUserMfaUser = createForeignKey(idUser, "id"); @@ -60,8 +68,11 @@ public QAdminUserMfa(PathMetadata metadata) { } public void addMetadata() { - addMetadata(idMfa, ColumnMetadata.named("id_mfa").withIndex(2).ofType(Types.BIGINT).withSize(19).notNull()); - addMetadata(idUser, ColumnMetadata.named("id_user").withIndex(1).ofType(Types.BIGINT).withSize(19).notNull()); + 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/mfa/MfaService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java index a8c5665..32ac3a5 100644 --- 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 @@ -6,22 +6,27 @@ 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.generated.AdminUser; import com.coreoz.plume.admin.services.configuration.AdminConfigurationService; import com.coreoz.plume.admin.websession.MfaSecretKeyEncryptionProvider; import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.yubico.webauthn.FinishRegistrationOptions; +import com.yubico.webauthn.RegistrationResult; import com.yubico.webauthn.RelyingParty; import com.yubico.webauthn.StartRegistrationOptions; import com.yubico.webauthn.data.AuthenticatorAttestationResponse; @@ -40,9 +45,12 @@ public class MfaService { private final GoogleAuthenticator authenticator = new GoogleAuthenticator(); private final AdminConfigurationService configurationService; + private final AdminMfaBrowserCredentialDao adminMfaBrowserCredentialDao; 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(); @Inject private MfaService( @@ -53,8 +61,9 @@ private MfaService( this.configurationService = configurationService; // TODO: Avoir un conf une liste des mfa à activer this.mfaSecretKeyEncryptionProvider = mfaSecretKeyEncryptionProvider; + this.adminMfaBrowserCredentialDao = adminMfaBrowserCredentialDao; RelyingPartyIdentity identity = RelyingPartyIdentity.builder() - .id("com.coreoz") // TODO: Conf ? + .id("localhost") // TODO: Conf ? .name(configurationService.appName()) .build(); this.relyingParty = RelyingParty.builder() @@ -111,34 +120,39 @@ public static byte[] generateQRCodeImage(String barcodeText, int width, int heig // --------------------- Browser --------------------- - public PublicKeyCredentialCreationOptions startRegistration(String userName) { + public PublicKeyCredentialCreationOptions startRegistration(AdminUser user) { byte[] userHandle = new byte[64]; random.nextBytes(userHandle); StartRegistrationOptions options = StartRegistrationOptions.builder() .user(UserIdentity.builder() - .name(userName) - .displayName(userName) + .name(user.getUserName()) + .displayName(user.getUserName()) .id(new ByteArray(userHandle)) .build()) .build(); - return relyingParty.startRegistration(options); + PublicKeyCredentialCreationOptions createOptions = relyingParty.startRegistration(options); + createOptionCache.put(user.getId(), createOptions); + return createOptions; } - public boolean finishRegistration(String responseJson, PublicKeyCredentialCreationOptions request) { + public boolean finishRegistration( + AdminUser user, + PublicKeyCredential pkc + ) { + PublicKeyCredentialCreationOptions request = createOptionCache.getIfPresent(user.getId()); + if (request == null) { + return false; + } try { - PublicKeyCredential response = - PublicKeyCredential.parseRegistrationResponseJson(responseJson); - relyingParty.finishRegistration( + RegistrationResult result = relyingParty.finishRegistration( FinishRegistrationOptions.builder() .request(request) - .response(response) + .response(pkc) .build() ); + adminMfaBrowserCredentialDao.registerCredential(user, result, pkc); return true; - } catch (IOException e) { - logger.error("Error parsing registration response", e); - return false; } catch (RegistrationFailedException e) { logger.error("Error finishing registration", e); return false; 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 22afc4d..8063cf6 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 @@ -8,11 +8,10 @@ import com.coreoz.plume.admin.db.daos.AdminMfaDao; import com.coreoz.plume.admin.db.daos.AdminUserDao; -import com.coreoz.plume.admin.db.generated.AdminMfa; +import com.coreoz.plume.admin.db.generated.AdminMfaAuthenticator; import com.coreoz.plume.admin.db.generated.AdminUser; import com.coreoz.plume.admin.services.hash.HashService; import com.coreoz.plume.admin.services.mfa.MfaService; -import com.coreoz.plume.admin.services.mfa.MfaTypeEnum; import com.coreoz.plume.admin.services.role.AdminRoleService; import com.coreoz.plume.admin.webservices.data.user.AdminUserParameters; import com.coreoz.plume.db.crud.CrudService; @@ -58,7 +57,7 @@ public Optional authenticateWithAuthenticator(String userName return adminUserDao .findByUserName(userName) .filter(user -> { - List registeredAuthenticators = adminMfaDao.findMfaByUserIdAndType(user.getId(), MfaTypeEnum.AUTHENTICATOR); + List registeredAuthenticators = adminMfaDao.findAuthenticatorByUserId(user.getId()); // If any of the MFA is valid, then the user is valid return registeredAuthenticators.stream().anyMatch(authenticator -> { try { @@ -87,13 +86,12 @@ public void update(AdminUserParameters parameters) { ); } - public String createMfaSecretKey(Long idUser, MfaTypeEnum type) throws Exception { + public String createMfaAuthenticatorSecretKey(Long idUser) throws Exception { AdminUser user = adminUserDao.findById(idUser); String secretKey = mfaService.generateSecretKey(); - AdminMfa mfa = new AdminMfa(); + AdminMfaAuthenticator mfa = new AdminMfaAuthenticator(); mfa.setSecretKey(mfaService.hashSecretKey(secretKey)); - mfa.setType(type.getType()); - adminMfaDao.addMfaToUser(user.getId(), mfa); + adminMfaDao.addMfaAuthenticatorToUser(user.getId(), mfa); adminUserDao.save(user); return secretKey; 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 fe452f6..40e2445 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,5 +1,6 @@ package com.coreoz.plume.admin.webservices; +import java.io.IOException; import java.security.SecureRandom; import javax.inject.Inject; @@ -15,6 +16,7 @@ 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; @@ -26,9 +28,14 @@ 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.io.BaseEncoding; import com.google.common.net.HttpHeaders; +import com.yubico.webauthn.data.AuthenticatorAttestationResponse; +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; @@ -130,7 +137,7 @@ public AdminMfaQrcode qrCodeUrl(AdminCredentials credentials) { // Generate MFA secret key and QR code URL try { - String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId(), MfaTypeEnum.AUTHENTICATOR); + String secretKey = adminUserService.createMfaAuthenticatorSecretKey(authenticatedUser.getUser().getId()); String qrCodeUrl = mfaService.getQRBarcodeURL(authenticatedUser.getUser().getUserName(), secretKey); // Return the QR code URL to the client @@ -150,7 +157,7 @@ public Response qrCode(AdminCredentials credentials) { // Generate MFA secret key and QR code URL try { - String secretKey = adminUserService.createMfaSecretKey(authenticatedUser.getUser().getId(), MfaTypeEnum.AUTHENTICATOR); + String secretKey = adminUserService.createMfaAuthenticatorSecretKey(authenticatedUser.getUser().getId()); byte[] qrCode = mfaService.generateQRCode(secretKey, secretKey); // Return the QR code image to the client @@ -184,12 +191,41 @@ public Response verifyMfa(AdminAuthenticatorCredentials credentials) { @POST @Operation(description = "Get the list of MFA credentials for the user") @Path("/start-registration") - public PublicKeyCredentialCreationOptions getWebAuthentCreationOptions(AdminCredentials credentials) { + public String getWebAuthentCreationOptions(AdminCredentials credentials) { // First user needs to be authenticated (an exception will be raised otherwise) AuthenticatedUser authenticatedUser = authenticateUser(credentials); // Generate the PublicKeyCredentialCreationOptions - return mfaService.startRegistration(authenticatedUser.getUser().getUserName()); + 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("/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(); + } } // ------------------------ Sessions ------------------------ 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; + +} From 51696d6b98b4c6d55a58b525eacb3cac409a59b8 Mon Sep 17 00:00:00 2001 From: Benoit Vasseur Date: Mon, 16 Sep 2024 19:01:29 +0200 Subject: [PATCH 6/6] Add authent with webbrowser --- .../db/daos/AdminMfaBrowserCredentialDao.java | 56 +++++++++++++++-- .../plume/admin/services/mfa/MfaService.java | 59 ++++++++++++++++-- .../admin/services/user/AdminUserService.java | 10 ++++ .../plume/admin/webservices/SessionWs.java | 60 ++++++++++++++++--- 4 files changed, 168 insertions(+), 17 deletions(-) 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 index 12a2a14..1855582 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -62,6 +63,24 @@ public void registerCredential( 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() @@ -106,17 +125,44 @@ public Optional getUsernameForUserHandle(ByteArray userHandle) { @Override public Optional lookup(ByteArray credentialId, ByteArray userHandle) { - String username = getUsernameForUserHandle(userHandle).orElse(null); - if (username == null) { + 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(); } - throw new UnsupportedOperationException("Unimplemented method 'lookup'"); + 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) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'lookupAll'"); + 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/services/mfa/MfaService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/mfa/MfaService.java index 32ac3a5..df182bc 100644 --- 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 @@ -20,22 +20,36 @@ 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 @@ -46,22 +60,27 @@ public class MfaService { 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(); + 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 + 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()) @@ -144,7 +163,6 @@ public boolean finishRegistration( return false; } try { - RegistrationResult result = relyingParty.finishRegistration( FinishRegistrationOptions.builder() .request(request) @@ -159,8 +177,39 @@ public boolean finishRegistration( } } - private Optional findExistingUser(String username) { - return Optional.empty(); + 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/user/AdminUserService.java b/plume-admin-ws/src/main/java/com/coreoz/plume/admin/services/user/AdminUserService.java index 8063cf6..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 @@ -10,11 +10,14 @@ 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; @@ -73,6 +76,13 @@ public Optional authenticateWithAuthenticator(String userName )); } + 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( 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 40e2445..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 @@ -6,6 +6,8 @@ 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; @@ -13,6 +15,7 @@ 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; @@ -31,9 +34,15 @@ 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; @@ -45,6 +54,7 @@ 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; @@ -130,7 +140,7 @@ public Response authenticate(AdminCredentials credentials) { @POST @Operation(description = "Generate a qrcode for MFA enrollment") - @Path("/qrcode-url") + @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); @@ -150,7 +160,7 @@ public AdminMfaQrcode qrCodeUrl(AdminCredentials credentials) { @POST @Operation(description = "Generate a qrcode for MFA enrollment") - @Path("/qrcode") + @Path("/auhenticator/qrcode") public Response qrCode(AdminCredentials credentials) { // First user needs to be authenticated (an exception will be raised otherwise) AuthenticatedUser authenticatedUser = authenticateUser(credentials); @@ -172,8 +182,8 @@ public Response qrCode(AdminCredentials credentials) { } @POST - @Path("/verify-mfa") - @Operation(description = "Verify MFA code") + @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); @@ -189,8 +199,8 @@ public Response verifyMfa(AdminAuthenticatorCredentials credentials) { // ------------------------ Browser Authenticator ------------------------ @POST - @Operation(description = "Get the list of MFA credentials for the user") - @Path("/start-registration") + @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); @@ -207,7 +217,7 @@ public String getWebAuthentCreationOptions(AdminCredentials credentials) { @POST @Operation(description = "Register public key of a new MFA credential") - @Path("/register-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()); @@ -228,6 +238,42 @@ public Response registerCredential(AdminPublicKeyCredentials credentials) { } } + @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)