taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

oauth.rs (5314B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{borrow::Cow, time::SystemTime};
     18 
     19 use base64::{Engine as _, prelude::BASE64_STANDARD};
     20 use hmac::{Hmac, Mac};
     21 use percent_encoding::NON_ALPHANUMERIC;
     22 use rand_core::RngCore;
     23 use reqwest::{Request, header::HeaderValue};
     24 use serde::{Deserialize, Serialize};
     25 use sha1::Sha1;
     26 
     27 #[derive(Debug, Clone, Serialize, Deserialize)]
     28 pub struct Token {
     29     #[serde(rename = "oauth_token")]
     30     pub key: String,
     31     #[serde(rename = "oauth_token_secret")]
     32     pub secret: String,
     33 }
     34 
     35 #[derive(Debug, Deserialize)]
     36 pub struct TokenAuth {
     37     pub oauth_token: String,
     38     pub oauth_verifier: String,
     39 }
     40 
     41 /** Generate a secure OAuth nonce  */
     42 fn oauth_nonce() -> String {
     43     // Generate 8 secure random bytes
     44     let mut buf = [0u8; 8];
     45     rand_core::OsRng.fill_bytes(&mut buf);
     46     // Encode as base64 string
     47     BASE64_STANDARD.encode(buf)
     48 }
     49 
     50 /** Generate an OAuth timestamp  */
     51 fn oauth_timestamp() -> u64 {
     52     let start = SystemTime::now();
     53     let since_the_epoch = start
     54         .duration_since(std::time::UNIX_EPOCH)
     55         .expect("Time went backwards");
     56 
     57     since_the_epoch.as_secs()
     58 }
     59 
     60 /** Generate a valid OAuth Authorization header */
     61 fn oauth_header(
     62     method: &reqwest::Method,
     63     url: &reqwest::Url,
     64     consumer: &Token,
     65     access: Option<&Token>,
     66     verifier: Option<&str>,
     67 ) -> String {
     68     // Per request value
     69     let oauth_nonce = oauth_nonce();
     70     let oauth_timestamp = oauth_timestamp().to_string();
     71 
     72     // Base string
     73     let base_string = {
     74         let oauth_data = {
     75             let mut oauth_query: Vec<(&str, &str)> = vec![
     76                 ("oauth_consumer_key", &consumer.key),
     77                 ("oauth_nonce", &oauth_nonce),
     78                 ("oauth_signature_method", "HMAC-SHA1"),
     79                 ("oauth_timestamp", &oauth_timestamp),
     80             ];
     81             if let Some(token) = &access {
     82                 oauth_query.push(("oauth_token", &token.key));
     83             }
     84             if let Some(verifier) = &verifier {
     85                 oauth_query.push(("oauth_verifier", verifier));
     86             }
     87             oauth_query.push(("oauth_version", "1.0"));
     88             let mut all_query: Vec<_> = oauth_query
     89                 .into_iter()
     90                 .map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b)))
     91                 .chain(url.query_pairs())
     92                 .collect();
     93             all_query.sort_unstable();
     94 
     95             let mut tmp: form_urlencoded::Serializer<'_, String> =
     96                 form_urlencoded::Serializer::new(String::new());
     97             for (k, v) in all_query {
     98                 tmp.append_pair(&k, &v);
     99             }
    100             tmp.finish()
    101         };
    102         let mut stripped = url.clone();
    103         stripped.set_query(None);
    104         form_urlencoded::Serializer::new(String::new())
    105             .append_key_only(method.as_str())
    106             .append_key_only(stripped.as_str())
    107             .append_key_only(&oauth_data)
    108             .finish()
    109     };
    110 
    111     // Signature
    112     let key = {
    113         let mut buf = consumer.secret.clone();
    114         buf.push('&');
    115         if let Some(token) = access {
    116             buf.push_str(&token.secret);
    117         }
    118         buf
    119     };
    120     let signature = Hmac::<Sha1>::new_from_slice(key.as_bytes())
    121         .expect("HMAC can take key of any size")
    122         .chain_update(base_string.as_bytes())
    123         .finalize()
    124         .into_bytes();
    125     let signature_encoded = BASE64_STANDARD.encode(signature);
    126 
    127     // Authorization header
    128     {
    129         let mut buf = "OAuth ".to_string();
    130         let mut append = |key: &str, value: &str| {
    131             buf.push_str(key);
    132             buf.push_str("=\"");
    133             for part in percent_encoding::percent_encode(value.as_bytes(), NON_ALPHANUMERIC) {
    134                 buf.push_str(part);
    135             }
    136             buf.push_str("\",");
    137         };
    138         append("oauth_consumer_key", &consumer.key);
    139         append("oauth_nonce", &oauth_nonce);
    140         append("oauth_signature_method", "HMAC-SHA1");
    141         append("oauth_timestamp", &oauth_timestamp);
    142         if let Some(token) = &access {
    143             append("oauth_token", &token.key);
    144         }
    145         if let Some(verifier) = &verifier {
    146             append("oauth_verifier", verifier);
    147         }
    148         append("oauth_version", "1.0");
    149         append("oauth_signature", &signature_encoded);
    150         buf
    151     }
    152 }
    153 
    154 /** Perform OAuth on an HTTP request */
    155 pub fn oauth(req: &mut Request, consumer: &Token, access: Option<&Token>, verifier: Option<&str>) {
    156     let header = oauth_header(req.method(), req.url(), consumer, access, verifier);
    157     req.headers_mut()
    158         .append("Authorization", HeaderValue::from_str(&header).unwrap());
    159 }