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}