summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-01-03 12:22:00 +0000
committerAntoine A <>2024-01-03 12:22:00 +0000
commit9346766e1ad82b90f1a498ff526ee2908691e41f (patch)
tree85a420f80d21b2773b6bb9ddfabaf01d1f49ef3c
parent846830610dc444377ded8b8e8c9f43d2c82adba6 (diff)
downloadlibeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.tar.gz
libeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.tar.bz2
libeufin-9346766e1ad82b90f1a498ff526ee2908691e41f.zip
2fa for account auth reconfig
-rw-r--r--API_CHANGES.md2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt53
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt1
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt23
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt15
-rw-r--r--database-versioning/libeufin-bank-0002.sql2
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