bitwarden_core/auth/
jwt_token.rs1use std::str::FromStr;
2
3use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
4use thiserror::Error;
5
6#[derive(serde::Deserialize)]
14pub struct JwtToken {
15 pub exp: u64,
17 pub sub: String,
19 pub email: Option<String>,
21 pub organization: Option<String>,
23 pub premium: Option<bool>,
26 pub scope: Vec<String>,
28}
29
30#[allow(missing_docs)]
32#[derive(Debug, Error)]
33pub enum JwtTokenParseError {
34 #[error("JWT token parse error: {0}")]
35 Parse(#[from] serde_json::Error),
36 #[error("JWT token decode error: {0}")]
37 Decode(#[from] NotB64UrlEncodedError),
38
39 #[error("JWT token has an invalid number of parts")]
40 InvalidParts,
41}
42
43impl FromStr for JwtToken {
44 type Err = JwtTokenParseError;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 let split = s.split('.').collect::<Vec<_>>();
51 if split.len() != 3 {
52 return Err(Self::Err::InvalidParts);
53 }
54 let decoded = B64Url::try_from(split[1])?;
55 Ok(serde_json::from_slice(decoded.as_bytes())?)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use crate::auth::jwt_token::JwtToken;
62
63 #[test]
64 fn can_decode_jwt() {
65 let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQz\
66 g1REJEOUFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJu\
67 YmYiOjE2NzUxMDM1NzcsImV4cCI6MTY3NTEwNzE3NywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI\
68 6IndlYiIsInN1YiI6ImUyNWQzN2YzLWI2MDMtNDBkZS04NGJhLWFmOTYwMTJmNWE0MiIsImF1dGhfdGltZSI6MTY3NT\
69 EwMzU0OSwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoidGVzdEBiaXR3YXJkZW4uY29tI\
70 iwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IkUzNElDWVhRUFRDS01EVldBREZDNktHNDJCQldJRDdJIiwi\
71 bmFtZSI6IlRlc3QiLCJvcmdvd25lciI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImRldml\
72 jZSI6Ijg5Mjg5M2FiLWRkNDMtNDUwYS04NGI1LWFhOWM1YjdiYjJkOCIsImp0aSI6IkEzMkVFNjY5NDdEQzlDNUE2MT\
73 IwRURBRTIwNzc5OUJFIiwiaWF0IjoxNjc1MTAzNTc3LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhb\
74 XIiOlsiQXBwbGljYXRpb24iXX0.AyDkKvjmyaSPQViQSa2sGTKIkDGrUAtDmwpE57K4DDWT0QvwDe7FMktmwiF4LH36\
75 wx_FnpH21VI1pzwJeTHXtaz3niANJtQZjzGFsNAna_95vrsxZC2YizgGlt6mX4YIGmAw9DiYrmaN0BvQOEm_caV_u6f\
76 a30iz9Kvjxf7cpzeZvPEysxGpB3k3TRYTkFUdV43HiXdhXMBhyyOpFU6Fk6yA41y7-8bGYc5mYGknWktmPD9Yx-1xKL\
77 ftFja1SnCoLPWvDeK60lqWZQiT4tZHCYJ7m0bBNCccYHc2Kk2Bo5-UoyDxazPwsqMxeNfjlaUuj3o5N_uQ-4n_gVbeA\
78 qWV2wrel5UhYjWnczMSLBtt9p0W35kkBPt3ZAnRWMtQMPNH04p-_L6cG-Xu6lDksBTwaavcmtnCKG8V91826EiQ8MrF\
79 wGWQRZV6tPKTDAYCgSAZGBY3QDmPGT5BeFcg5Ag_nYYIIifKP-kv10v_N-TOcT3NeGBOUlAZ-9m7iT7Rk3vC--SDZdA\
80 U5turoBFiiPL2XXfAjM7P0r7J91gfXc0FaD6I2jDxOmym5h7Yn5phLsbC2NlIXkZp54dKHICenPl4ve6ndDIJacVeS5\
81 f3LEddAPV8cAFza4DjA8pZJLFrMyRvMXcL_PjKF8qPVzqVWh03lfJ4clOIxR2gOuWIc902Y5E";
82
83 let token: JwtToken = jwt.parse().unwrap();
84 assert_eq!(token.exp, 1675107177);
85 assert_eq!(token.sub, "e25d37f3-b603-40de-84ba-af96012f5a42");
86 assert_eq!(token.email.as_deref(), Some("[email protected]"));
87 assert_eq!(token.organization.as_deref(), None);
88 assert_eq!(token.premium, Some(false));
89 assert_eq!(token.scope[0], "api");
90 assert_eq!(token.scope[1], "offline_access");
91 }
92
93 #[test]
98 fn can_decode_premium_claim() {
99 let payload = bitwarden_encoding::B64Url::from(
102 br#"{"sub":"e25d37f3-b603-40de-84ba-af96012f5a42","email":"[email protected]","premium":true,"scope":["api"],"exp":1675107177}"#
103 .as_slice(),
104 );
105 let jwt = format!("aaaa.{payload}.bbbb");
106
107 let token: JwtToken = jwt.parse().unwrap();
108 assert_eq!(token.premium, Some(true));
109 assert_eq!(token.email.as_deref(), Some("[email protected]"));
110 }
111
112 #[test]
115 fn premium_absent_decodes_to_none() {
116 let payload = bitwarden_encoding::B64Url::from(
117 br#"{"sub":"e25d37f3-b603-40de-84ba-af96012f5a42","scope":["api"],"exp":1675107177}"#
118 .as_slice(),
119 );
120 let jwt = format!("aaaa.{payload}.bbbb");
121
122 let token: JwtToken = jwt.parse().unwrap();
123 assert_eq!(token.premium, None);
124 }
125}