auth.rs (17132B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 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 use std::{fmt::Display, marker::PhantomData, sync::Arc}; 21 22 use axum::{ 23 extract::FromRequestParts, 24 http::{ 25 HeaderMap, HeaderValue, StatusCode, Uri, 26 header::{AUTHORIZATION, WWW_AUTHENTICATE}, 27 request::Parts, 28 }, 29 }; 30 use compact_str::CompactString; 31 use jiff::Timestamp; 32 use serde::{Deserialize, Serialize}; 33 use sqlx::PgPool; 34 use taler_api::error::{ 35 ApiError, ApiResult, failure, failure_code, failure_status, forbidden, unauthorized, 36 }; 37 use taler_common::{ 38 encoding::{base32, base64}, 39 error_code::ErrorCode, 40 types::time::TalerTimestamp, 41 }; 42 use taler_macros::EnumMeta; 43 use tracing::warn; 44 45 use crate::{ 46 api::BankState, 47 db::{ 48 account::{BankInfo, CheckPasswordResult, check_password}, 49 token::access_info, 50 }, 51 payto::PaytoCtx, 52 pw::PwCrypto, 53 }; 54 55 pub const TOKEN_PREFIX: &str = "secret-token:"; 56 57 #[derive(Clone, PartialEq)] 58 #[allow(non_camel_case_types)] 59 pub enum TokenLogicalScope { 60 readonly, 61 readwrite, 62 revenue, 63 refreshable, 64 readonly_wiregateway, 65 readwrite_wiregateway, 66 observability, 67 } 68 69 impl TokenLogicalScope { 70 pub fn is_valid_scope(&self, scope: TokenScope, refreshable: bool) -> bool { 71 match self { 72 TokenLogicalScope::readonly => { 73 matches!(scope, TokenScope::readonly | TokenScope::readwrite) 74 } 75 TokenLogicalScope::readwrite => matches!(scope, TokenScope::readwrite), 76 TokenLogicalScope::revenue => matches!( 77 scope, 78 TokenScope::readonly | TokenScope::readwrite | TokenScope::revenue 79 ), 80 TokenLogicalScope::refreshable => refreshable, 81 TokenLogicalScope::readonly_wiregateway => matches!( 82 scope, 83 TokenScope::readonly | TokenScope::readwrite | TokenScope::wiregateway 84 ), 85 TokenLogicalScope::readwrite_wiregateway => { 86 matches!(scope, TokenScope::readwrite | TokenScope::wiregateway) 87 } 88 TokenLogicalScope::observability => matches!( 89 scope, 90 TokenScope::readonly | TokenScope::readwrite | TokenScope::observability 91 ), 92 } 93 } 94 } 95 96 #[derive( 97 Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize, EnumMeta, clap::ValueEnum, 98 )] 99 #[sqlx(type_name = "token_scope_enum")] 100 #[enum_meta(Str)] 101 #[allow(non_camel_case_types)] 102 pub enum TokenScope { 103 readonly, 104 readwrite, 105 revenue, 106 wiregateway, 107 observability, 108 } 109 110 impl TokenScope { 111 pub fn logical(&self) -> TokenLogicalScope { 112 match self { 113 TokenScope::readonly => TokenLogicalScope::readonly, 114 TokenScope::readwrite => TokenLogicalScope::readwrite, 115 TokenScope::revenue => TokenLogicalScope::revenue, 116 TokenScope::wiregateway => TokenLogicalScope::readwrite_wiregateway, 117 TokenScope::observability => TokenLogicalScope::observability, 118 } 119 } 120 } 121 122 pub struct RegistrationAuth { 123 pub is_admin: bool, 124 } 125 126 impl FromRequestParts<Arc<BankState>> for RegistrationAuth { 127 type Rejection = ApiError; 128 129 async fn from_request_parts( 130 parts: &mut Parts, 131 state: &Arc<BankState>, 132 ) -> Result<Self, Self::Rejection> { 133 let res = auth_request( 134 &state.db, 135 &state.cfg.ctx, 136 &state.cfg.pw_crypto, 137 TokenLogicalScope::readwrite, 138 false, 139 state.cfg.basic_auth_compat, 140 &parts.headers, 141 ) 142 .await; 143 if state.cfg.allow_registration { 144 if let Ok((info, _)) = res { 145 Ok(RegistrationAuth { 146 is_admin: info.is_admin(), 147 }) 148 } else { 149 Ok(RegistrationAuth { is_admin: false }) 150 } 151 } else { 152 let (info, _) = res?; 153 if !info.is_admin() { 154 Err(forbidden("Only administrator allowed")) 155 } else { 156 Ok(RegistrationAuth { is_admin: true }) 157 } 158 } 159 } 160 } 161 162 pub enum AuthKind { 163 AdminOnly, 164 UserOnly, 165 UserOrAdmin, 166 } 167 168 pub trait UserAuthScope: Send { 169 const SCOPE: TokenLogicalScope; 170 const KIND: AuthKind; 171 const ALLOW_BASIC_AUTH: bool = false; 172 } 173 174 fn extract_username(url: &Uri) -> &str { 175 let mut iter = url.path().strip_prefix('/').unwrap().split('/'); 176 assert_eq!(iter.next(), Some("accounts")); 177 iter.next().unwrap() 178 } 179 180 pub struct UserAuth<S> { 181 pub username: CompactString, 182 pub token: Option<Vec<u8>>, 183 auth_info: BankInfo, 184 user_info: Option<BankInfo>, 185 scope: PhantomData<S>, 186 } 187 188 impl<S> UserAuth<S> { 189 /** Retrieve the bank account info for the selected username */ 190 pub async fn bank_info(&mut self, db: &PgPool, ctx: &PaytoCtx) -> ApiResult<&BankInfo> { 191 if self.user_info.is_none() { 192 if self.auth_info.username == self.username { 193 return Ok(&self.auth_info); 194 } 195 let info = super::db::account::bank_info(db, ctx, &self.username).await?; 196 197 let Some(info) = info else { 198 return Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)); 199 }; 200 self.user_info = Some(info); 201 } 202 Ok(self.user_info.as_ref().unwrap()) 203 } 204 205 /** Check if authenticated user is admin */ 206 pub fn is_admin(&self) -> bool { 207 self.auth_info.is_admin() 208 } 209 } 210 211 impl<S: UserAuthScope> FromRequestParts<Arc<BankState>> for UserAuth<S> { 212 type Rejection = ApiError; 213 214 async fn from_request_parts( 215 parts: &mut Parts, 216 state: &Arc<BankState>, 217 ) -> Result<Self, Self::Rejection> { 218 let username = extract_username(&parts.uri); 219 let (info, token) = auth_request( 220 &state.db, 221 &state.cfg.ctx, 222 &state.cfg.pw_crypto, 223 S::SCOPE, 224 S::ALLOW_BASIC_AUTH || state.cfg.basic_auth_compat, 225 state.cfg.basic_auth_compat, 226 &parts.headers, 227 ) 228 .await?; 229 230 match S::KIND { 231 AuthKind::AdminOnly => { 232 if !info.is_admin() { 233 return Err(require_admin()); 234 } 235 } 236 AuthKind::UserOnly => { 237 if info.username != username { 238 return Err(forbidden(format_args!( 239 "Customer {} have no right on {username} account", 240 info.username 241 ))); 242 } 243 } 244 AuthKind::UserOrAdmin => { 245 if info.username != username && !info.is_admin() { 246 return Err(forbidden(format_args!( 247 "Customer {} have no right on {username} account", 248 info.username 249 ))); 250 } 251 } 252 } 253 254 Ok(Self { 255 username: username.into(), 256 auth_info: info, 257 user_info: None, 258 token, 259 scope: PhantomData, 260 }) 261 } 262 } 263 264 pub struct UserOptAuth<S> { 265 pub username: CompactString, 266 auth_info: Option<BankInfo>, 267 user_info: Option<BankInfo>, 268 scope: PhantomData<S>, 269 } 270 271 impl<S> UserOptAuth<S> { 272 pub fn is_authenticated(&self) -> bool { 273 self.auth_info.is_some() 274 } 275 276 /** Retrieve the bank account info for the selected username if authenticated or if public */ 277 pub async fn bank_info_auth_or_public( 278 &mut self, 279 db: &PgPool, 280 ctx: &PaytoCtx, 281 ) -> ApiResult<&BankInfo> { 282 if self.user_info.is_none() { 283 if let Some(info) = &self.auth_info 284 && info.username == self.username 285 { 286 return Ok(info); 287 } 288 let info = super::db::account::bank_info(db, ctx, &self.username).await?; 289 290 let Some(info) = info else { 291 return Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)); 292 }; 293 self.user_info = Some(info); 294 } 295 let info = self.user_info.as_ref().unwrap(); 296 if !info.is_public && !self.is_authenticated() { 297 Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) 298 } else { 299 Ok(info) 300 } 301 } 302 } 303 304 impl<S: UserAuthScope> FromRequestParts<Arc<BankState>> for UserOptAuth<S> { 305 type Rejection = ApiError; 306 307 async fn from_request_parts( 308 parts: &mut Parts, 309 state: &Arc<BankState>, 310 ) -> Result<Self, Self::Rejection> { 311 if parts.headers.get(AUTHORIZATION).is_some() { 312 let auth = UserAuth::<S>::from_request_parts(parts, state).await?; 313 Ok(Self { 314 username: auth.username, 315 auth_info: Some(auth.auth_info), 316 user_info: None, 317 scope: PhantomData, 318 }) 319 } else { 320 let username = extract_username(&parts.uri); 321 Ok(Self { 322 username: username.into(), 323 auth_info: None, 324 user_info: None, 325 scope: PhantomData, 326 }) 327 } 328 } 329 } 330 331 pub struct UserRWScope; 332 333 impl UserAuthScope for UserRWScope { 334 const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; 335 const KIND: AuthKind = AuthKind::UserOrAdmin; 336 } 337 338 pub struct UserORWScope; 339 340 impl UserAuthScope for UserORWScope { 341 const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; 342 const KIND: AuthKind = AuthKind::UserOnly; 343 } 344 345 pub struct UserRScope; 346 347 impl UserAuthScope for UserRScope { 348 const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly; 349 const KIND: AuthKind = AuthKind::UserOrAdmin; 350 } 351 352 pub struct UserTokenScope; 353 354 impl UserAuthScope for UserTokenScope { 355 const SCOPE: TokenLogicalScope = TokenLogicalScope::refreshable; 356 const KIND: AuthKind = AuthKind::UserOnly; 357 const ALLOW_BASIC_AUTH: bool = true; 358 } 359 360 pub struct RevenueScope; 361 362 impl UserAuthScope for RevenueScope { 363 const SCOPE: TokenLogicalScope = TokenLogicalScope::revenue; 364 const KIND: AuthKind = AuthKind::UserOrAdmin; 365 } 366 367 pub struct WireRScope; 368 369 impl UserAuthScope for WireRScope { 370 const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly_wiregateway; 371 const KIND: AuthKind = AuthKind::UserOnly; 372 } 373 374 pub struct WireRWScope; 375 376 impl UserAuthScope for WireRWScope { 377 const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite_wiregateway; 378 const KIND: AuthKind = AuthKind::UserOnly; 379 } 380 381 pub struct UserAdminScope; 382 383 impl UserAuthScope for UserAdminScope { 384 const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; 385 const KIND: AuthKind = AuthKind::AdminOnly; 386 } 387 388 pub type UserRWAuth = UserAuth<UserRWScope>; 389 pub type UserRAuth = UserAuth<UserRScope>; 390 pub type UserORWAuth = UserAuth<UserORWScope>; 391 pub type UserTokenAuth = UserAuth<UserTokenScope>; 392 pub type UserOptRAuth = UserOptAuth<UserRScope>; 393 pub type RevenueAuth = UserAuth<RevenueScope>; 394 pub type WireRAuth = UserAuth<WireRScope>; 395 pub type WireRWAuth = UserAuth<WireRWScope>; 396 pub type UserAdminAuth = UserAuth<UserAdminScope>; 397 398 pub trait RootAuthScope: Send { 399 const SCOPE: TokenLogicalScope; 400 } 401 402 pub type AdminRWAuth = AdminAuth<RootRWScope>; 403 pub type AdminRAuth = AdminAuth<RootRScope>; 404 405 pub struct RootRWScope; 406 impl RootAuthScope for RootRWScope { 407 const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; 408 } 409 410 pub struct RootRScope; 411 impl RootAuthScope for RootRScope { 412 const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly; 413 } 414 415 pub struct ObservabilityScope; 416 impl RootAuthScope for ObservabilityScope { 417 const SCOPE: TokenLogicalScope = TokenLogicalScope::observability; 418 } 419 420 pub struct AdminAuth<S> { 421 scope: PhantomData<S>, 422 } 423 424 impl<S: RootAuthScope> FromRequestParts<Arc<BankState>> for AdminAuth<S> { 425 type Rejection = ApiError; 426 427 async fn from_request_parts( 428 parts: &mut Parts, 429 state: &Arc<BankState>, 430 ) -> Result<Self, Self::Rejection> { 431 let (info, _) = auth_request( 432 &state.db, 433 &state.cfg.ctx, 434 &state.cfg.pw_crypto, 435 S::SCOPE, 436 state.cfg.basic_auth_compat, 437 state.cfg.basic_auth_compat, 438 &parts.headers, 439 ) 440 .await?; 441 442 if !info.is_admin() { 443 return Err(require_admin()); 444 } 445 446 Ok(Self { scope: PhantomData }) 447 } 448 } 449 450 /** 451 * Authenticate an HTTP request for [requiredScope] according to the scheme that is mentioned 452 * in the Authorization header. 453 * The allowed schemes are either 'Basic' or 'Bearer'. 454 * 455 * Returns the authenticated customer username. 456 */ 457 async fn auth_request( 458 db: &PgPool, 459 ctx: &PaytoCtx, 460 pw_crypto: &PwCrypto, 461 scope: TokenLogicalScope, 462 allow_pw: bool, 463 compat_pw: bool, 464 headers: &HeaderMap, 465 ) -> ApiResult<(BankInfo, Option<Vec<u8>>)> { 466 fn headers_malformed(hint: impl Display) -> ApiError { 467 failure_status( 468 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 469 hint, 470 StatusCode::UNAUTHORIZED, 471 ) 472 } 473 474 let header = headers.get(AUTHORIZATION); 475 let Some(authorisation) = header else { 476 let err = failure_status( 477 ErrorCode::GENERIC_PARAMETER_MISSING, 478 "Authorization header not found", 479 StatusCode::UNAUTHORIZED, 480 ); 481 if allow_pw || compat_pw { 482 return Err(err.with_header( 483 WWW_AUTHENTICATE, 484 HeaderValue::from_static(r#"Basic realm="LibEuFin Bank", charset="UTF-8""#), 485 )); 486 } else { 487 return Err(err); 488 } 489 }; 490 491 let Some((hscheme, parameter)) = authorisation 492 .to_str() 493 .ok() 494 .and_then(|it| it.split_once(' ')) 495 else { 496 return Err(failure( 497 ErrorCode::GENERIC_UNAUTHORIZED, 498 "Authorization header is malformed", 499 )); 500 }; 501 502 match hscheme { 503 "Basic" => { 504 let Some(decoded) = base64::decode(parameter) 505 .ok() 506 .and_then(|decoded| String::from_utf8(decoded).ok()) 507 else { 508 return Err(headers_malformed( 509 "Malformed Basic auth credentials found in the Authorization header", 510 )); 511 }; 512 let Some((username, pw)) = decoded.split_once(":") else { 513 return Err(headers_malformed( 514 "Malformed Basic auth credentials found in the Authorization header", 515 )); 516 }; 517 if !allow_pw { 518 warn!(target: "api", "User '{username}' used deprecated password auth"); 519 if !compat_pw { 520 return Err(unauthorized("Authorization method 'Basic' not supported")); 521 } 522 } 523 524 match check_password(db, ctx, pw_crypto, username, pw).await? { 525 CheckPasswordResult::UnknownAccount => Err(unauthorized("Unknown account")), 526 CheckPasswordResult::PasswordMismatch => Err(unauthorized("Bad password")), 527 CheckPasswordResult::Locked => Err(failure_code(ErrorCode::BANK_ACCOUNT_LOCKED)), 528 CheckPasswordResult::Success(info) => Ok((info, None)), 529 } 530 } 531 "Bearer" => { 532 let Some(token) = parameter.strip_prefix(TOKEN_PREFIX) else { 533 return Err(headers_malformed("Bearer token malformed")); 534 }; 535 let decoded = base32::decode(token.as_bytes()).map_err(headers_malformed)?; 536 537 let now = Timestamp::now(); 538 let Some((token, info)) = access_info(db, ctx, &decoded, &now).await? else { 539 return Err(failure_code(ErrorCode::GENERIC_TOKEN_UNKNOWN)); 540 }; 541 542 if let TalerTimestamp::Timestamp(expiration) = token.expiration 543 && expiration < now 544 { 545 return Err(failure_code(ErrorCode::GENERIC_TOKEN_EXPIRED)); 546 } else if !scope.is_valid_scope(token.scope, token.is_refreshable) { 547 return Err(failure_code( 548 ErrorCode::GENERIC_TOKEN_PERMISSION_INSUFFICIENT, 549 )); 550 } 551 552 Ok((info, Some(decoded))) 553 } 554 _ => Err(failure( 555 ErrorCode::GENERIC_UNAUTHORIZED, 556 format!("Authorization method '{hscheme}' wrong or not supported"), 557 )), 558 } 559 } 560 561 pub fn require_admin() -> ApiError { 562 forbidden("Only administrator allowed") 563 }