Skip to main content

stalwart_lib/common/src/auth/oauth/
token.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use 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; // Jan 1, 2000
30
31impl 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        // Build context
40        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            // Include password hash if expiration is over 1 hour
50            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        // Set expiration time
68        let issued_at = SystemTime::now()
69            .duration_since(SystemTime::UNIX_EPOCH)
70            .map_or(0, |d| d.as_secs())
71            .saturating_sub(OAUTH_EPOCH); // Jan 1, 2000
72        let expiry = issued_at + expiry_in;
73
74        // Calculate nonce
75        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        // Encrypt random bytes
91        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        // Base64 decode token
114        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        // Validate expiration
143        let now = SystemTime::now()
144            .duration_since(SystemTime::UNIX_EPOCH)
145            .map_or(0, |d| d.as_secs())
146            .saturating_sub(OAUTH_EPOCH); // Jan 1, 2000
147        if expiry <= now || issued_at > now {
148            return Err(crate::trc::AuthEvent::TokenExpired.into_err());
149        }
150
151        // Validate grant type
152        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        // Obtain password hash
159        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        // Build context
170        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        // Calculate nonce
180        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        // Decrypt
196        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        // Success
210        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}