libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }