commit 7f1d0c909e29c0d0a0880dffce7a572f13726b9c
parent 2e848710da3032ea739cd91a4c78b2936e5a031c
Author: Antoine A <>
Date: Wed, 3 Jan 2024 00:32:08 +0000
2fa for bank transactions
Diffstat:
9 files changed, 82 insertions(+), 46 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -374,6 +374,43 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) {
}
}
+suspend fun ApplicationCall.bankTransactionHttp(db: Database, ctx: BankConfig, req: TransactionCreateRequest, is2fa: Boolean) {
+ val subject = req.payto_uri.message ?: throw badRequest("Wire transfer lacks subject")
+ val amount = req.payto_uri.amount ?: req.amount ?: throw badRequest("Wire transfer lacks amount")
+ ctx.checkRegionalCurrency(amount)
+ val res = db.transaction.create(
+ creditAccountPayto = req.payto_uri,
+ debitAccountUsername = username,
+ subject = subject,
+ amount = amount,
+ timestamp = Instant.now(),
+ is2fa = is2fa
+ )
+ when (res) {
+ BankTransactionResult.UnknownDebtor -> throw unknownAccount(username)
+ BankTransactionResult.TanRequired -> {
+ respondChallenge(db, Operation.bank_transaction, req)
+ }
+ BankTransactionResult.BothPartySame -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.BANK_SAME_ACCOUNT
+ )
+ BankTransactionResult.UnknownCreditor -> throw conflict(
+ "Creditor account was not found",
+ TalerErrorCode.BANK_UNKNOWN_CREDITOR
+ )
+ BankTransactionResult.AdminCreditor -> throw conflict(
+ "Cannot transfer money to admin account",
+ TalerErrorCode.BANK_ADMIN_CREDITOR
+ )
+ BankTransactionResult.BalanceInsufficient -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.BANK_UNALLOWED_DEBIT
+ )
+ is BankTransactionResult.Success -> respond(TransactionCreateResponse(res.id))
+ }
+}
+
private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
auth(db, TokenScope.readonly) {
get("/accounts/{USERNAME}/transactions") {
@@ -399,38 +436,8 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
}
auth(db, TokenScope.readwrite) {
post("/accounts/{USERNAME}/transactions") {
- val tx = call.receive<TransactionCreateRequest>()
- val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject")
- val amount =
- tx.payto_uri.amount ?: tx.amount ?: throw badRequest("Wire transfer lacks amount")
- ctx.checkRegionalCurrency(amount)
- val res = db.transaction.create(
- creditAccountPayto = tx.payto_uri,
- debitAccountUsername = username,
- subject = subject,
- amount = amount,
- timestamp = Instant.now(),
- )
- when (res) {
- is BankTransactionResult.UnknownDebtor -> throw unknownAccount(username)
- is BankTransactionResult.BothPartySame -> throw conflict(
- "Wire transfer attempted with credit and debit party being the same bank account",
- TalerErrorCode.BANK_SAME_ACCOUNT
- )
- is BankTransactionResult.UnknownCreditor -> throw conflict(
- "Creditor account was not found",
- TalerErrorCode.BANK_UNKNOWN_CREDITOR
- )
- is BankTransactionResult.AdminCreditor -> throw conflict(
- "Cannot transfer money to admin account",
- TalerErrorCode.BANK_ADMIN_CREDITOR
- )
- is BankTransactionResult.BalanceInsufficient -> throw conflict(
- "Insufficient funds",
- TalerErrorCode.BANK_UNALLOWED_DEBIT
- )
- is BankTransactionResult.Success -> call.respond(TransactionCreateResponse(res.id))
- }
+ val req = call.receive<TransactionCreateRequest>()
+ call.bankTransactionHttp(db, ctx, req, false)
}
}
}
@@ -800,6 +807,10 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) {
Operation.account_delete -> {
call.deleteAccountHttp(db, ctx, true)
}
+ Operation.bank_transaction -> {
+ val req = Json.decodeFromString<TransactionCreateRequest>(res.body)
+ call.bankTransactionHttp(db, ctx, req, true)
+ }
}
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -74,7 +74,8 @@ enum class Timeframe {
enum class Operation {
account_reconfig,
- account_delete
+ account_delete,
+ bank_transaction
}
@Serializable(with = Option.Serializer::class)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -144,7 +144,7 @@ class AccountDAO(private val db: Database) {
if (bonus.value != 0L || bonus.frac != 0) {
conn.prepareStatement("""
SELECT out_balance_insufficient
- FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?)
+ FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,true)
""").run {
setString(1, internalPaytoUri.canonical)
setLong(2, bonus.value)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -37,6 +37,7 @@ class TransactionDAO(private val db: Database) {
object UnknownDebtor: BankTransactionResult()
object BothPartySame: BankTransactionResult()
object BalanceInsufficient: BankTransactionResult()
+ object TanRequired: BankTransactionResult()
}
/** Create a new transaction */
@@ -46,6 +47,7 @@ class TransactionDAO(private val db: Database) {
subject: String,
amount: TalerAmount,
timestamp: Instant,
+ is2fa: Boolean
): BankTransactionResult = db.serializable { conn ->
val now = timestamp.toDbMicros() ?: throw faultyTimestampByBank();
conn.transaction {
@@ -55,6 +57,7 @@ class TransactionDAO(private val db: Database) {
,out_debtor_not_found
,out_same_account
,out_balance_insufficient
+ ,out_tan_required
,out_credit_bank_account_id
,out_debit_bank_account_id
,out_credit_row_id
@@ -62,7 +65,7 @@ class TransactionDAO(private val db: Database) {
,out_creditor_is_exchange
,out_debtor_is_exchange
,out_creditor_admin
- FROM bank_transaction(?,?,?,(?,?)::taler_amount,?)
+ FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?)
"""
)
stmt.setString(1, creditAccountPayto.canonical)
@@ -71,6 +74,7 @@ class TransactionDAO(private val db: Database) {
stmt.setLong(4, amount.value)
stmt.setInt(5, amount.frac)
stmt.setLong(6, now)
+ stmt.setBoolean(7, is2fa)
stmt.executeQuery().use {
when {
!it.next() -> throw internalServerError("Bank transaction didn't properly return")
@@ -79,6 +83,7 @@ class TransactionDAO(private val db: Database) {
it.getBoolean("out_same_account") -> BankTransactionResult.BothPartySame
it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BalanceInsufficient
it.getBoolean("out_creditor_admin") -> BankTransactionResult.AdminCreditor
+ it.getBoolean("out_tan_required") -> BankTransactionResult.TanRequired
else -> {
val creditAccountId = it.getLong("out_credit_bank_account_id")
val creditRowId = it.getLong("out_credit_row_id")
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
@@ -55,6 +55,7 @@ class AmountTest {
subject = "test",
amount = due,
timestamp = Instant.now(),
+ is2fa = false
)
val txBool = when (txRes) {
BankTransactionResult.BalanceInsufficient -> false
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -792,8 +792,7 @@ class CoreBankTransactionsApiTest {
repeat(2) {
tx("merchant", "KUDOS:3", "customer")
}
- client.post("/accounts/merchant/transactions") {
- pwAuth("merchant")
+ client.postA("/accounts/merchant/transactions") {
json {
"payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:5"
}
@@ -811,9 +810,9 @@ class CoreBankTransactionsApiTest {
assertBalance("exchange", "+KUDOS:0")
tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction
tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction
- val reserve_pub = randEddsaPublicKey();
- tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Accept incoming
- tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Bounce reserve_pub reuse
+ val reservePub = randEddsaPublicKey();
+ tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Accept incoming
+ tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Bounce reserve_pub reuse
assertBalance("merchant", "-KUDOS:1")
assertBalance("exchange", "+KUDOS:1")
@@ -828,6 +827,22 @@ class CoreBankTransactionsApiTest {
tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Warn wtid reuse
assertBalance("merchant", "+KUDOS:3")
assertBalance("exchange", "-KUDOS:3")
+
+ // Check 2fa
+ fillTanInfo("merchant")
+ assertBalance("merchant", "+KUDOS:3")
+ assertBalance("customer", "+KUDOS:0")
+ client.postA("/accounts/merchant/transactions") {
+ json {
+ "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1"
+ }
+ }.assertChallenge { _,_->
+ assertBalance("merchant", "+KUDOS:3")
+ assertBalance("customer", "+KUDOS:0")
+ }.assertOkJson <TransactionCreateResponse> {
+ assertBalance("merchant", "+KUDOS:2")
+ assertBalance("customer", "+KUDOS:1")
+ }
}
}
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
@@ -187,7 +187,7 @@ suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String,
json {
"payto_uri" to "${paytos[to] ?: tmpPayTo}?message=${subject.encodeURLQueryComponent()}&amount=$amount"
}
- }.assertOkJson<TransactionCreateResponse>().row_id
+ }.maybeChallenge().assertOkJson<TransactionCreateResponse>().row_id
}
/** Perform a taler outgoing transaction of [amount] from exchange to merchant */
@@ -338,7 +338,7 @@ suspend fun HttpResponse.assertErr(code: TalerErrorCode): HttpResponse {
return this
}
-suspend fun HttpResponse.challenge(): HttpResponse {
+suspend fun HttpResponse.maybeChallenge(): HttpResponse {
return if (this.status == HttpStatusCode.Accepted) {
this.assertChallenge()
} else {
diff --git a/database-versioning/libeufin-bank-0002.sql b/database-versioning/libeufin-bank-0002.sql
@@ -25,7 +25,7 @@ ALTER TABLE customers
ADD tan_channel tan_enum NULL;
CREATE TYPE op_enum
- AS ENUM ('account_reconfig', 'account_delete');
+ AS ENUM ('account_reconfig', 'account_delete', 'bank_transaction');
CREATE TABLE tan_challenges
(challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -442,12 +442,14 @@ CREATE FUNCTION bank_transaction(
IN in_subject TEXT,
IN in_amount taler_amount,
IN in_timestamp BIGINT,
+ IN in_is_tan BOOLEAN,
-- Error status
OUT out_creditor_not_found BOOLEAN,
OUT out_debtor_not_found BOOLEAN,
OUT out_same_account BOOLEAN,
OUT out_balance_insufficient BOOLEAN,
OUT out_creditor_admin BOOLEAN,
+ OUT out_tan_required BOOLEAN,
-- Success return
OUT out_credit_bank_account_id BIGINT,
OUT out_debit_bank_account_id BIGINT,
@@ -471,17 +473,18 @@ ELSIF out_creditor_admin THEN
RETURN;
END IF;
-- Find debit bank account id and check it's a different account
-SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id
- INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account
+SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND tan_channel IS NOT NULL
+ INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required
FROM bank_accounts
JOIN customers ON customer_id=owning_customer_id
WHERE login = in_debit_account_username;
IF NOT FOUND THEN
out_debtor_not_found=TRUE;
RETURN;
-ELSIF out_same_account THEN
+ELSIF out_same_account OR out_tan_required THEN
RETURN;
END IF;
+-- TODO check balance insufficient ?
-- Perform bank transfer
SELECT
transfer.out_balance_insufficient,