commit c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d
parent 4e74dd31c91e3209c34a381f45a666e4c3c477fb
Author: MS <ms@taler.net>
Date: Tue, 3 Oct 2023 09:44:23 +0200
implementing token deletion
Diffstat:
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {