beep_auth/domain/models/
token.rs

1use base64::{Engine, engine::general_purpose};
2use serde::{Deserialize, Serialize};
3use tracing::error;
4
5use crate::domain::models::{AuthError, Claims, Jwt};
6
7#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
8pub struct Token(pub String);
9
10impl Token {
11    pub fn new(token: impl Into<String>) -> Self {
12        Self(token.into())
13    }
14
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18
19    pub fn decode_manual(&self) -> Result<Jwt, AuthError> {
20        let t: Vec<&str> = self.0.split(".").collect();
21
22        if t.len() != 3 {
23            return Err(AuthError::InvalidToken {
24                message: "JWT must have 3 parts separated by dots".to_string(),
25            });
26        }
27
28        let payload = t[1];
29
30        let decoded = general_purpose::URL_SAFE_NO_PAD
31            .decode(payload)
32            .map_err(|e| {
33                error!("failed to decode JWT payload: {:?}", e);
34                AuthError::InvalidToken {
35                    message: format!("failed to decode JWT payload: {:?}", e),
36                }
37            })?;
38
39        let payload_str = String::from_utf8(decoded).map_err(|e| {
40            error!("failed to decode JWT payload: {:?}", e);
41            AuthError::InvalidToken {
42                message: format!("Failed to convert payload to UTF-8: {}", e),
43            }
44        })?;
45
46        let claims: Claims = serde_json::from_str(&payload_str).map_err(|e| {
47            error!("failed to deserialize JWT claims: {:?}", e);
48            AuthError::InvalidToken {
49                message: format!("failed to deserialize JWT claims: {:?}", e),
50            }
51        })?;
52
53        Ok(Jwt {
54            claims,
55            token: self.clone(),
56        })
57    }
58
59    pub fn extract_claims(&self) -> Result<Claims, AuthError> {
60        let jwt = self.decode_manual()?;
61        Ok(jwt.claims)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use jsonwebtoken::{EncodingKey, Header, encode};
68    use serde_json::json;
69
70    use crate::domain::models::{AuthError, Token};
71
72    fn create_test_jwt(claims: serde_json::Value) -> String {
73        let header = Header::default();
74        let key = EncodingKey::from_secret("secret".as_ref());
75        encode(&header, &claims, &key).unwrap()
76    }
77
78    #[test]
79    fn test_token_new_and_as_str() {
80        let token_str = "test.token.here";
81        let token = Token::new(token_str);
82
83        assert_eq!(token.as_str(), token_str);
84    }
85
86    #[test]
87    fn test_token_decode_manual_success() {
88        let test_claims = json!({
89            "sub": "user-123",
90            "iss": "https://auth.beep.com",
91            "aud": "beep-api",
92            "exp": 1735689600,
93            "email": "john.doe@example.com",
94            "email_verified": true,
95            "scope": "read:messages write:messages",
96            "preferred_username": "johndoe",
97            "name": "John Doe"
98        });
99
100        let jwt_token = create_test_jwt(test_claims);
101        let token = Token::new(jwt_token.clone());
102        let result = token.decode_manual();
103
104        assert!(result.is_ok());
105        let jwt = result.unwrap();
106        assert_eq!(jwt.claims.sub.0, "user-123");
107        assert_eq!(jwt.claims.iss, "https://auth.beep.com");
108        assert_eq!(jwt.token.0, jwt_token);
109    }
110
111    #[test]
112    fn test_token_extract_claims() {
113        let test_claims = json!({
114            "sub": "user-456",
115            "iss": "https://auth.beep.com",
116            "email": "jane@example.com",
117            "email_verified": true,
118            "scope": "admin",
119            "preferred_username": "jane"
120        });
121
122        let jwt_token = create_test_jwt(test_claims);
123        let token = Token::new(jwt_token);
124        let result = token.extract_claims();
125
126        assert!(result.is_ok());
127        let claims = result.unwrap();
128        assert_eq!(claims.sub.0, "user-456");
129        assert_eq!(claims.email, Some("jane@example.com".to_string()));
130    }
131
132    #[test]
133    fn test_token_decode_manual_invalid_format() {
134        let token = Token::new("invalid.token".to_string());
135        let result = token.decode_manual();
136
137        assert!(result.is_err());
138        match result.unwrap_err() {
139            AuthError::InvalidToken { message } => {
140                assert!(message.contains("3 parts"));
141            }
142            _ => panic!("Expected InvalidToken error"),
143        }
144    }
145
146    #[test]
147    fn test_token_decode_manual_with_real_token() {
148        let token_str = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiaE9ZRENETC14TFhyWVRGZERTMmlwMzdHdHhFNlpUVVI4a2swSm9CVDhzIn0.eyJleHAiOjE3NjExMTc5NTYsImlhdCI6MTc2MTExNzg5NiwianRpIjoib25ydHJvOjJhMjNjYjkyLTc1MTktYzgzYS0wMGM2LWIxNDQyOTlkYjE1NSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwMC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjE0NDM0Y2JhLThmMzItNDliYi1hMzllLTgzNzhhN2NkZGVhMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaSIsInNpZCI6ImY2YjUwZWY2LTJlNjItNjAxNS1lNTJjLTA5NzA4NWUyYTAxOCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTmF0aGFlbCBCb25uYWwiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJuYXRoYWVsIiwiZ2l2ZW5fbmFtZSI6Ik5hdGhhZWwiLCJmYW1pbHlfbmFtZSI6IkJvbm5hbCIsImVtYWlsIjoibmF0aGFlbEBib25uYWwuY2xvdWQifQ.ApKQsnjT2gCgqngCndHTNU2W9YJzuHGHRLk4OE-_b4Sk650vSUS0AhMWPuAgEwVjLm2y8UpOJ_64BXDcnQMZzKHNo2_xj5c8P8glvBM-02YJlR_ssbUlReJPvLLKzwFTPdKF_FDsEIXkroV-ds8aU5OmOX8emdxb79XzdHkaWbl13IErHqMnRMsAvh742ZQeCqbedr8R3uH6V5qbbNu7H9kTf2EGX7G66rfpY-Zl8EyR4fWCVwjVLr_5tLsUFteajADf2RtW9dZRsUW9M9g9WIzT_tNdsTQhBj1q3kHkwhhC6hVVz2VaLNgYKikLu8QDfGy4BZ6nHZobrq4eKr3HQg";
149
150        let token = Token::new(token_str.to_string());
151        let result = token.decode_manual();
152
153        assert!(result.is_ok());
154        let jwt = result.unwrap();
155        assert_eq!(jwt.claims.sub.0, "14434cba-8f32-49bb-a39e-8378a7cddea3");
156        assert_eq!(jwt.claims.email, Some("nathael@bonnal.cloud".to_string()));
157    }
158}