libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 9346766e1ad82b90f1a498ff526ee2908691e41f
parent 846830610dc444377ded8b8e8c9f43d2c82adba6
Author: Antoine A <>
Date:   Wed,  3 Jan 2024 12:22:00 +0000

2fa for account auth reconfig

Diffstat:
MAPI_CHANGES.md | 2++
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 53+++++++++++++++++++++++++++++++----------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 5+++--
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 1+
Mbank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 23++++++++++++++++-------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 15+++++++++++++++
Mdatabase-versioning/libeufin-bank-0002.sql | 2+-
7 files changed, 69 insertions(+), 32 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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