diff options
author | MS <ms@taler.net> | 2023-10-03 09:44:23 +0200 |
---|---|---|
committer | MS <ms@taler.net> | 2023-10-03 09:44:23 +0200 |
commit | c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d (patch) | |
tree | f0b290a08214151f597ccb56824bc952731e8709 | |
parent | 4e74dd31c91e3209c34a381f45a666e4c3c477fb (diff) | |
download | libeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.tar.gz libeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.tar.bz2 libeufin-c58a867f3e0a32f5bfa7cfa1e40ad02cbfa1907d.zip |
implementing token deletion
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 27 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Database.kt | 26 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 3 | ||||
-rw-r--r-- | bank/src/test/kotlin/DatabaseTest.kt | 29 | ||||
-rw-r--r-- | bank/src/test/kotlin/LibeuFinApiTest.kt | 50 |
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() { |