TokenDAO.kt (8294B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2023-2025 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.bank.db 21 22 import tech.libeufin.bank.* 23 import tech.libeufin.common.PageParams 24 import tech.libeufin.common.asInstant 25 import tech.libeufin.common.db.* 26 import tech.libeufin.common.micros 27 import java.time.Instant 28 29 /** Data access logic for auth tokens */ 30 class TokenDAO(private val db: Database) { 31 /** Result status of token creation */ 32 sealed interface TokenCreationResult { 33 data object Success: TokenCreationResult 34 data object TanRequired: TokenCreationResult 35 } 36 37 /** Create new token for [username] */ 38 suspend fun create( 39 username: String, 40 content: ByteArray, 41 creationTime: Instant, 42 expirationTime: Instant, 43 scope: TokenScope, 44 isRefreshable: Boolean, 45 description: String?, 46 is2fa: Boolean 47 ): TokenCreationResult = db.serializable( 48 """ 49 SELECT out_tan_required FROM create_token( 50 ?,?,?,?,?::token_scope_enum,?,?,? 51 ) 52 """ 53 ) { 54 bind(username) 55 bind(content) 56 bind(creationTime) 57 bind(expirationTime) 58 bind(scope) 59 bind(isRefreshable) 60 bind(description) 61 bind(is2fa) 62 one { 63 when { 64 it.getBoolean("out_tan_required") -> TokenCreationResult.TanRequired 65 else -> TokenCreationResult.Success 66 } 67 } 68 } 69 70 /** 71 * Atomically create a replacement for [source] and shorten the source 72 * token's lifetime to [sourceExpirationCap]. LEAST makes the overlap 73 * deadline non-sliding when the same source is refreshed more than once. 74 */ 75 suspend fun refresh( 76 username: String, 77 source: ByteArray, 78 content: ByteArray, 79 creationTime: Instant, 80 expirationTime: Instant, 81 sourceExpirationCap: Instant, 82 scope: TokenScope, 83 isRefreshable: Boolean, 84 description: String? 85 ): Boolean = db.serializable( 86 """ 87 WITH source_token AS ( 88 UPDATE bearer_tokens 89 SET expiration_time=LEAST(expiration_time, ?) 90 WHERE content=? 91 AND bank_customer=( 92 SELECT customer_id FROM customers 93 WHERE username=? AND deleted_at IS NULL 94 ) 95 RETURNING bank_customer 96 ), replacement AS ( 97 INSERT INTO bearer_tokens ( 98 content, creation_time, expiration_time, scope, bank_customer, 99 is_refreshable, description, last_access 100 ) 101 SELECT ?, ?, ?, ?::token_scope_enum, bank_customer, ?, ?, ? 102 FROM source_token 103 RETURNING bearer_token_id 104 ) 105 SELECT EXISTS(SELECT FROM replacement) 106 """ 107 ) { 108 bind(sourceExpirationCap) 109 bind(source) 110 bind(username) 111 bind(content) 112 bind(creationTime) 113 bind(expirationTime) 114 bind(scope) 115 bind(isRefreshable) 116 bind(description) 117 bind(creationTime) 118 one { it.getBoolean(1) } 119 } 120 121 /** Get info for [token] */ 122 suspend fun access(token: ByteArray, accessTime: Instant): BearerToken? = db.serializable( 123 """ 124 UPDATE bearer_tokens 125 SET last_access=? 126 FROM customers 127 WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL 128 RETURNING 129 creation_time, 130 expiration_time, 131 scope, 132 is_refreshable 133 """ 134 ) { 135 bind(accessTime) 136 bind(token) 137 oneOrNull { 138 BearerToken( 139 creationTime = it.getLong("creation_time").asInstant(), 140 expirationTime = it.getLong("expiration_time").asInstant(), 141 scope = it.getEnum("scope"), 142 isRefreshable = it.getBoolean("is_refreshable") 143 ) 144 } 145 } 146 147 /** Get info for [token] and its associated bank account*/ 148 suspend fun accessInfo(token: ByteArray, accessTime: Instant): Pair<BearerToken, BankInfo>? = db.serializable( 149 """ 150 UPDATE bearer_tokens 151 SET last_access=? 152 FROM customers 153 JOIN bank_accounts ON customer_id=owning_customer_id 154 WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL 155 RETURNING 156 creation_time, 157 expiration_time, 158 scope, 159 is_refreshable, 160 username, 161 is_taler_exchange, 162 bank_account_id, 163 internal_payto, 164 name, 165 tan_channels, 166 email, 167 phone 168 """ 169 ) { 170 bind(accessTime) 171 bind(token) 172 oneOrNull { 173 Pair( 174 BearerToken( 175 creationTime = it.getLong("creation_time").asInstant(), 176 expirationTime = it.getLong("expiration_time").asInstant(), 177 scope = it.getEnum("scope"), 178 isRefreshable = it.getBoolean("is_refreshable") 179 ), 180 BankInfo( 181 username = it.getString("username"), 182 payto = it.getBankPayto("internal_payto", "name", db.ctx), 183 bankAccountId = it.getLong("bank_account_id"), 184 isTalerExchange = it.getBoolean("is_taler_exchange"), 185 channels = it.getEnumSet<TanChannel>("tan_channels"), 186 phone = it.getString("phone"), 187 email = it.getString("email") 188 ) 189 ) 190 } 191 } 192 193 /** Delete token [token] */ 194 suspend fun delete(token: ByteArray) = db.serializable( 195 "DELETE FROM bearer_tokens WHERE content = ?" 196 ) { 197 bind(token) 198 executeUpdate() 199 } 200 201 /** Delete token [id] owned by the account identified by [username]. */ 202 suspend fun deleteById(id: Long, username: String) = db.serializable( 203 """ 204 DELETE FROM bearer_tokens 205 WHERE bearer_token_id = ? 206 AND bank_customer=( 207 SELECT customer_id FROM customers 208 WHERE username=? AND deleted_at IS NULL 209 ) 210 """ 211 ) { 212 bind(id) 213 bind(username) 214 executeUpdateCheck() 215 } 216 217 /** Get a page of all tokens of [username] accounts */ 218 suspend fun page(params: PageParams, username: String, timestamp: Instant): List<TokenInfo> 219 = db.page( 220 params, 221 "bearer_token_id", 222 """ 223 SELECT 224 creation_time, 225 expiration_time, 226 scope, 227 is_refreshable, 228 description, 229 last_access, 230 bearer_token_id 231 FROM bearer_tokens 232 WHERE 233 expiration_time > ? AND 234 bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = ?) 235 AND 236 """, 237 { 238 bind(timestamp.micros()) 239 bind(username) 240 } 241 ) { 242 TokenInfo( 243 creation_time = it.getTalerTimestamp("creation_time"), 244 expiration = it.getTalerTimestamp("expiration_time"), 245 scope = it.getEnum("scope"), 246 refreshable = it.getBoolean("is_refreshable"), 247 description = it.getString("description"), 248 last_access = it.getTalerTimestamp("last_access"), 249 row_id = it.getLong("bearer_token_id"), 250 token_id = it.getLong("bearer_token_id") 251 ) 252 } 253 }