gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Implementing token authentication.


From: gnunet
Subject: [libeufin] branch master updated: Implementing token authentication.
Date: Sun, 17 Sep 2023 10:09:21 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 365df3c3 Implementing token authentication.
365df3c3 is described below

commit 365df3c31e524a542eab8551d3384fbd32654ec9
Author: MS <ms@taler.net>
AuthorDate: Sun Sep 17 10:08:48 2023 +0200

    Implementing token authentication.
---
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 152 +---------
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    | 137 +++++----
 .../main/kotlin/tech/libeufin/bank/bankTypes.kt    | 329 +++++++++++++++++++++
 .../tech/libeufin/bank/{Helpers.kt => helpers.kt}  |  79 ++++-
 bank/src/test/kotlin/DatabaseTest.kt               |   8 +-
 bank/src/test/kotlin/JsonTest.kt                   |   2 +-
 bank/src/test/kotlin/LibeuFinApiTest.kt            |  57 +++-
 database-versioning/libeufin-bank-0001.sql         |   1 +
 util/src/main/kotlin/LibeufinErrorCodes.kt         |  77 -----
 util/src/main/kotlin/TalerErrorCode.kt             |   2 -
 util/src/main/kotlin/time.kt                       |   7 +-
 11 files changed, 556 insertions(+), 295 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 0bfd88ad..73194c36 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -28,143 +28,9 @@ import java.util.*
 
 private const val DB_CTR_LIMIT = 1000000
 
-data class Customer(
-    val login: String,
-    val passwordHash: String,
-    val name: String,
-    val dbRowId: Long? = null, // mostly used when retrieving records.
-    val email: String? = null,
-    val phone: String? = null,
-    val cashoutPayto: String? = null,
-    val cashoutCurrency: String? = null
-)
-fun Customer.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Cutsomer '$login' had no DB row ID")
-
-/**
- * Represents a Taler amount.  This type can be used both
- * to hold database records and amounts coming from the parser.
- * If maybeCurrency is null, then the constructor defaults it
- * to be the "internal currency".  Internal currency is the one
- * with which Libeufin-Bank moves funds within itself, therefore
- * not to be mistaken with the cashout currency, which is the one
- * that gets credited to Libeufin-Bank users to their cashout_payto_uri.
- *
- * maybeCurrency is typically null when the TalerAmount object gets
- * defined by the Database class.
- */
-class TalerAmount(
-    val value: Long,
-    val frac: Int,
-    maybeCurrency: String? = null
-) {
-    val currency: String = if (maybeCurrency == null) {
-        val internalCurrency = db.configGet("internal_currency")
-            ?: throw internalServerError("internal_currency not found in the 
config")
-        internalCurrency
-    } else maybeCurrency
-
-    override fun equals(other: Any?): Boolean {
-        return other is TalerAmount &&
-                other.value == this.value &&
-                other.frac == this.frac &&
-                other.currency == this.currency
-    }
-}
 
-// BIC got removed, because it'll be expressed in the internal_payto_uri.
-data class BankAccount(
-    val internalPaytoUri: String,
-    val owningCustomerId: Long,
-    val isPublic: Boolean = false,
-    val isTalerExchange: Boolean = false,
-    val lastNexusFetchRowId: Long = 0L,
-    val balance: TalerAmount? = null,
-    val hasDebt: Boolean,
-    val maxDebt: TalerAmount
-)
-
-enum class TransactionDirection {
-    credit, debit
-}
-
-enum class TanChannel {
-    sms, email, file
-}
-
-enum class TokenScope {
-    readonly, readwrite
-}
-
-data class BearerToken(
-    val content: ByteArray,
-    val scope: TokenScope,
-    val creationTime: Long,
-    val expirationTime: Long,
-    /**
-     * Serial ID of the database row that hosts the bank customer
-     * that is associated with this token.  NOTE: if the token is
-     * refreshed by a client that doesn't have a user+password login
-     * in the system, the creator remains always the original bank
-     * customer that created the very first token.
-     */
-    val bankCustomer: Long
-)
-
-data class BankInternalTransaction(
-    val creditorAccountId: Long,
-    val debtorAccountId: Long,
-    val subject: String,
-    val amount: TalerAmount,
-    val transactionDate: Long,
-    val accountServicerReference: String,
-    val endToEndId: String,
-    val paymentInformationId: String
-)
-
-data class BankAccountTransaction(
-    val creditorPaytoUri: String,
-    val creditorName: String,
-    val debtorPaytoUri: String,
-    val debtorName: String,
-    val subject: String,
-    val amount: TalerAmount,
-    val transactionDate: Long, // microseconds
-    val accountServicerReference: String,
-    val paymentInformationId: String,
-    val endToEndId: String,
-    val direction: TransactionDirection,
-    val bankAccountId: Long,
-)
-
-data class TalerWithdrawalOperation(
-    val withdrawalUuid: UUID,
-    val amount: TalerAmount,
-    val selectionDone: Boolean = false,
-    val aborted: Boolean = false,
-    val confirmationDone: Boolean = false,
-    val reservePub: ByteArray?,
-    val selectedExchangePayto: String?,
-    val walletBankAccount: Long
-)
+fun Customer.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Cutsomer '$login' had no DB row ID")
 
-data class Cashout(
-    val cashoutUuid: UUID,
-    val localTransaction: Long? = null,
-    val amountDebit: TalerAmount,
-    val amountCredit: TalerAmount,
-    val buyAtRatio: Int,
-    val buyInFee: TalerAmount,
-    val sellAtRatio: Int,
-    val sellOutFee: TalerAmount,
-    val subject: String,
-    val creationTime: Long,
-    val tanConfirmationTime: Long? = null,
-    val tanChannel: TanChannel,
-    val tanCode: String,
-    val bankAccount: Long,
-    val credit_payto_uri: String,
-    val cashoutCurrency: String
-)
 
 class Database(private val dbConfig: String) {
     private var dbConn: PgConnection? = null
@@ -306,7 +172,8 @@ class Database(private val dbConfig: String) {
                 phone = it.getString("phone"),
                 email = it.getString("email"),
                 cashoutCurrency = it.getString("cashout_currency"),
-                cashoutPayto = it.getString("cashout_payto")
+                cashoutPayto = it.getString("cashout_payto"),
+                dbRowId = customer_id
             )
         }
     }
@@ -351,16 +218,17 @@ class Database(private val dbConfig: String) {
                 creation_time,
                 expiration_time,
                 scope,
-                bank_customer              
+                bank_customer,
+                is_refreshable
                ) VALUES
-               (?, ?, ?, ?::token_scope_enum, ?)
+               (?, ?, ?, ?::token_scope_enum, ?, ?)
         """)
         stmt.setBytes(1, token.content)
         stmt.setLong(2, token.creationTime)
         stmt.setLong(3, token.expirationTime)
         stmt.setString(4, token.scope.name)
         stmt.setLong(5, token.bankCustomer)
-
+        stmt.setBoolean(6, token.isRefreshable)
         return myExecute(stmt)
     }
     fun bearerTokenGet(token: ByteArray): BearerToken? {
@@ -370,7 +238,8 @@ class Database(private val dbConfig: String) {
               expiration_time,
               creation_time,
               bank_customer,
-              scope
+              scope,
+              is_refreshable
             FROM bearer_tokens
             WHERE content=?;            
         """)
@@ -387,7 +256,8 @@ class Database(private val dbConfig: String) {
                     if (this == TokenScope.readwrite.name) return@run 
TokenScope.readwrite
                     if (this == TokenScope.readonly.name) return@run 
TokenScope.readonly
                     else throw internalServerError("Wrong token scope found in 
the database: $this")
-                }
+                },
+                isRefreshable = it.getBoolean("is_refreshable")
             )
         }
     }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 12dc927a..2e30038d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -36,57 +36,23 @@ import io.ktor.server.routing.*
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.encoding.decodeStructure
 import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.SerializersModule
 import net.taler.common.errorcodes.TalerErrorCode
+import net.taler.wallet.crypto.Base32Crockford
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.event.Level
 import tech.libeufin.util.*
+import java.time.Duration
+import kotlin.random.Random
 
 // GLOBALS
 val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank")
 val db = Database(System.getProperty("BANK_DB_CONNECTION_STRING"))
 const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
+val TOKEN_DEFAULT_DURATION_US = Duration.ofDays(1L).seconds * 1000000
 
-// TYPES
-
-// FIXME: double-check the enum numeric value.
-enum class FracDigits(howMany: Int) {
-    TWO(2),
-    EIGHT(8)
-}
-
-@Serializable
-data class TalerError(
-    val code: Int,
-    val hint: String? = null
-)
-
-@Serializable
-data class ChallengeContactData(
-    val email: String? = null,
-    val phone: String? = null
-)
-@Serializable
-data class RegisterAccountRequest(
-    val username: String,
-    val password: String,
-    val name: String,
-    val is_public: Boolean = false,
-    val is_taler_exchange: Boolean = false,
-    val challenge_contact_data: ChallengeContactData? = null,
-    val cashout_payto_uri: String? = null,
-    val internal_payto_uri: String? = null
-)
-
-/**
- * This is the _internal_ representation of a RelativeTime
- * JSON type.
- */
-data class RelativeTime(
-    val d_us: Long
-)
 
 /**
  * This custom (de)serializer interprets the RelativeTime JSON
@@ -122,17 +88,7 @@ object RelativeTimeSerializer : KSerializer<RelativeTime> {
             element<JsonElement>("d_us")
         }
 }
-@Serializable
-data class TokenRequest(
-    val scope: TokenScope,
-    @Contextual
-    val duration: RelativeTime
-)
 
-class LibeufinBankException(
-    val httpStatus: HttpStatusCode,
-    val talerError: TalerError
-) : Exception(talerError.hint)
 
 /**
  * This function tries to authenticate the call according
@@ -187,7 +143,12 @@ val webApp: Application.() -> Unit = {
     install(ContentNegotiation) {
         json(Json {
             ignoreUnknownKeys = true
-            isLenient = false
+            // Registering custom parser for RelativeTime
+            serializersModule = SerializersModule {
+                contextual(RelativeTime::class) {
+                    RelativeTimeSerializer
+                }
+            }
         })
     }
     install(RequestValidation)
@@ -246,12 +207,78 @@ val webApp: Application.() -> Unit = {
         }
     }
     routing {
-        post("/accounts/{USERNAME}/auth-token") {
-            val customer = call.myAuth(TokenScope.readwrite)
+        post("/accounts/{USERNAME}/token") {
+            val customer = call.myAuth(TokenScope.refreshable) ?: throw 
unauthorized("Authentication failed")
             val endpointOwner = call.expectUriComponent("USERNAME")
-            if (customer == null || customer.login != endpointOwner)
-                throw unauthorized("Auth failed or client has no rights")
-
+            if (customer.login != endpointOwner)
+                throw forbidden(
+                    "User has no rights on this enpoint",
+                    TalerErrorCode.TALER_EC_END // FIXME: need generic 
forbidden
+                )
+            val maybeAuthToken = call.getAuthToken()
+            val req = call.receive<TokenRequest>()
+            /**
+             * This block checks permissions ONLY IF the call was authenticated
+             * with a token.  Basic auth gets always granted.
+             */
+            if (maybeAuthToken != null) {
+                val tokenBytes = Base32Crockford.decode(maybeAuthToken)
+                val refreshingToken = db.bearerTokenGet(tokenBytes) ?: throw 
internalServerError(
+                    "Token used to auth not found in the database!"
+                )
+                if (refreshingToken.scope == TokenScope.readonly && req.scope 
== TokenScope.readwrite)
+                    throw forbidden(
+                        "Cannot generate RW token from RO",
+                        
TalerErrorCode.TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT
+                    )
+            }
+            val tokenBytes = ByteArray(32).apply {
+                java.util.Random().nextBytes(this)
+            }
+            val maxDurationTime: Long = db.configGet("token_max_duration").run 
{
+                if (this == null)
+                    return@run Long.MAX_VALUE
+                return@run try {
+                    this.toLong()
+                } catch (e: Exception) {
+                    logger.error("Could not convert config's 
token_max_duration to Long")
+                    throw internalServerError(e.message)
+                }
+            }
+            if (req.duration != null && 
req.duration.d_us.compareTo(maxDurationTime) == 1)
+                throw forbidden(
+                    "Token duration bigger than bank's limit",
+                    // FIXME: define new EC for this case.
+                    TalerErrorCode.TALER_EC_END
+                )
+            val tokenDurationUs  = req.duration?.d_us ?: 
TOKEN_DEFAULT_DURATION_US
+            val customerDbRow = customer.dbRowId ?: throw internalServerError(
+                "Coud not resort customer '${customer.login}' database row ID"
+            )
+            val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
+            if (expirationTimestampUs < tokenDurationUs)
+                throw badRequest(
+                    "Token duration caused arithmetic overflow",
+                    // FIXME: need dedicate EC (?)
+                    talerErrorCode = TalerErrorCode.TALER_EC_END
+                )
+            val token = BearerToken(
+                bankCustomer = customerDbRow,
+                content = tokenBytes,
+                creationTime = expirationTimestampUs,
+                expirationTime = expirationTimestampUs,
+                scope = req.scope,
+                isRefreshable = req.refreshable
+            )
+            if (!db.bearerTokenCreate(token))
+                throw internalServerError("Failed at inserting new token in 
the database")
+            call.respond(TokenSuccessResponse(
+                access_token = Base32Crockford.encode(tokenBytes),
+                expiration = Timestamp(
+                    t_s = expirationTimestampUs / 1000000L
+                )
+            ))
+            return@post
         }
         post("/accounts") {
             // check if only admin.
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt
new file mode 100644
index 00000000..25e7edca
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt
@@ -0,0 +1,329 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING.  If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
+import java.util.*
+
+// Allowed lengths for fractional digits in amounts.
+enum class FracDigits(howMany: Int) {
+    TWO(2),
+    EIGHT(8)
+}
+
+
+// It contains the number of microseconds since the Epoch.
+@Serializable
+data class Timestamp(
+    val t_s: Long // FIXME (?): not supporting "never" at the moment.
+)
+
+/**
+ * HTTP response type of successful token refresh.
+ * access_token is the Crockford encoding of the 32 byte
+ * access token, whereas 'expiration' is the point in time
+ * when this token expires.
+ */
+@Serializable
+data class TokenSuccessResponse(
+    val access_token: String,
+    val expiration: Timestamp
+)
+
+/**
+ * Error object to respond to the client.  The
+ * 'code' field takes values from the GANA gnu-taler-error-code
+ * specification.  'hint' is a human-readable description
+ * of the error.
+ */
+@Serializable
+data class TalerError(
+    val code: Int,
+    val hint: String? = null
+)
+
+/* Contains contact data to send TAN challges to the
+* users, to let them complete cashout operations. */
+@Serializable
+data class ChallengeContactData(
+    val email: String? = null,
+    val phone: String? = null
+)
+
+// Type expected at POST /accounts
+@Serializable
+data class RegisterAccountRequest(
+    val username: String,
+    val password: String,
+    val name: String,
+    val is_public: Boolean = false,
+    val is_taler_exchange: Boolean = false,
+    val challenge_contact_data: ChallengeContactData? = null,
+    // External bank account where to send cashout amounts.
+    val cashout_payto_uri: String? = null,
+    // Bank account internal to Libeufin-Bank.
+    val internal_payto_uri: String? = null
+)
+
+/* Internal representation of relative times.  The
+* "forever" case is represented with Long.MAX_VALUE.
+*/
+data class RelativeTime(
+    val d_us: Long
+)
+
+/**
+ * Type expected at POST /accounts/{USERNAME}/token
+ * It complies with Taler's design document #49
+ */
+@Serializable
+data class TokenRequest(
+    val scope: TokenScope,
+    @Contextual
+    val duration: RelativeTime? = null,
+    val refreshable: Boolean = false
+)
+
+/**
+ * Convenience type to throw errors along the bank activity
+ * and that is meant to be caught by Ktor and responded to the
+ * client.
+ */
+class LibeufinBankException(
+    // Status code that Ktor will set for the response.
+    val httpStatus: HttpStatusCode,
+    // Error detail object, after Taler API.
+    val talerError: TalerError
+) : Exception(talerError.hint)
+
+/**
+ * Convenience type to hold customer data, typically after such
+ * data gets fetched from the database.  It is also used to _insert_
+ * customer data to the database.
+ */
+data class Customer(
+    val login: String,
+    val passwordHash: String,
+    val name: String,
+    /**
+     * Only non-null when this object is defined _by_ the
+     * database.
+     */
+    val dbRowId: Long? = null,
+    val email: String? = null,
+    val phone: String? = null,
+    /**
+     * External bank account where customers send
+     * their cashout amounts.
+     */
+    val cashoutPayto: String? = null,
+    /**
+     * Currency of the external bank account where
+     * customers send their cashout amounts.
+     */
+    val cashoutCurrency: String? = null
+)
+
+/**
+* Represents a Taler amount.  This type can be used both
+* to hold database records and amounts coming from the parser.
+* If maybeCurrency is null, then the constructor defaults it
+* to be the "internal currency".  Internal currency is the one
+* with which Libeufin-Bank moves funds within itself, therefore
+* not to be mistaken with the cashout currency, which is the one
+* that gets credited to Libeufin-Bank users to their cashout_payto_uri.
+*
+* maybeCurrency is typically null when the TalerAmount object gets
+* defined by the Database class.
+*/
+class TalerAmount(
+    val value: Long,
+    val frac: Int,
+    maybeCurrency: String? = null
+) {
+    val currency: String = if (maybeCurrency == null) {
+        val internalCurrency = db.configGet("internal_currency")
+            ?: throw internalServerError("internal_currency not found in the 
config")
+        internalCurrency
+    } else maybeCurrency
+
+    override fun equals(other: Any?): Boolean {
+        return other is TalerAmount &&
+                other.value == this.value &&
+                other.frac == this.frac &&
+                other.currency == this.currency
+    }
+}
+
+/**
+ * Convenience type to get and set bank account information
+ * from/to the database.
+ */
+data class BankAccount(
+    val internalPaytoUri: String,
+    // Database row ID of the customer that owns this bank account.
+    val owningCustomerId: Long,
+    val isPublic: Boolean = false,
+    val isTalerExchange: Boolean = false,
+    /**
+     * Because bank accounts MAY be funded by an external currency,
+     * local bank accounts need to query Nexus, in order to find this
+     * out.  This field is a pointer to the latest incoming payment that
+     * was contained in a Nexus history response.
+     *
+     * Typically, the 'admin' bank account uses this field, in order
+     * to initiate Taler withdrawals that depend on an external currency
+     * being wired by wallet owners.
+     */
+    val lastNexusFetchRowId: Long = 0L,
+    val balance: TalerAmount? = null,
+    val hasDebt: Boolean,
+    val maxDebt: TalerAmount
+)
+
+// Allowed values for bank transactions directions.
+enum class TransactionDirection {
+    credit,
+    debit
+}
+
+// Allowed values for cashout TAN channels.
+enum class TanChannel {
+    sms,
+    email,
+    file // Writes cashout TANs to /tmp, for testing.
+}
+
+// Scopes for authentication tokens.
+enum class TokenScope {
+    readonly,
+    readwrite,
+    refreshable // Not spec'd as a scope!
+}
+
+/**
+ * Convenience type to set/get authentication tokens to/from
+ * the database.
+ */
+data class BearerToken(
+    val content: ByteArray,
+    val scope: TokenScope,
+    val isRefreshable: Boolean = false,
+    val creationTime: Long,
+    val expirationTime: Long,
+    /**
+     * Serial ID of the database row that hosts the bank customer
+     * that is associated with this token.  NOTE: if the token is
+     * refreshed by a client that doesn't have a user+password login
+     * in the system, the creator remains always the original bank
+     * customer that created the very first token.
+     */
+    val bankCustomer: Long
+)
+
+/**
+ * Convenience type to _communicate_ a bank transfer to the
+ * database procedure, NOT representing therefore any particular
+ * table.  The procedure will then retrieve all the tables data
+ * from this type.
+ */
+data class BankInternalTransaction(
+    // Database row ID of the internal bank account sending the payment.
+    val creditorAccountId: Long,
+    // Database row ID of the internal bank account receiving the payment.
+    val debtorAccountId: Long,
+    val subject: String,
+    val amount: TalerAmount,
+    val transactionDate: Long,
+    val accountServicerReference: String, // ISO20022
+    val endToEndId: String, // ISO20022
+    val paymentInformationId: String // ISO20022
+)
+
+/**
+ * Convenience type representing bank transactions as they
+ * are in the respective database table.  Only used to _get_
+ * the information from the database.
+ */
+data class BankAccountTransaction(
+    val creditorPaytoUri: String,
+    val creditorName: String,
+    val debtorPaytoUri: String,
+    val debtorName: String,
+    val subject: String,
+    val amount: TalerAmount,
+    val transactionDate: Long, // microseconds
+    /**
+     * Is the transaction debit, or credit for the
+     * bank account pointed by this object?
+     */
+    val direction: TransactionDirection,
+    /**
+     * database row ID of the bank account that is
+     * impacted by the direction.  For example, if the
+     * direction is debit, then this value points to the
+     * bank account of the payer.
+     */
+    val bankAccountId: Long,
+    // Following are ISO20022 specific.
+    val accountServicerReference: String,
+    val paymentInformationId: String,
+    val endToEndId: String,
+)
+
+/**
+ * Represents a Taler withdrawal operation, as it is
+ * stored in the respective database table.
+ */
+data class TalerWithdrawalOperation(
+    val withdrawalUuid: UUID,
+    val amount: TalerAmount,
+    val selectionDone: Boolean = false,
+    val aborted: Boolean = false,
+    val confirmationDone: Boolean = false,
+    val reservePub: ByteArray?,
+    val selectedExchangePayto: String?,
+    val walletBankAccount: Long
+)
+
+/**
+ * Represents a cashout operation, as it is stored
+ * in the respective database table.
+ */
+data class Cashout(
+    val cashoutUuid: UUID,
+    val localTransaction: Long? = null,
+    val amountDebit: TalerAmount,
+    val amountCredit: TalerAmount,
+    val buyAtRatio: Int,
+    val buyInFee: TalerAmount,
+    val sellAtRatio: Int,
+    val sellOutFee: TalerAmount,
+    val subject: String,
+    val creationTime: Long,
+    val tanConfirmationTime: Long? = null,
+    val tanChannel: TanChannel,
+    val tanCode: String,
+    val bankAccount: Long,
+    val credit_payto_uri: String,
+    val cashoutCurrency: String
+)
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
similarity index 67%
rename from bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt
rename to bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 41f4d4c1..28c58fe5 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -20,11 +20,29 @@
 package tech.libeufin.bank
 
 import io.ktor.http.*
+import io.ktor.server.application.*
 import net.taler.common.errorcodes.TalerErrorCode
+import net.taler.wallet.crypto.Base32Crockford
 import tech.libeufin.util.*
 import java.lang.NumberFormatException
 
-// HELPERS.
+// Get the auth token (stripped of the bearer-token:-prefix)
+// IF the call was authenticated with it.
+fun ApplicationCall.getAuthToken(): String? {
+    val h = getAuthorizationRawHeader(this.request) ?: return null
+    val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
+        "Authorization header is malformed.",
+        TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+    )
+    if (authDetails.scheme == "Bearer")
+        return splitBearerToken(authDetails.content) ?: throw
+        throw badRequest(
+            "Authorization header is malformed (could not strip the prefix 
from Bearer token).",
+            TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+        )
+    return null // Not a Bearer token case.
+}
+
 
 /**
  * Performs the HTTP basic authentication.  Returns the
@@ -56,15 +74,53 @@ fun doBasicAuth(encodedCredentials: String): Customer? {
     return maybeCustomer
 }
 
+/**
+ * This function takes a prefixed Bearer token, removes the
+ * bearer-token:-prefix and returns it.  Returns null, if the
+ * input is invalid.
+ */
+private fun splitBearerToken(tok: String): String? {
+    val tokenSplit = tok.split(":", limit = 2)
+    if (tokenSplit.size != 2) return null
+    if (tokenSplit[0] != "bearer-token") return null
+    return tokenSplit[1]
+}
+
 /* Performs the bearer-token authentication.  Returns the
  * authenticated customer on success, null otherwise. */
 fun doTokenAuth(
     token: String,
-    requiredScope: TokenScope, // readonly or readwrite
+    requiredScope: TokenScope,
 ): Customer? {
-    val maybeToken: BearerToken = 
db.bearerTokenGet(token.toByteArray(Charsets.UTF_8)) ?: return null
-    val isExpired: Boolean = maybeToken.expirationTime - getNow().toMicro() < 0
-    if (isExpired || maybeToken.scope != requiredScope) return null // FIXME: 
mention the reason?
+    val bareToken = splitBearerToken(token) ?: throw badRequest(
+        "Bearer token malformed",
+        talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+    )
+    val tokenBytes = try {
+        Base32Crockford.decode(bareToken)
+    } catch (e: Exception) {
+        throw badRequest(
+            e.message,
+            TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+        )
+    }
+    val maybeToken: BearerToken? = db.bearerTokenGet(tokenBytes)
+    if (maybeToken == null) {
+        logger.error("Auth token not found")
+        return null
+    }
+    if (maybeToken.expirationTime - getNowUs() < 0) {
+        logger.error("Auth token is expired")
+        return null
+    }
+    if (maybeToken.scope == TokenScope.readonly && requiredScope == 
TokenScope.readwrite) {
+        logger.error("Auth token has insufficient scope")
+        return null
+    }
+    if (!maybeToken.isRefreshable && requiredScope == TokenScope.refreshable) {
+        logger.error("Could not refresh unrefreshable token")
+        return null
+    }
     // Getting the related username.
     return db.customerGetFromRowId(maybeToken.bankCustomer)
         ?: throw LibeufinBankException(
@@ -75,6 +131,15 @@ fun doTokenAuth(
             ))
 }
 
+fun forbidden(hint: String? = null, talerErrorCode: TalerErrorCode): 
LibeufinBankException =
+    LibeufinBankException(
+        httpStatus = HttpStatusCode.Forbidden,
+        talerError = TalerError(
+            code = talerErrorCode.code,
+            hint = hint
+        )
+    )
+
 fun unauthorized(hint: String? = null): LibeufinBankException =
     LibeufinBankException(
         httpStatus = HttpStatusCode.Unauthorized,
@@ -83,7 +148,7 @@ fun unauthorized(hint: String? = null): 
LibeufinBankException =
             hint = hint
         )
     )
