stalwart_lib/common/src/auth/oauth/
token.rs1use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncrypt};
8use crate::Server;
9use crate::directory::{PrincipalData, QueryParams};
10use crate::store::{
11 blake3,
12 rand::{Rng, rng},
13};
14use crate::trc::AddContext;
15use crate::utils::codec::leb128::{Leb128Iterator, Leb128Vec};
16use mail_builder::encoders::base64::base64_encode;
17use mail_parser::decoders::base64::base64_decode;
18use std::time::SystemTime;
19
20pub struct TokenInfo {
21 pub grant_type: GrantType,
22 pub account_id: u32,
23 pub client_id: String,
24 pub expiry: u64,
25 pub issued_at: u64,
26 pub expires_in: u64,
27}
28
29const OAUTH_EPOCH: u64 = 946684800; impl Server {
32 pub async fn encode_access_token(
33 &self,
34 grant_type: GrantType,
35 account_id: u32,
36 client_id: &str,
37 expiry_in: u64,
38 ) -> crate::trc::Result<String> {
39 let mut password_hash = String::new();
41
42 if !matches!(grant_type, GrantType::Rsvp) {
43 if client_id.len() > CLIENT_ID_MAX_LEN {
44 return Err(crate::trc::AuthEvent::Error
45 .into_err()
46 .details("Client id too long"));
47 }
48
49 if expiry_in > 3600 {
51 password_hash = self
52 .password_hash(account_id)
53 .await
54 .caused_by(crate::trc::location!())?
55 }
56 }
57
58 let key = &self.core.oauth.oauth_key;
59 let context = format!(
60 "{} {} {} {}",
61 grant_type.as_str(),
62 client_id,
63 account_id,
64 password_hash
65 );
66
67 let issued_at = SystemTime::now()
69 .duration_since(SystemTime::UNIX_EPOCH)
70 .map_or(0, |d| d.as_secs())
71 .saturating_sub(OAUTH_EPOCH); let expiry = issued_at + expiry_in;
73
74 let mut hasher = blake3::Hasher::new();
76 if !password_hash.is_empty() {
77 hasher.update(password_hash.as_bytes());
78 }
79 hasher.update(grant_type.as_str().as_bytes());
80 hasher.update(issued_at.to_be_bytes().as_slice());
81 hasher.update(expiry.to_be_bytes().as_slice());
82 let nonce = hasher
83 .finalize()
84 .as_bytes()
85 .iter()
86 .take(SymmetricEncrypt::NONCE_LEN)
87 .copied()
88 .collect::<Vec<_>>();
89
90 let mut token = SymmetricEncrypt::new(key.as_bytes(), &context)
92 .encrypt(&rng().random::<[u8; RANDOM_CODE_LEN]>(), &nonce)
93 .map_err(|_| {
94 crate::trc::AuthEvent::Error
95 .into_err()
96 .ctx(crate::trc::Key::Reason, "Failed to encrypt token")
97 .caused_by(crate::trc::location!())
98 })?;
99 token.push_leb128(account_id);
100 token.push(grant_type.id());
101 token.push_leb128(issued_at);
102 token.push_leb128(expiry);
103 token.extend_from_slice(client_id.as_bytes());
104
105 Ok(String::from_utf8(base64_encode(&token).unwrap_or_default()).unwrap())
106 }
107
108 pub async fn validate_access_token(
109 &self,
110 expected_grant_type: Option<GrantType>,
111 token_: &str,
112 ) -> crate::trc::Result<TokenInfo> {
113 let token = base64_decode(token_.as_bytes()).ok_or_else(|| {
115 crate::trc::AuthEvent::Error
116 .into_err()
117 .ctx(crate::trc::Key::Reason, "Failed to decode token")
118 .caused_by(crate::trc::location!())
119 .details(token_.to_string())
120 })?;
121 let (account_id, grant_type, issued_at, expiry, client_id) = token
122 .get((RANDOM_CODE_LEN + SymmetricEncrypt::ENCRYPT_TAG_LEN)..)
123 .and_then(|bytes| {
124 let mut bytes = bytes.iter();
125 (
126 bytes.next_leb128()?,
127 GrantType::from_id(bytes.next().copied()?)?,
128 bytes.next_leb128::<u64>()?,
129 bytes.next_leb128::<u64>()?,
130 bytes.copied().map(char::from).collect::<String>(),
131 )
132 .into()
133 })
134 .ok_or_else(|| {
135 crate::trc::AuthEvent::Error
136 .into_err()
137 .ctx(crate::trc::Key::Reason, "Failed to decode token")
138 .caused_by(crate::trc::location!())
139 .details(token_.to_string())
140 })?;
141
142 let now = SystemTime::now()
144 .duration_since(SystemTime::UNIX_EPOCH)
145 .map_or(0, |d| d.as_secs())
146 .saturating_sub(OAUTH_EPOCH); if expiry <= now || issued_at > now {
148 return Err(crate::trc::AuthEvent::TokenExpired.into_err());
149 }
150
151 if expected_grant_type.is_some_and(|g| g != grant_type) {
153 return Err(crate::trc::AuthEvent::Error
154 .into_err()
155 .details("Invalid grant type"));
156 }
157
158 let password_hash = if !matches!(grant_type, GrantType::Rsvp) && expiry - issued_at > 3600 {
160 self.password_hash(account_id).await.map_err(|err| {
161 crate::trc::AuthEvent::Error
162 .into_err()
163 .ctx(crate::trc::Key::Details, err)
164 })?
165 } else {
166 "".into()
167 };
168
169 let key = self.core.oauth.oauth_key.clone();
171 let context = format!(
172 "{} {} {} {}",
173 grant_type.as_str(),
174 client_id,
175 account_id,
176 password_hash
177 );
178
179 let mut hasher = blake3::Hasher::new();
181 if !password_hash.is_empty() {
182 hasher.update(password_hash.as_bytes());
183 }
184 hasher.update(grant_type.as_str().as_bytes());
185 hasher.update(issued_at.to_be_bytes().as_slice());
186 hasher.update(expiry.to_be_bytes().as_slice());
187 let nonce = hasher
188 .finalize()
189 .as_bytes()
190 .iter()
191 .take(SymmetricEncrypt::NONCE_LEN)
192 .copied()
193 .collect::<Vec<_>>();
194
195 SymmetricEncrypt::new(key.as_bytes(), &context)
197 .decrypt(
198 &token[..RANDOM_CODE_LEN + SymmetricEncrypt::ENCRYPT_TAG_LEN],
199 &nonce,
200 )
201 .map_err(|err| {
202 crate::trc::AuthEvent::Error
203 .into_err()
204 .ctx(crate::trc::Key::Details, "Failed to decode token")
205 .caused_by(crate::trc::location!())
206 .reason(err)
207 })?;
208
209 Ok(TokenInfo {
211 grant_type,
212 account_id,
213 client_id,
214 expiry: expiry + OAUTH_EPOCH,
215 issued_at: issued_at + OAUTH_EPOCH,
216 expires_in: expiry - now,
217 })
218 }
219
220 pub async fn password_hash(&self, account_id: u32) -> crate::trc::Result<String> {
221 if account_id != u32::MAX {
222 self.core
223 .storage
224 .directory
225 .query(QueryParams::id(account_id).with_return_member_of(false))
226 .await
227 .caused_by(crate::trc::location!())?
228 .ok_or_else(|| {
229 crate::trc::AuthEvent::Error
230 .into_err()
231 .details("Account no longer exists")
232 })?
233 .data
234 .into_iter()
235 .filter_map(|v| {
236 if let PrincipalData::Password(secret) = v {
237 Some(secret)
238 } else {
239 None
240 }
241 })
242 .next()
243 .ok_or(
244 crate::trc::AuthEvent::Error
245 .into_err()
246 .details("Account does not contain secrets")
247 .caused_by(crate::trc::location!()),
248 )
249 } else if let Some((_, secret)) = &self.core.jmap.fallback_admin {
250 Ok(secret.into())
251 } else {
252 Err(crate::trc::AuthEvent::Error
253 .into_err()
254 .details("Invalid account ID")
255 .caused_by(crate::trc::location!()))
256 }
257 }
258}