taler-rust

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

oauth.rs (5233B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025, 2026 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 aws_lc_rs::hmac;
     20 use base64::{Engine as _, prelude::BASE64_STANDARD};
     21 use http_client::builder::Req;
     22 use hyper::header;
     23 use percent_encoding::NON_ALPHANUMERIC;
     24 use serde::{Deserialize, Serialize};
     25 
     26 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
     27 pub struct Token {
     28     #[serde(rename = "oauth_token")]
     29     pub key: String,
     30     #[serde(rename = "oauth_token_secret")]
     31     pub secret: String,
     32 }
     33 
     34 #[derive(Debug, Deserialize)]
     35 pub struct TokenAuth {
     36     pub oauth_token: String,
     37     pub oauth_verifier: String,
     38 }
     39 
     40 /** Generate a secure OAuth nonce  */
     41 fn oauth_nonce() -> String {
     42     // Generate 8 secure random bytes
     43     let mut buf = [0u8; 8];
     44     getrandom::fill(&mut buf).expect("Failed to get random bytes from OS");
     45     // Encode as base64 string
     46     BASE64_STANDARD.encode(buf)
     47 }
     48 
     49 /** Generate an OAuth timestamp  */
     50 fn oauth_timestamp() -> u64 {
     51     let start = SystemTime::now();
     52     let since_the_epoch = start
     53         .duration_since(std::time::UNIX_EPOCH)
     54         .expect("Time went backwards");
     55 
     56     since_the_epoch.as_secs()
     57 }
     58 
     59 /** Generate a valid OAuth Authorization header */
     60 fn oauth_header(
     61     method: &hyper::Method,
     62     url: &url::Url,
     63     consumer: &Token,
     64     access: Option<&Token>,
     65     verifier: Option<&str>,
     66 ) -> String {
     67     // Per request value
     68     let oauth_nonce = oauth_nonce();
     69     let oauth_timestamp = oauth_timestamp().to_string();
     70 
     71     // Base string
     72     let base_string = {
     73         let oauth_data = {
     74             let mut oauth_query: Vec<(&str, &str)> = vec![
     75                 ("oauth_consumer_key", &consumer.key),
     76                 ("oauth_nonce", &oauth_nonce),
     77                 ("oauth_signature_method", "HMAC-SHA1"),
     78                 ("oauth_timestamp", &oauth_timestamp),
     79             ];
     80             if let Some(token) = &access {
     81                 oauth_query.push(("oauth_token", &token.key));
     82             }
     83             if let Some(verifier) = &verifier {
     84                 oauth_query.push(("oauth_verifier", verifier));
     85             }
     86             oauth_query.push(("oauth_version", "1.0"));
     87             let mut all_query: Vec<_> = oauth_query
     88                 .into_iter()
     89                 .map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b)))
     90                 .chain(url.query_pairs())
     91                 .collect();
     92             all_query.sort_unstable();
     93 
     94             let mut tmp: form_urlencoded::Serializer<'_, String> =
     95                 form_urlencoded::Serializer::new(String::new());
     96             for (k, v) in all_query {
     97                 tmp.append_pair(&k, &v);
     98             }
     99             tmp.finish()
    100         };
    101         let mut stripped = url.clone();
    102         stripped.set_query(None);
    103         form_urlencoded::Serializer::new(String::new())
    104             .append_key_only(method.as_str())
    105             .append_key_only(stripped.as_str())
    106             .append_key_only(&oauth_data)
    107             .finish()
    108     };
    109 
    110     // Signature
    111     let key = {
    112         let mut buf = consumer.secret.clone();
    113         buf.push('&');
    114         if let Some(token) = access {
    115             buf.push_str(&token.secret);
    116         }
    117         buf
    118     };
    119     let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key.as_bytes());
    120     let signature = hmac::sign(&key, base_string.as_bytes());
    121     let signature_encoded = BASE64_STANDARD.encode(signature.as_ref());
    122 
    123     // Authorization header
    124     {
    125         let mut buf = "OAuth ".to_string();
    126         let mut append = |key: &str, value: &str| {
    127             buf.push_str(key);
    128             buf.push_str("=\"");
    129             for part in percent_encoding::percent_encode(value.as_bytes(), NON_ALPHANUMERIC) {
    130                 buf.push_str(part);
    131             }
    132             buf.push_str("\",");
    133         };
    134         append("oauth_consumer_key", &consumer.key);
    135         append("oauth_nonce", &oauth_nonce);
    136         append("oauth_signature_method", "HMAC-SHA1");
    137         append("oauth_timestamp", &oauth_timestamp);
    138         if let Some(token) = &access {
    139             append("oauth_token", &token.key);
    140         }
    141         if let Some(verifier) = &verifier {
    142             append("oauth_verifier", verifier);
    143         }
    144         append("oauth_version", "1.0");
    145         append("oauth_signature", &signature_encoded);
    146         buf
    147     }
    148 }
    149 
    150 /** Perform OAuth on an HTTP request */
    151 pub fn oauth(req: Req, consumer: &Token, access: Option<&Token>, verifier: Option<&str>) -> Req {
    152     let header = oauth_header(req.method(), req.url(), consumer, access, verifier);
    153     req.sensitive_header(header::AUTHORIZATION, header)
    154 }