-fun internalServerError(hint: String): LibeufinBankException =
+fun internalServerError(hint: String?): LibeufinBankException =
     LibeufinBankException(
         httpStatus = HttpStatusCode.InternalServerError,
         talerError = TalerError(
@@ -96,7 +161,7 @@ fun badRequest(
     talerErrorCode: TalerErrorCode = 
TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
 ): LibeufinBankException =
     LibeufinBankException(
-        httpStatus = HttpStatusCode.InternalServerError,
+        httpStatus = HttpStatusCode.BadRequest,
         talerError = TalerError(
             code = talerErrorCode.code,
             hint = hint
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index 6961a9b5..290d0d19 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -20,9 +20,7 @@
 
 import org.junit.Test
 import tech.libeufin.bank.*
-import tech.libeufin.util.execCommand
-import tech.libeufin.util.getNow
-import tech.libeufin.util.toMicro
+import tech.libeufin.util.getNowUs
 import java.util.Random
 import java.util.UUID
 
@@ -68,8 +66,8 @@ class DatabaseTest {
         val token = BearerToken(
             bankCustomer = 1L,
             content = tokenBytes,
-            creationTime = getNow().toMicro(), // make .toMicro()? implicit?
-            expirationTime = getNow().plusDays(1).toMicro(),
+            creationTime = getNowUs(), // make .toMicro()? implicit?
+            expirationTime = getNowUs(),
             scope = TokenScope.readonly
         )
         assert(db.bearerTokenGet(token.content) == null)
diff --git a/bank/src/test/kotlin/JsonTest.kt b/bank/src/test/kotlin/JsonTest.kt
index 384334a6..63eff6f9 100644
--- a/bank/src/test/kotlin/JsonTest.kt
+++ b/bank/src/test/kotlin/JsonTest.kt
@@ -44,6 +44,6 @@ class JsonTest {
               "duration": {"d_us": 30}
             }
         """.trimIndent())
-        assert(tokenReq.scope == TokenScope.readonly && tokenReq.duration.d_us 
== 30L)
+        assert(tokenReq.scope == TokenScope.readonly && 
tokenReq.duration?.d_us == 30L)
     }
 }
\ No newline at end of file
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 48ca69a6..4ee9a803 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -1,14 +1,67 @@
+import io.ktor.auth.*
 import io.ktor.client.plugins.*
 import io.ktor.client.request.*
+import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
-import kotlinx.serialization.json.Json
+import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.execCommand
+import tech.libeufin.util.getNowUs
+import java.time.Duration
+import kotlin.random.Random
 
 class LibeuFinApiTest {
+    private val customerFoo = Customer(
+    login = "foo",
+    passwordHash = CryptoUtil.hashpw("pw"),
+    name = "Foo",
+    phone = "+00",
+    email = "foo@b.ar",
+    cashoutPayto = "payto://external-IBAN",
+    cashoutCurrency = "KUDOS"
+    )
+    // Checking the POST /token handling.
+    @Test
+    fun tokenTest() {
+        val db = initDb()
+        assert(db.customerCreate(customerFoo) != null)
+        testApplication {
+            application(webApp)
+            client.post("/accounts/foo/token") {
+                expectSuccess = true
+                contentType(ContentType.Application.Json)
+                basicAuth("foo", "pw")
+                setBody("""
+                    {"scope": "readonly"}
+                """.trimIndent())
+            }
+            // foo tries on bar endpoint
+            val r = client.post("/accounts/bar/token") {
+                expectSuccess = false
+                basicAuth("foo", "pw")
+            }
+            assert(r.status == HttpStatusCode.Forbidden)
+            // Make ad-hoc token for foo.
+            val fooTok = ByteArray(32).apply { Random.nextBytes(this) }
+            assert(db.bearerTokenCreate(BearerToken(
+                content = fooTok,
+                bankCustomer = 1L, // only foo exists.
+                scope = TokenScope.readonly,
+                creationTime = getNowUs(),
+                isRefreshable = true,
+                expirationTime = getNowUs() + (Duration.ofHours(1).toMillis() 
* 1000)
+            )))
+            // Testing the bearer-token:-scheme.
+            client.post("/accounts/foo/token") {
+                headers.set("Authorization", "Bearer 
bearer-token:${Base32Crockford.encode(fooTok)}")
+                contentType(ContentType.Application.Json)
+                setBody("{\"scope\": \"readonly\"}")
+                expectSuccess = true
+            }
+        }
+    }
     /**
      * Testing the account creation, its idempotency and
      * the restriction to admin to create accounts.
diff --git a/database-versioning/libeufin-bank-0001.sql 
b/database-versioning/libeufin-bank-0001.sql
index 9daaa7b0..0037ab14 100644
--- a/database-versioning/libeufin-bank-0001.sql
+++ b/database-versioning/libeufin-bank-0001.sql
@@ -82,6 +82,7 @@ CREATE TABLE IF NOT EXISTS bearer_tokens
   ,creation_time INT8
   ,expiration_time INT8
   ,scope token_scope_enum
+  ,is_refreshable BOOLEAN
   ,bank_customer BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE 
CASCADE
 );
 
diff --git a/util/src/main/kotlin/LibeufinErrorCodes.kt 
b/util/src/main/kotlin/LibeufinErrorCodes.kt
deleted file mode 100644
index e60b4015..00000000
--- a/util/src/main/kotlin/LibeufinErrorCodes.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-     This file is part of GNU Taler
-     Copyright (C) 2012-2020 Taler Systems SA
-
-     GNU Taler is free software: you can redistribute it and/or modify it
-     under the terms of the GNU Lesser General Public License as published
-     by the Free Software Foundation, either version 3 of the License,
-     or (at your option) any later version.
-
-     GNU Taler is distributed in the hope that it will be useful, but
-     WITHOUT ANY WARRANTY; without even the implied warranty of
-     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-     Lesser General Public License for more details.
-
-     You should have received a copy of the GNU Lesser General Public License
-     along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-     SPDX-License-Identifier: LGPL3.0-or-later
-
-     Note: the LGPL does not apply to all components of GNU Taler,
-     but it does apply to this file.
- */
-
-package tech.libeufin.util
-
-enum class LibeufinErrorCode(val code: Int) {
-
-    /**
-     * The error case didn't have a dedicate code.
-     */
-    LIBEUFIN_EC_NONE(0),
-
-    /**
-     * A payment being processed is neither CRDT not DBIT.  This
-     * type of error should be detected _before_ storing the data
-     * into the database.
-     */
-    LIBEUFIN_EC_INVALID_PAYMENT_DIRECTION(1),
-
-    /**
-     * A bad piece of information made it to the database.  For
-     * example, a transaction whose direction is neither CRDT nor DBIT
-     * was found in the database.
-     */
-    LIBEUFIN_EC_INVALID_STATE(2),
-
-    /**
-     * A bank's invariant is not holding anymore.  For example, a customer's
-     * balance doesn't match the history of their bank account.
-     */
-    LIBEUFIN_EC_INCONSISTENT_STATE(3),
-
-    /**
-     * Access was forbidden due to wrong credentials.
-     */
-    LIBEUFIN_EC_AUTHENTICATION_FAILED(4),
-
-    /**
-     * A parameter in the request was malformed.
-     * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
-     * (A value of 0 indicates that the error is generated client-side).
-     */
-    LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED(5),
-
-    /**
-     * Two different resources are NOT having the same currency.
-     */
-    LIBEUFIN_EC_CURRENCY_INCONSISTENT(6),
-
-    /**
-     * A request is using a unsupported currency.  Usually returned
-     * along 400 Bad Request
-     */
-    LIBEUFIN_EC_BAD_CURRENCY(7),
-
-    LIBEUFIN_EC_TIMEOUT_EXPIRED(8)
-}
\ No newline at end of file
diff --git a/util/src/main/kotlin/TalerErrorCode.kt 
b/util/src/main/kotlin/TalerErrorCode.kt
index 7a509405..bf46a498 100644
--- a/util/src/main/kotlin/TalerErrorCode.kt
+++ b/util/src/main/kotlin/TalerErrorCode.kt
@@ -4295,6 +4295,4 @@ enum class TalerErrorCode(val code: Int) {
    * (A value of 0 indicates that the error is generated client-side).
    */
   TALER_EC_END(9999),
-
-
 }
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
index 3589dec2..687a97f7 100644
--- a/util/src/main/kotlin/time.kt
+++ b/util/src/main/kotlin/time.kt
@@ -20,10 +20,7 @@
 package tech.libeufin.util
 
 import java.time.*
-import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
 
-fun getNow(): ZonedDateTime {
-    return ZonedDateTime.now(ZoneId.systemDefault())
-}
 
-fun ZonedDateTime.toMicro(): Long = this.nano / 1000L
\ No newline at end of file
+fun getNowUs(): Long = ChronoUnit.MICROS.between(Instant.EPOCH, Instant.now())
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]