diff options
author | Antoine A <> | 2024-01-03 12:22:00 +0000 |
---|---|---|
committer | Antoine A <> | 2024-01-03 12:22:00 +0000 |
commit | 9346766e1ad82b90f1a498ff526ee2908691e41f (patch) | |
tree | 85a420f80d21b2773b6bb9ddfabaf01d1f49ef3c | |
parent | 846830610dc444377ded8b8e8c9f43d2c82adba6 (diff) | |
download | libeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.tar.gz libeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.tar.bz2 libeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.zip |
2fa for account auth reconfig
-rw-r--r-- | API_CHANGES.md | 2 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 53 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 5 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 1 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 23 | ||||
-rw-r--r-- | bank/src/test/kotlin/CoreBankApiTest.kt | 15 | ||||
-rw-r--r-- | database-versioning/libeufin-bank-0002.sql | 2 |
7 files changed, 69 insertions, 32 deletions
diff --git a/API_CHANGES.md b/API_CHANGES.md index 436e9373..3379c505 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -28,6 +28,8 @@ This files contains all the API changes for the current release: - POST /accounts/USERNAME/cashouts/CASHOUT_ID: remove confirmation_time, tan_channel, tan_info and status fields - POST /accounts/$USERNAME/cashouts: remove status field - POST /cashouts: remove status field +- PATCH /accounts/USERNAME: add tan_channel +- GET /accounts/USERNAME: add tan_channel ## bank cli diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt index 6688aabb..d8f00ca7 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -267,6 +267,24 @@ suspend fun ApplicationCall.patchAccountHttp( } } +suspend fun ApplicationCall.reconfigAuthHttp(db: Database, ctx: BankConfig, req:AccountPasswordChange, is2fa: Boolean) { + if (!isAdmin && req.old_password == null) { + throw conflict( + "non-admin user cannot change password without providing old password", + TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD + ) + } + when (db.account.reconfigPassword(username, req.new_password, req.old_password, is2fa || isAdmin)) { + AccountPatchAuthResult.Success -> respond(HttpStatusCode.NoContent) + AccountPatchAuthResult.TanRequired -> respondChallenge(db, Operation.account_auth_reconfig, req) + AccountPatchAuthResult.UnknownAccount -> throw unknownAccount(username) + AccountPatchAuthResult.OldPasswordMismatch -> throw conflict( + "old password does not match", + TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD + ) + } +} + suspend fun ApplicationCall.deleteAccountHttp(db: Database, ctx: BankConfig, is2fa: Boolean) { // Not deleting reserved names. if (RESERVED_ACCOUNTS.contains(username)) @@ -330,20 +348,7 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { } patch("/accounts/{USERNAME}/auth") { val req = call.receive<AccountPasswordChange>() - if (!isAdmin && req.old_password == null) { - throw conflict( - "non-admin user cannot change password without providing old password", - TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD - ) - } - when (db.account.reconfigPassword(username, req.new_password, req.old_password)) { - AccountPatchAuthResult.Success -> call.respond(HttpStatusCode.NoContent) - AccountPatchAuthResult.UnknownAccount -> throw unknownAccount(username) - AccountPatchAuthResult.OldPasswordMismatch -> throw conflict( - "old password does not match", - TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD - ) - } + call.reconfigAuthHttp(db, ctx, req, false) } } get("/public-accounts") { @@ -705,23 +710,27 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) { ) is TanSolveResult.Success -> when (res.op) { Operation.account_reconfig -> { - val req = Json.decodeFromString<AccountReconfiguration>(res.body); - call.patchAccountHttp(db, ctx, req, true, res.channel, res.info) + val tmp = Json.decodeFromString<AccountReconfiguration>(res.body); + call.patchAccountHttp(db, ctx, tmp, true, res.channel, res.info) + } + Operation.account_auth_reconfig -> { + val tmp = Json.decodeFromString<AccountPasswordChange>(res.body) + call.reconfigAuthHttp(db, ctx, tmp, true) } Operation.account_delete -> { call.deleteAccountHttp(db, ctx, true) } Operation.bank_transaction -> { - val req = Json.decodeFromString<TransactionCreateRequest>(res.body) - call.bankTransactionHttp(db, ctx, req, true) + val tmp = Json.decodeFromString<TransactionCreateRequest>(res.body) + call.bankTransactionHttp(db, ctx, tmp, true) } Operation.cashout -> { - val req = Json.decodeFromString<CashoutRequest>(res.body) - call.cashoutHttp(db, ctx, req, true) + val tmp = Json.decodeFromString<CashoutRequest>(res.body) + call.cashoutHttp(db, ctx, tmp, true) } Operation.withdrawal -> { - val req = Json.decodeFromString<StoredUUID>(res.body) - call.confirmWithdrawalHttp(db, ctx, req.value, true) + val tmp = Json.decodeFromString<StoredUUID>(res.body) + call.confirmWithdrawalHttp(db, ctx, tmp.value, true) } } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt index bfe30d96..735fd043 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -332,11 +332,12 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { val dbCfg = cfg.loadDbConfig() val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) runBlocking { - val res = db.account.reconfigPassword(username, password, null) + val res = db.account.reconfigPassword(username, password, null, true) when (res) { AccountPatchAuthResult.UnknownAccount -> throw Exception("Password change for '$username' account failed: unknown account") - AccountPatchAuthResult.OldPasswordMismatch -> { /* Can never happen */ } + AccountPatchAuthResult.OldPasswordMismatch, + AccountPatchAuthResult.TanRequired -> { /* Can never happen */ } AccountPatchAuthResult.Success -> logger.info("Password change for '$username' account succeeded") } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt index e7f16765..7a35755c 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -75,6 +75,7 @@ enum class Timeframe { enum class Operation { account_reconfig, account_delete, + account_auth_reconfig, bank_transaction, cashout, withdrawal diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt index 63ebf71c..9d93d3ec 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -357,20 +357,29 @@ class AccountDAO(private val db: Database) { enum class AccountPatchAuthResult { UnknownAccount, OldPasswordMismatch, + TanRequired, Success } /** Change account [login] password to [newPw] if current match [oldPw] */ - suspend fun reconfigPassword(login: String, newPw: String, oldPw: String?): AccountPatchAuthResult = db.serializable { + suspend fun reconfigPassword( + login: String, + newPw: String, + oldPw: String?, + is2fa: Boolean + ): AccountPatchAuthResult = db.serializable { it.transaction { conn -> - val currentPwh = conn.prepareStatement(""" - SELECT password_hash FROM customers WHERE login=? + val (currentPwh, tanRequired) = conn.prepareStatement(""" + SELECT password_hash, (NOT ? AND tan_channel IS NOT NULL) FROM customers WHERE login=? """).run { - setString(1, login) - oneOrNull { it.getString(1) } + setBoolean(1, is2fa) + setString(2, login) + oneOrNull { + Pair(it.getString(1), it.getBoolean(2)) + } ?: return@transaction AccountPatchAuthResult.UnknownAccount } - if (currentPwh == null) { - AccountPatchAuthResult.UnknownAccount + if (tanRequired) { + AccountPatchAuthResult.TanRequired } else if (oldPw != null && !CryptoUtil.checkpw(oldPw, currentPwh)) { AccountPatchAuthResult.OldPasswordMismatch } else { diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt index f6c65c7c..ad6758a3 100644 --- a/bank/src/test/kotlin/CoreBankApiTest.kt +++ b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -562,6 +562,21 @@ class CoreBankAccountsApiTest { client.patch("/accounts/customer/auth") { pwAuth("admin") json { + "new_password" to "customer-password" + } + }.assertNoContent() + + // Check 2FA + fillTanInfo("customer") + client.patchA("/accounts/customer/auth") { + json { + "old_password" to "customer-password" + "new_password" to "it-password" + } + }.assertChallenge().assertNoContent() + client.patch("/accounts/customer/auth") { + pwAuth("admin") + json { "new_password" to "new-password" } }.assertNoContent() diff --git a/database-versioning/libeufin-bank-0002.sql b/database-versioning/libeufin-bank-0002.sql index da3e9b7d..3c7f270c 100644 --- a/database-versioning/libeufin-bank-0002.sql +++ b/database-versioning/libeufin-bank-0002.sql @@ -32,7 +32,7 @@ ALTER TABLE customers ADD tan_channel tan_enum NULL; CREATE TYPE op_enum - AS ENUM ('account_reconfig', 'account_delete', 'bank_transaction', 'cashout', 'withdrawal'); + AS ENUM ('account_reconfig', 'account_auth_reconfig', 'account_delete', 'bank_transaction', 'cashout', 'withdrawal'); CREATE TABLE tan_challenges (challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE |