aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-10-03 09:44:23 +0200
committerMS <ms@taler.net>2023-10-03 09:44:23 +0200
commitc58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d (patch)
treef0b290a08214151f597ccb56824bc952731e8709
parent4e74dd31c91e3209c34a381f45a666e4c3c477fb (diff)
downloadlibeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.tar.gz
libeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.tar.bz2
libeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.zip
implementing token deletion
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt27
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Database.kt26
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt3
-rw-r--r--bank/src/test/kotlin/DatabaseTest.kt29
-rw-r--r--bank/src/test/kotlin/LibeuFinApiTest.kt50
5 files changed, 128 insertions, 7 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index 489b71e0..6770e559 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -25,7 +25,32 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.account
fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
delete("/accounts/{USERNAME}/token") {
- throw internalServerError("Token deletion not implemented.")
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized()
+ /**
+ * The following command ensures that this call was
+ * authenticated with the bearer token and NOT with
+ * basic auth. FIXME: this "409 Conflict" case is not documented.
+ */
+ val token = call.getAuthToken() ?: throw badRequest("Basic auth not supported here.")
+ val resourceName = call.getResourceName("USERNAME")
+ /**
+ * The following check makes sure that the token belongs
+ * to the username contained in {USERNAME}.
+ */
+ if (!resourceName.canI(c, withAdmin = true)) throw forbidden()
+
+ /**
+ * Not sanity-checking the token, as it was used by the authentication already.
+ * If harder errors happen, then they'll get Ktor respond with 500.
+ */
+ db.bearerTokenDelete(Base32Crockford.decode(token))
+ /**
+ * Responding 204 regardless of it being actually deleted or not.
+ * If it wasn't found, then it must have been deleted before we
+ * reached here, but the token was valid as it served the authentication
+ * => no reason to fail the request.
+ */
+ call.respond(HttpStatusCode.NoContent)
}
post("/accounts/{USERNAME}/token") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index a18f5bd2..e221bd2c 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -243,10 +243,10 @@ class Database(private val dbConfig: String, private val bankCurrency: String) {
FROM customer_delete(?);
""")
stmt.setString(1, login)
- stmt.executeQuery().apply {
- if (!this.next()) throw internalServerError("Deletion returned nothing.")
- if (this.getBoolean("out_nx_customer")) return CustomerDeletionResult.CUSTOMER_NOT_FOUND
- if (this.getBoolean("out_balance_not_zero")) return CustomerDeletionResult.BALANCE_NOT_ZERO
+ stmt.executeQuery().use {
+ if (!it.next()) throw internalServerError("Deletion returned nothing.")
+ if (it.getBoolean("out_nx_customer")) return CustomerDeletionResult.CUSTOMER_NOT_FOUND
+ if (it.getBoolean("out_balance_not_zero")) return CustomerDeletionResult.BALANCE_NOT_ZERO
return CustomerDeletionResult.SUCCESS
}
}
@@ -378,6 +378,24 @@ class Database(private val dbConfig: String, private val bankCurrency: String) {
)
}
}
+ /**
+ * Deletes a bearer token from the database. Returns true,
+ * if deletion succeeds or false if the token could not be
+ * deleted (= not found).
+ */
+ fun bearerTokenDelete(token: ByteArray): Boolean {
+ reconnect()
+ val stmt = prepare("""
+ DELETE FROM bearer_tokens
+ WHERE content = ?
+ RETURNING bearer_token_id;
+ """)
+ stmt.setBytes(1, token)
+ stmt.executeQuery().use {
+ if (!it.next()) return false;
+ return true
+ }
+ }
// MIXED CUSTOMER AND BANK ACCOUNT DATA
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 9f08f447..5329efa1 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -105,7 +105,8 @@ fun doTokenAuth(
requiredScope: TokenScope,
): Customer? {
val bareToken = splitBearerToken(token) ?: throw badRequest(
- "Bearer token malformed", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ "Bearer token malformed",
+ talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
val tokenBytes = try {
Base32Crockford.decode(bareToken)
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
index 89415b3a..55d4a1c9 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -25,6 +25,7 @@ import java.sql.DriverManager
import java.time.Instant
import java.util.Random
import java.util.UUID
+import kotlin.experimental.inv
// Foo pays Bar with custom subject.
fun genTx(
@@ -143,10 +144,36 @@ class DatabaseTest {
scope = TokenScope.readonly
)
assert(db.bearerTokenGet(token.content) == null)
- db.customerCreate(customerBar) // Tokens need owners.
+ assert(db.customerCreate(customerBar) != null) // Tokens need owners.
assert(db.bearerTokenCreate(token))
assert(db.bearerTokenGet(tokenBytes) != null)
}
+
+ @Test
+ fun tokenDeletionTest() {
+ val db = initDb()
+ val token = ByteArray(32)
+ // Token not there, must fail.
+ assert(!db.bearerTokenDelete(token))
+ assert(db.customerCreate(customerBar) != null) // Tokens need owners.
+ assert(db.bearerTokenCreate(
+ BearerToken(
+ bankCustomer = 1L,
+ content = token,
+ creationTime = Instant.now(),
+ expirationTime = Instant.now().plusSeconds(10),
+ scope = TokenScope.readwrite
+ )
+ ))
+ // Wrong token given, must fail
+ val anotherToken = token.map {
+ it.inv() // flipping every bit.
+ }
+ assert(!db.bearerTokenDelete(anotherToken.toByteArray()))
+ // Token there, must succeed.
+ assert(db.bearerTokenDelete(token))
+ }
+
@Test
fun bankTransactionsTest() {
val db = initDb()
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 3b63183a..a66bd062 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -180,6 +180,56 @@ class LibeuFinApiTest {
}
}
+ @Test
+ fun tokenDeletionTest() {
+ val db = initDb()
+ val ctx = getTestContext()
+ assert(db.customerCreate(customerFoo) != null)
+ val token = ByteArray(32)
+ Random.nextBytes(token)
+ assert(db.bearerTokenCreate(
+ BearerToken(
+ bankCustomer = 1L,
+ content = token,
+ creationTime = Instant.now(),
+ expirationTime = Instant.now().plusSeconds(10),
+ scope = TokenScope.readwrite
+ )
+ ))
+ testApplication {
+ application {
+ corebankWebApp(db, ctx)
+ }
+ // Legitimate first attempt, should succeed
+ client.delete("/accounts/foo/token") {
+ expectSuccess = true
+ headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}"
+ }.apply {
+ assert(this.status == HttpStatusCode.NoContent)
+ }
+ // Trying after deletion should hit 404.
+ client.delete("/accounts/foo/token") {
+ expectSuccess = false
+ headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}"
+ }.apply {
+ assert(this.status == HttpStatusCode.Unauthorized)
+ }
+ // Checking foo can still be served by basic auth, after token deletion.
+ assert(db.bankAccountCreate(
+ BankAccount(
+ hasDebt = false,
+ internalPaytoUri = "payto://iban/DE1234",
+ maxDebt = TalerAmount(100, 0, "KUDOS"),
+ owningCustomerId = 1
+ )
+ ) != null)
+ client.get("/accounts/foo") {
+ expectSuccess = true
+ basicAuth("foo", "pw")
+ }
+ }
+ }
+
// Creating token with "forever" duration.
@Test
fun tokenForeverTest() {