airlab_lib/token/
mod.rs

1#![allow(clippy::module_name_repetitions)]
2mod error;
3
4pub use self::error::{Error, Result};
5
6use crate::b64::{b64u_decode_to_string, b64u_encode};
7use crate::config::auth_config;
8use crate::time::{now_utc, now_utc_plus_sec_str, parse_utc};
9use hmac::{Hmac, Mac};
10use sha2::Sha512;
11use std::fmt::Display;
12use std::str::FromStr;
13use uuid::Uuid;
14
15#[derive(Debug)]
16#[cfg_attr(test, derive(PartialEq, Eq))]
17pub struct Token {
18    pub ident: String,
19    pub exp: String,
20    pub sign_b64u: String,
21}
22
23impl FromStr for Token {
24    type Err = Error;
25
26    fn from_str(token_str: &str) -> std::result::Result<Self, Self::Err> {
27        let splits: Vec<&str> = token_str.split('.').collect();
28        if splits.len() != 3 {
29            return Err(Error::InvalidFormat);
30        }
31        let (ident_b64u, exp_b64u, sign_b64u) = (splits[0], splits[1], splits[2]);
32
33        Ok(Self {
34            ident: b64u_decode_to_string(ident_b64u).map_err(|_| Error::CannotDecodeIdent)?,
35
36            exp: b64u_decode_to_string(exp_b64u).map_err(|_| Error::CannotDecodeExp)?,
37
38            sign_b64u: sign_b64u.to_string(),
39        })
40    }
41}
42
43impl Display for Token {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(
46            f,
47            "{}.{}.{}",
48            b64u_encode(&self.ident),
49            b64u_encode(&self.exp),
50            self.sign_b64u
51        )
52    }
53}
54
55pub fn generate_web_token(user: &str, salt: Uuid) -> Result<Token> {
56    let config = &auth_config();
57    _generate_token(user, config.TOKEN_DURATION_SEC, salt, &config.TOKEN_KEY)
58}
59
60pub fn validate_web_token(origin_token: &Token, salt: Uuid) -> Result<()> {
61    let config = &auth_config();
62    _validate_token_sign_and_exp(origin_token, salt, &config.TOKEN_KEY)?;
63
64    Ok(())
65}
66
67fn _generate_token(ident: &str, duration_sec: f64, salt: Uuid, key: &[u8]) -> Result<Token> {
68    let ident = ident.to_string();
69    let exp = now_utc_plus_sec_str(duration_sec);
70
71    let sign_b64u = _token_sign_into_b64u(&ident, &exp, salt, key)?;
72
73    Ok(Token {
74        ident,
75        exp,
76        sign_b64u,
77    })
78}
79
80fn _validate_token_sign_and_exp(origin_token: &Token, salt: Uuid, key: &[u8]) -> Result<()> {
81    let new_sign_b64u = _token_sign_into_b64u(&origin_token.ident, &origin_token.exp, salt, key)?;
82
83    if new_sign_b64u != origin_token.sign_b64u {
84        return Err(Error::SignatureNotMatching);
85    }
86
87    let origin_exp = parse_utc(&origin_token.exp).map_err(|_| Error::ExpNotIso)?;
88    let now = now_utc();
89
90    if origin_exp < now {
91        return Err(Error::Expired);
92    }
93
94    Ok(())
95}
96
97fn _token_sign_into_b64u(ident: &str, exp: &str, salt: Uuid, key: &[u8]) -> Result<String> {
98    let content = format!("{}.{}", b64u_encode(ident), b64u_encode(exp));
99
100    let mut hmac_sha512 =
101        Hmac::<Sha512>::new_from_slice(key).map_err(|_| Error::HmacFailNewFromSlice)?;
102
103    hmac_sha512.update(content.as_bytes());
104    hmac_sha512.update(salt.as_bytes());
105
106    let hmac_result = hmac_sha512.finalize();
107    let result_bytes = hmac_result.into_bytes();
108    let result = b64u_encode(result_bytes);
109
110    Ok(result)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use anyhow::Result;
117    use std::thread;
118    use std::time::Duration;
119
120    #[test]
121    fn test_token_display_ok() -> Result<()> {
122        let fx_token_str = "ZngtaWRlbnQtMDE.MjAyMy0wNS0xN1QxNTozMDowMFo.some-sign-b64u-encoded";
123        let fx_token = Token {
124            ident: "fx-ident-01".to_string(),
125            exp: "2023-05-17T15:30:00Z".to_string(),
126            sign_b64u: "some-sign-b64u-encoded".to_string(),
127        };
128
129        assert_eq!(fx_token.to_string(), fx_token_str);
130
131        Ok(())
132    }
133
134    #[test]
135    fn test_token_from_str_ok() -> Result<()> {
136        let fx_token_str = "ZngtaWRlbnQtMDE.MjAyMy0wNS0xN1QxNTozMDowMFo.some-sign-b64u-encoded";
137        let fx_token = Token {
138            ident: "fx-ident-01".to_string(),
139            exp: "2023-05-17T15:30:00Z".to_string(),
140            sign_b64u: "some-sign-b64u-encoded".to_string(),
141        };
142
143        let token: Token = fx_token_str.parse()?;
144
145        assert_eq!(token, fx_token);
146
147        Ok(())
148    }
149
150    #[test]
151    fn test_validate_web_token_ok() -> Result<()> {
152        let fx_user = "user_one";
153        let fx_salt = Uuid::parse_str("f05e8961-d6ad-4086-9e78-a6de065e5453").unwrap();
154        let fx_duration_sec = 0.02;
155        let token_key = &auth_config().TOKEN_KEY;
156        let fx_token = _generate_token(fx_user, fx_duration_sec, fx_salt, token_key)?;
157
158        thread::sleep(Duration::from_millis(10));
159        let res = validate_web_token(&fx_token, fx_salt);
160
161        res?;
162
163        Ok(())
164    }
165
166    #[test]
167    fn test_validate_web_token_err_expired() -> Result<()> {
168        let fx_user = "user_one";
169        let fx_salt = Uuid::parse_str("f05e8961-d6ad-4086-9e78-a6de065e5453").unwrap();
170        let fx_duration_sec = 0.01;
171        let token_key = &auth_config().TOKEN_KEY;
172        let fx_token = _generate_token(fx_user, fx_duration_sec, fx_salt, token_key)?;
173
174        thread::sleep(Duration::from_millis(20));
175        let res = validate_web_token(&fx_token, fx_salt);
176
177        assert!(
178            matches!(res, Err(Error::Expired)),
179            "Should have matched `Err(Error::Expired)` but was `{res:?}`"
180        );
181
182        Ok(())
183    }
184}