1use std::{sync::Arc, time::SystemTime};
6
7use anyhow::bail;
8use blake2::digest::{FixedOutput, Mac};
9use bytes::Bytes;
10use chacha20poly1305::{
11 XChaCha20Poly1305, XNonce,
12 aead::{Aead, AeadCore, OsRng},
13};
14use flexbuffers::VectorReader;
15use ordinary_config::{
16 AccessTokenConfig, AuthConfig, ClientPasswordHash, HmacTokenAlgorithm, HmacTokenConfig,
17 InviteConfig, InviteMode, MfaConfig, PasswordConfig, PasswordProtocol, RefreshTokenConfig,
18 SignedTokenAlgorithm, SignedTokenConfig, TotpAlgorithm, TotpConfig,
19};
20use tracing::instrument;
21use uuid::Uuid;
22use x25519_dalek::{EphemeralSecret, PublicKey};
23
24use saferlmdb::{
25 self as lmdb, Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put,
26};
27
28pub use opaque_ke::ServerSetup;
29
30use totp_rs::{Algorithm, Secret, TOTP};
31
32use crate::{
33 DefaultCipherSuite, EXP_LEN, SIG_LEN, ZEROED_KEY,
34 keys::decrypt_256_bit_key,
35 recovery::{check_code, consume_code},
36 token::generate_hmac,
37 validate_account,
38};
39use crate::{
40 keys::{KeyAlg, decrypt_ed25519_pair, generate_256_bit_key, generate_ed25519_pair},
41 token::extract_signed_no_check,
42};
43
44use sha2::{Digest, Sha256};
45
46pub struct Auth {
47 pub domain: String,
48
49 pub config: AuthConfig,
50
51 totp_alg: Algorithm,
52
53 access_token_key: [u8; 32],
54 reset_password_token_key: [u8; 32],
55 invite_token_key: [u8; 32],
56
57 token_signing_key: [u8; 32],
58 pub token_verifying_key: [u8; 32],
59
60 encryption_key: [u8; 32],
61
62 env: Arc<Environment>,
64
65 account_db: Arc<Database<'static>>,
68
69 state_db: Arc<Database<'static>>,
71
72 invite_db: Arc<Database<'static>>,
74
75 recovery_db: Arc<Database<'static>>,
77}
78
79type InviteClaimsValidator = fn(&str, &VectorReader<&[u8]>) -> anyhow::Result<bool>;
80
81impl Auth {
82 #[allow(clippy::too_many_lines)]
83 pub fn new(
84 domain: String,
85 config: Option<AuthConfig>,
86
87 encryption_key: [u8; 32],
88
89 env: Arc<Environment>,
90 ) -> anyhow::Result<Self> {
91 let account_db = Arc::new(Database::open(
92 env.clone(),
93 Some("user"),
94 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
95 )?);
96
97 let state_db = Arc::new(Database::open(
98 env.clone(),
99 Some("auth"),
100 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
101 )?);
102
103 let invite_db = Arc::new(Database::open(
104 env.clone(),
105 Some("invite"),
106 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
107 )?);
108
109 let recovery_db = Arc::new(Database::open(
110 env.clone(),
111 Some("recovery"),
112 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
113 )?);
114
115 let key_db = Arc::new(Database::open(
117 env.clone(),
118 Some("key"),
119 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
120 )?);
121
122 let mut config = config.unwrap_or_else(|| AuthConfig {
123 password: PasswordConfig {
124 protocol: PasswordProtocol::Opaque,
125 },
126 mfa: MfaConfig {
127 totp: TotpConfig {
128 template: None,
129 algorithm: TotpAlgorithm::Sha1,
130 },
131 },
132 hmac_token: HmacTokenConfig {
133 algorithm: HmacTokenAlgorithm::HmacBlake2b256,
134 rotation: 60 * 60 * 24 * 7,
135 },
136 signed_token: SignedTokenConfig {
137 algorithm: SignedTokenAlgorithm::DsaEd25519,
138 rotation: 60 * 60 * 24 * 30,
139 },
140 refresh_token: RefreshTokenConfig {
141 lifetime: 60 * 60 * 24 * 7,
142 },
143 access_token: AccessTokenConfig {
144 lifetime: 60 * 60 * 24,
145
146 claims: vec![],
147 },
148 client_hash: ClientPasswordHash::Sha256,
149 cookies_enabled: true,
150 invite: None,
151 });
152
153 config.access_token.claims.sort_by(|a, b| a.idx.cmp(&b.idx));
154
155 let txn = WriteTransaction::new(env.clone())?;
156
157 let (token_hmac_key, token_signing_key, token_verifying_key) = {
158 use chacha20poly1305::aead::KeyInit;
159
160 let mut access = txn.access();
161 let mut rng = OsRng;
162
163 let cipher = XChaCha20Poly1305::new(&encryption_key.into());
164
165 let hmac_key = if let Ok(kid_key) =
166 access.get::<[u8], [u8]>(&key_db, &[KeyAlg::Blake2SMac256.as_byte()])
167 {
168 let (kid, curr_key) = decrypt_256_bit_key(&cipher, kid_key)?;
169
170 if let Some(timestamp) = kid.get_timestamp() {
171 if (timestamp.to_unix().0 + u64::from(config.hmac_token.rotation))
172 < SystemTime::now()
173 .duration_since(SystemTime::UNIX_EPOCH)?
174 .as_secs()
175 {
176 let (kid_key, new_key, _kid) = generate_256_bit_key(&cipher, &mut rng)?;
177
178 access.put(
179 &key_db,
180 &[KeyAlg::Blake2SMac256.as_byte()],
181 &kid_key,
182 &put::Flags::empty(),
183 )?;
184
185 new_key
186 } else {
187 curr_key
188 }
189 } else {
190 curr_key
191 }
192 } else {
193 let (kid_key, hmac_key, _kid) = generate_256_bit_key(&cipher, &mut rng)?;
194
195 access.put(
196 &key_db,
197 &[KeyAlg::Blake2SMac256.as_byte()],
198 &kid_key,
199 &put::Flags::empty(),
200 )?;
201
202 hmac_key
203 };
204
205 let (signing_key, verifying_key) = if let Ok(kid_keys) =
206 access.get::<[u8], [u8]>(&key_db, &[KeyAlg::Ed25519Pair.as_byte()])
207 {
208 let (kid, curr_signing_key, curr_verifying_key) =
209 decrypt_ed25519_pair(&cipher, kid_keys)?;
210
211 if let Some(timestamp) = kid.get_timestamp() {
212 if (timestamp.to_unix().0 + u64::from(config.signed_token.rotation))
213 < SystemTime::now()
214 .duration_since(SystemTime::UNIX_EPOCH)?
215 .as_secs()
216 {
217 let (kid_keys, new_signing_key, new_verifying_key, _kid) =
218 generate_ed25519_pair(&cipher, &mut rng)?;
219
220 access.put(
221 &key_db,
222 &[KeyAlg::Ed25519Pair.as_byte()],
223 &kid_keys,
224 &put::Flags::empty(),
225 )?;
226
227 (new_signing_key, new_verifying_key)
228 } else {
229 (curr_signing_key, curr_verifying_key)
230 }
231 } else {
232 (curr_signing_key, curr_verifying_key)
233 }
234 } else {
235 let (kid_keys, signing_key, verifying_key, _kid) =
236 generate_ed25519_pair(&cipher, &mut rng)?;
237
238 access.put(
239 &key_db,
240 &[KeyAlg::Ed25519Pair.as_byte()],
241 &kid_keys,
242 &put::Flags::empty(),
243 )?;
244
245 (signing_key, verifying_key)
246 };
247
248 (hmac_key, signing_key, verifying_key)
249 };
250
251 txn.commit()?;
252
253 let access_token_key: [u8; 32] =
260 blake2::Blake2sMac256::new_from_slice(&token_hmac_key[..])?
261 .chain_update(b"access")
262 .finalize_fixed()
263 .into();
264
265 let reset_password_token_key: [u8; 32] =
266 blake2::Blake2sMac256::new_from_slice(&token_hmac_key[..])?
267 .chain_update(b"reset_password")
268 .finalize_fixed()
269 .into();
270
271 let invite_token_key: [u8; 32] =
272 blake2::Blake2sMac256::new_from_slice(&token_hmac_key[..])?
273 .chain_update(b"invite")
274 .finalize_fixed()
275 .into();
276
277 Ok(Self {
278 domain,
279
280 totp_alg: match &config.mfa.totp.algorithm {
281 TotpAlgorithm::Sha1 => Algorithm::SHA1,
282 },
283 config,
284
285 access_token_key,
286 reset_password_token_key,
287 invite_token_key,
288
289 token_signing_key,
290 token_verifying_key,
291
292 encryption_key,
293
294 env,
295 state_db,
296 account_db,
297 invite_db,
298 recovery_db,
299 })
300 }
301
302 const MIN_REGISTRATION_START_LEN: usize = 1 + 32;
304
305 #[allow(clippy::type_complexity)]
307 #[instrument(
308 skip(
309 self,
310 payload,
311 existing_account,
312 invite_claims_validator,
313 checked_claims
314 ),
315 err
316 )]
317 pub fn registration_start(
318 &self,
319 payload: Bytes,
320 existing_account: Option<&str>,
321 invite_claims_validator: Option<InviteClaimsValidator>,
322 checked_claims: Option<(VectorReader<&[u8]>, &[u8])>,
323 ) -> anyhow::Result<Bytes> {
324 use chacha20poly1305::aead::KeyInit;
325
326 if payload.len() < Self::MIN_REGISTRATION_START_LEN {
327 bail!("payload too small");
328 }
329
330 let account_len = payload[0] as usize;
331
332 if account_len != payload.len() - Self::MIN_REGISTRATION_START_LEN {
333 bail!("invalid format");
334 }
335
336 let raw_account = &payload[1..=account_len];
337 let raw_account_str = std::str::from_utf8(raw_account)?;
338
339 let account_str = validate_account(raw_account_str)?;
340 let account = account_str.as_bytes();
341
342 if let Some(existing_account) = existing_account
343 && existing_account != account_str
344 {
345 bail!("account mismatch");
346 }
347
348 tracing::info!(account = %account_str);
349
350 let invite_claims = if self.config.invite.is_some() && existing_account.is_none() {
351 let Some((claims_vec, claims)) = checked_claims else {
352 bail!("no checked claims")
353 };
354
355 if let Some(validator) = invite_claims_validator {
356 let res = validator(&account_str, &claims_vec)?;
357
358 if !res {
359 bail!("failed to validate invite token claims");
360 }
361 }
362
363 Some(claims)
364 } else {
365 None
366 };
367
368 let txn = WriteTransaction::new(self.env.clone())?;
369
370 let out = {
371 let mut access = txn.access();
372
373 if existing_account.is_none() {
374 if access.get::<[u8], [u8]>(&self.account_db, account).is_ok() {
375 bail!("account has already been registered.");
376 }
377
378 if access.get::<[u8], [u8]>(&self.state_db, account).is_ok() {
379 bail!("account has already been registered.");
380 }
381 }
382
383 let client_start = &payload[account_len + 1..account_len + 1 + 32];
384
385 let mut rng = OsRng;
386 let opaque = ServerSetup::<DefaultCipherSuite>::new(&mut rng);
387
388 let out = crate::registration::server_start(&opaque, account, client_start)?;
389
390 let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
391 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
392
393 let mut encrypted_stored = match cipher.encrypt(&nonce, opaque.serialize().as_ref()) {
394 Ok(v) => v,
395 Err(err) => bail!("{err}"),
396 };
397 encrypted_stored.extend_from_slice(&nonce);
398
399 if let Some(invite_claims) = invite_claims {
400 encrypted_stored.extend_from_slice(invite_claims);
401 }
402
403 access.put(
404 &self.state_db,
405 account,
406 &encrypted_stored,
407 &put::Flags::empty(),
408 )?;
409
410 out
411 };
412
413 txn.commit()?;
414
415 Ok(out)
416 }
417
418 const MIN_REGISTRATION_FINISH_LEN: usize = 1 + 32 + 192;
420
421 #[allow(clippy::type_complexity, clippy::too_many_lines)]
422 #[instrument(skip(self, payload, existing_account), err)]
424 pub fn registration_finish(
425 &self,
426 payload: Bytes,
427 existing_account: Option<&str>,
428 ) -> anyhow::Result<(Bytes, Vec<u8>, Option<Bytes>)> {
429 use chacha20poly1305::aead::KeyInit;
430
431 if payload.len() < Self::MIN_REGISTRATION_FINISH_LEN {
432 bail!("payload too small");
433 }
434
435 let account_len = payload[0] as usize;
436
437 if account_len != payload.len() - Self::MIN_REGISTRATION_FINISH_LEN {
438 bail!("invalid format");
439 }
440
441 let raw_account = &payload[1..=account_len];
442 let raw_account_str = std::str::from_utf8(raw_account)?;
443
444 let account_str = validate_account(raw_account_str)?;
445 let account = account_str.as_bytes();
446
447 if let Some(existing_account) = existing_account
448 && existing_account != account_str
449 {
450 bail!("account mismatch");
451 }
452
453 tracing::info!(account = %account_str);
454
455 let public_key: [u8; 32] = payload[account_len + 1..account_len + 33].try_into()?;
456
457 if public_key == ZEROED_KEY {
458 bail!("public key cannot be all 0s");
459 }
460
461 let client_finish = &payload[account_len + 33..account_len + 33 + 192];
462
463 let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
464
465 let totp_nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
466 let password_file_nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
467
468 let txn = WriteTransaction::new(self.env.clone())?;
469
470 let (mut totp_mfa_secret, recovery_codes, invite_claims) = {
471 let mut invite_claims = None;
472
473 let mut access = txn.access();
474
475 let (totp_mfa_secret, recovery_codes) = if existing_account.is_none() {
476 if access.get::<[u8], [u8]>(&self.account_db, account).is_ok() {
477 bail!("account has already been registered");
478 }
479
480 let password_file = crate::registration::server_finish(client_finish)?;
481
482 let mut password_file =
483 match cipher.encrypt(&password_file_nonce, password_file.as_ref()) {
484 Ok(v) => v,
485 Err(err) => bail!("{err}"),
486 };
487 password_file.extend_from_slice(&password_file_nonce);
488
489 let totp_mfa_secret = Secret::generate_secret().to_bytes()?;
491
492 let mut encrypted_totp_mfa_secret =
493 match cipher.encrypt(&totp_nonce, totp_mfa_secret.as_ref()) {
494 Ok(v) => v,
495 Err(err) => bail!("{err}"),
496 };
497 encrypted_totp_mfa_secret.extend_from_slice(&totp_nonce);
498
499 password_file.splice(0..0, encrypted_totp_mfa_secret.iter().copied());
500
501 let serialized_opaque_and_invite_claims =
502 access.get::<[u8], [u8]>(&self.state_db, account)?;
503
504 if serialized_opaque_and_invite_claims.len() > 168 {
505 invite_claims = Some(Bytes::copy_from_slice(
506 &serialized_opaque_and_invite_claims[168..],
507 ));
508 }
509
510 password_file.splice(
511 0..0,
512 serialized_opaque_and_invite_claims[0..168].iter().copied(),
513 );
514
515 let mut empty_claims_builder =
516 flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
517 let empty_claims_vec = empty_claims_builder.start_vector();
518 empty_claims_vec.end_vector();
519
520 password_file.extend_from_slice(empty_claims_builder.view());
521
522 let (stored_recovery_codes, recovery_codes) = crate::recovery::generate_codes()?;
523
524 access.put(
525 &self.recovery_db,
526 account,
527 &stored_recovery_codes[..],
528 &put::Flags::empty(),
529 )?;
530
531 access.put(
532 &self.account_db,
533 account,
534 &password_file,
535 &put::Flags::empty(),
536 )?;
537
538 (totp_mfa_secret, recovery_codes)
539 } else {
540 let account_record: &[u8] = access.get(&self.account_db, account)?;
541
542 let password_file = crate::registration::server_finish(client_finish)?;
543
544 let mut password_file =
545 match cipher.encrypt(&password_file_nonce, password_file.as_ref()) {
546 Ok(v) => v,
547 Err(err) => bail!("{err}"),
548 };
549 password_file.extend_from_slice(&password_file_nonce);
550
551 password_file.splice(0..0, account_record[168..228].iter().copied());
552
553 let serialized_opaque_and_invite_claims =
554 access.get::<[u8], [u8]>(&self.state_db, account)?;
555
556 if serialized_opaque_and_invite_claims.len() > 168 {
557 invite_claims = Some(Bytes::copy_from_slice(
558 &serialized_opaque_and_invite_claims[168..],
559 ));
560 }
561
562 password_file.splice(
563 0..0,
564 serialized_opaque_and_invite_claims[0..168].iter().copied(),
565 );
566
567 password_file.extend_from_slice(&account_record[460..]);
568
569 access.put(
570 &self.account_db,
571 account,
572 &password_file,
573 &put::Flags::empty(),
574 )?;
575
576 (vec![], String::new())
577 };
578
579 access.del_key(&self.state_db, account)?;
580
581 (totp_mfa_secret, recovery_codes, invite_claims)
582 };
583
584 txn.commit()?;
585
586 if existing_account.is_none() {
587 let public_key = PublicKey::from(public_key);
588 let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
589 let ephemeral_public_key = PublicKey::from(&ephemeral_secret);
590
591 let shared_secret = ephemeral_secret.diffie_hellman(&public_key);
592
593 if !shared_secret.was_contributory() {
594 bail!("non-contributory shared secret");
595 }
596
597 let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
598 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
599
600 totp_mfa_secret.extend_from_slice(recovery_codes.as_bytes());
601
602 let mut encrypted_secret = match cipher.encrypt(&nonce, totp_mfa_secret.as_ref()) {
603 Ok(es) => es,
604 Err(err) => bail!("{err}"),
605 };
606 encrypted_secret.extend_from_slice(&nonce);
607 encrypted_secret.extend_from_slice(ephemeral_public_key.as_bytes());
608
609 Ok((
610 Bytes::from(encrypted_secret),
611 account.to_vec(),
612 invite_claims,
613 ))
614 } else {
615 Ok((Bytes::new(), account.to_vec(), invite_claims))
616 }
617 }
618
619 const MIN_LOGIN_START_LEN: usize = 1 + 96;
621
622 #[instrument(skip(self, payload), err)]
624 pub fn login_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
625 if payload.len() < Self::MIN_LOGIN_START_LEN {
626 bail!("payload too small");
627 }
628
629 let account_len = payload[0] as usize;
630
631 if account_len != payload.len() - Self::MIN_LOGIN_START_LEN {
632 bail!("invalid format");
633 }
634
635 let raw_account = &payload[1..=account_len];
636 let raw_account_str = std::str::from_utf8(raw_account)?;
637
638 let account_str = validate_account(raw_account_str)?;
639 let account = account_str.as_bytes();
640
641 tracing::info!(account = %account_str);
642
643 let client_start = &payload[account_len + 1..account_len + 1 + 96];
644
645 let txn = WriteTransaction::new(self.env.clone())?;
646
647 let message = {
648 use chacha20poly1305::aead::KeyInit;
649
650 let mut access = txn.access();
651
652 let account_record: &[u8] = access.get(&self.account_db, account)?;
653
654 let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
655
656 let nonce = XNonce::from_slice(&account_record[144..168]);
657 let serialized_opaque = match cipher.decrypt(nonce, &account_record[0..144]) {
658 Ok(v) => v,
659 Err(err) => bail!("{err}"),
660 };
661
662 let opaque = match ServerSetup::<DefaultCipherSuite>::deserialize(&serialized_opaque) {
663 Ok(o) => o,
664 Err(e) => bail!("{e}"),
665 };
666
667 let nonce = XNonce::from_slice(&account_record[436..460]);
668 let password_file = match cipher.decrypt(nonce, &account_record[228..436]) {
669 Ok(v) => v,
670 Err(err) => bail!("{err}"),
671 };
672
673 let (state, message) =
674 crate::login::server_start(&opaque, account, &password_file, client_start)?;
675
676 access.put(&self.state_db, account, &state[..], &put::Flags::empty())?;
677
678 message
679 };
680
681 txn.commit()?;
682
683 Ok(message)
684 }
685
686 const MIN_LOGIN_FINISH_LEN: usize = 1 + 32 + 16 + 24 + 64;
688
689 #[instrument(skip(self, payload, check_mfa), err)]
691 #[allow(clippy::type_complexity)]
692 pub fn login_finish(
693 &self,
694 payload: Bytes,
695 check_mfa: bool,
696 ) -> anyhow::Result<(Bytes, [u8; 32], Option<Bytes>)> {
697 let payload_len = payload.len();
698
699 if payload_len < Self::MIN_LOGIN_FINISH_LEN {
700 bail!("payload is too small");
701 }
702
703 let account_len = payload[0] as usize;
704
705 if payload_len != account_len + Self::MIN_LOGIN_FINISH_LEN
706 && payload_len != account_len + Self::MIN_LOGIN_FINISH_LEN + 32
707 {
708 bail!("invalid format");
709 }
710
711 let raw_account = &payload[1..=account_len];
712 let raw_account_str = std::str::from_utf8(raw_account)?;
713
714 let account_str = validate_account(raw_account_str)?;
715 let account = account_str.as_bytes();
716
717 tracing::info!(account = %account_str);
718
719 let encrypted_mfa_code = &payload[account_len + 1..payload_len - 24 - 64];
720 let mfa_code_nonce = &payload[payload_len - 24 - 64..payload_len - 64];
721 let client_finish = &payload[payload_len - 64..];
722
723 let txn = WriteTransaction::new(self.env.clone())?;
724
725 let (encrypted_refresh_token, key, verifier) = {
726 let mut access = txn.access();
727
728 let account_record: &[u8] = access.get(&self.account_db, account)?;
729
730 let cipher = {
731 use chacha20poly1305::aead::KeyInit;
732 XChaCha20Poly1305::new(&self.encryption_key.into())
733 };
734
735 let nonce = XNonce::from_slice(&account_record[204..228]);
736 let mfa_secret = match cipher.decrypt(nonce, &account_record[168..204]) {
737 Ok(v) => v,
738 Err(err) => bail!("{err}"),
739 };
740
741 let mfa_hash = if check_mfa {
742 let mfa_code = TOTP::new(
743 self.totp_alg,
744 6,
745 1,
746 30,
747 Secret::Raw(mfa_secret).to_bytes()?,
748 Some(self.domain.clone()),
749 std::str::from_utf8(account)?.to_string(),
750 )?
751 .generate_current()?;
752
753 let mut mfa_input = self.domain.as_bytes().to_vec();
754 mfa_input.extend_from_slice(account);
755 mfa_input.extend_from_slice(mfa_code.as_bytes());
756
757 match &self.config.client_hash {
758 ClientPasswordHash::Sha256 => {
759 let mut hasher = Sha256::new();
760 hasher.update(&mfa_input);
761 hasher.finalize().to_vec()
762 }
763 }
764 } else {
765 vec![0u8; 32]
766 };
767
768 let server_start: &[u8] = access.get(&self.state_db, account)?;
769
770 let (encrypted_refresh_token, key, verifier) = crate::login::server_finish(
771 self,
772 account,
773 &mfa_hash[..],
774 encrypted_mfa_code,
775 mfa_code_nonce,
776 client_finish,
777 server_start,
778 )?;
779
780 access.del_key(&self.state_db, account)?;
781
782 (encrypted_refresh_token, key, verifier)
783 };
784
785 txn.commit()?;
786 Ok((encrypted_refresh_token, key, verifier))
787 }
788
789 #[instrument(skip(self, payload), err)]
790 pub fn reset_password_login_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
791 self.login_start(payload)
792 }
793
794 #[instrument(skip(self, payload), err)]
795 pub fn reset_password_login_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
796 use chacha20poly1305::aead::KeyInit;
797
798 let (_, key, verifier) = self.login_finish(payload.clone(), true)?;
799
800 let account_len = (*payload.first().expect("payload already checked")) as usize;
801 let account_str = std::str::from_utf8(&payload[1..=account_len])?;
802
803 let mut reset_password_claims =
804 flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
805 let mut reset_password_claims_vec = reset_password_claims.start_vector();
806
807 let token_uuid = Uuid::now_v7();
808 let token_uuid_bytes = token_uuid.as_bytes();
809
810 reset_password_claims_vec.push(flexbuffers::Blob(&token_uuid_bytes[..]));
811 reset_password_claims_vec.push(self.domain.as_str());
812 reset_password_claims_vec.push(account_str);
813
814 if let Some(verifier) = verifier {
815 reset_password_claims_vec.push(flexbuffers::Blob(&verifier[..]));
816 }
817
818 reset_password_claims_vec.end_vector();
819
820 let password_reset_token = generate_hmac(
821 reset_password_claims.view(),
822 60 * 15,
823 &self.reset_password_token_key,
824 )?;
825
826 let cipher = XChaCha20Poly1305::new(&key.into());
827 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
828 let mut encrypted_password_reset_token =
829 match cipher.encrypt(&nonce, password_reset_token.as_ref()) {
830 Ok(et) => et,
831 Err(err) => bail!("{err}"),
832 };
833
834 encrypted_password_reset_token.extend_from_slice(&nonce);
835
836 Ok(Bytes::copy_from_slice(&encrypted_password_reset_token))
837 }
838
839 #[instrument(skip(self, payload, token), err)]
840 pub fn reset_password_registration_start(
841 &self,
842 payload: Bytes,
843 token: &[u8],
844 ) -> anyhow::Result<Bytes> {
845 let account_str = self.verify_password_reset_token(token)?;
846 self.registration_start(payload, Some(account_str), None, None)
847 }
848
849 #[instrument(skip(self, payload), err)]
850 pub fn reset_password_registration_finish(
851 &self,
852 payload: Bytes,
853 token: &[u8],
854 ) -> anyhow::Result<()> {
855 let account_str = self.verify_password_reset_token(token)?;
856 self.registration_finish(payload, Some(account_str))?;
857
858 Ok(())
859 }
860
861 #[instrument(skip(self, payload), err)]
862 pub fn forgot_password_start(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
863 let payload_len = payload.len();
864 if payload_len < 33 {
865 bail!("invalid payload");
866 }
867
868 let recovery_code = payload.split_off(payload_len - 32);
869
870 let account_len = (*payload.first().expect("payload already checked")) as usize;
871 if account_len > payload.len() - 1 {
872 bail!("invalid format");
873 }
874
875 let account = Bytes::copy_from_slice(&payload[1..=account_len]);
876 let account_str = std::str::from_utf8(&account[..])?;
877
878 let txn = ReadTransaction::new(self.env.clone())?;
879
880 let access = txn.access();
881
882 let read_recovery_codes = access.get(&self.recovery_db, &account[..])?;
883
884 let is_valid_recovery_code = check_code(&recovery_code[..], read_recovery_codes)?;
885
886 if !is_valid_recovery_code {
887 bail!("invalid recovery code");
888 }
889
890 self.registration_start(payload, Some(account_str), None, None)
891 }
892
893 #[instrument(skip(self, payload), err)]
894 pub fn forgot_password_finish(&self, mut payload: Bytes) -> anyhow::Result<()> {
895 let payload_len = payload.len();
896 if payload_len < 12 {
897 bail!("invalid payload");
898 }
899
900 let recovery_code = payload.split_off(payload_len - 11);
901
902 let account_len = (*payload.first().expect("payload already checked")) as usize;
903 if account_len > payload.len() - 1 {
904 bail!("invalid format");
905 }
906
907 let account = Bytes::copy_from_slice(&payload[1..=account_len]);
908 let account_str = std::str::from_utf8(&account[..])?;
909
910 let txn = WriteTransaction::new(self.env.clone())?;
911
912 {
913 let mut access = txn.access();
914
915 let read_recovery_codes = access.get(&self.recovery_db, &account[..])?;
916
917 let (is_valid_recovery_code, stored_codes) =
918 consume_code(&recovery_code[..], read_recovery_codes)?;
919
920 if is_valid_recovery_code {
921 access.put(
922 &self.recovery_db,
923 &account[..],
924 &stored_codes[..],
925 &put::Flags::empty(),
926 )?;
927 } else {
928 bail!("invalid recovery code");
929 }
930 }
931
932 txn.commit()?;
933
934 self.registration_finish(payload, Some(account_str))?;
935
936 Ok(())
937 }
938
939 #[instrument(skip(self, payload), err)]
940 pub fn reset_totp_mfa_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
941 self.login_start(payload)
942 }
943
944 #[instrument(skip(self, payload), err)]
945 pub fn reset_totp_mfa_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
946 use chacha20poly1305::aead::KeyInit;
947
948 let (_, key, _) = self.login_finish(payload.clone(), true)?;
949
950 let account_len = (*payload.first().expect("payload already checked")) as usize;
951 let account = &payload[1..=account_len];
952
953 let txn = WriteTransaction::new(self.env.clone())?;
954
955 let mfa_secret = {
956 let mut access = txn.access();
957
958 let mfa_secret = Secret::generate_secret().to_bytes()?;
959
960 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
961 let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
962
963 let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
964 Ok(v) => v,
965 Err(err) => bail!("{err}"),
966 };
967 encrypted_mfa_secret.extend_from_slice(&nonce);
968
969 let account_record: &[u8] = access.get(&self.account_db, account)?;
970 let mut account_record = account_record.to_vec();
971
972 account_record.splice(168..228, encrypted_mfa_secret);
973
974 access.put(
975 &self.account_db,
976 account,
977 &account_record,
978 &put::Flags::empty(),
979 )?;
980
981 mfa_secret
982 };
983
984 txn.commit()?;
985
986 let cipher = XChaCha20Poly1305::new(&key.into());
987 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
988 let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
989 Ok(es) => es,
990 Err(err) => bail!("{err}"),
991 };
992
993 encrypted_mfa_secret.extend_from_slice(&nonce);
994
995 Ok(Bytes::copy_from_slice(&encrypted_mfa_secret))
996 }
997
998 #[instrument(skip(self, payload), err)]
999 pub fn lost_totp_mfa_start(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
1000 let payload_len = payload.len();
1001 if payload_len < 33 {
1002 bail!("invalid payload");
1003 }
1004
1005 let recovery_code = payload.split_off(payload_len - 32);
1006
1007 let server_message = self.login_start(payload.clone())?;
1008
1009 let account_len = (*payload.first().expect("payload already checked")) as usize;
1010 let account = &payload[1..=account_len];
1011
1012 let txn = ReadTransaction::new(self.env.clone())?;
1013
1014 let access = txn.access();
1015
1016 let read_recovery_codes = access.get(&self.recovery_db, account)?;
1017
1018 let is_valid_recovery_code = check_code(&recovery_code[..], read_recovery_codes)?;
1019
1020 if !is_valid_recovery_code {
1021 bail!("invalid recovery code");
1022 }
1023
1024 Ok(server_message)
1025 }
1026
1027 #[instrument(skip(self, payload), err)]
1028 pub fn lost_totp_mfa_finish(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
1029 use chacha20poly1305::aead::KeyInit;
1030
1031 let payload_len = payload.len();
1032 if payload_len < 12 {
1033 bail!("invalid payload");
1034 }
1035
1036 let recovery_code = payload.split_off(payload_len - 11);
1037
1038 let (_, key, _) = self.login_finish(payload.clone(), false)?;
1039
1040 let account_len = (*payload.first().expect("payload already checked")) as usize;
1041 let account = &payload[1..=account_len];
1042
1043 let txn = WriteTransaction::new(self.env.clone())?;
1044
1045 let mfa_secret = {
1046 let mut access = txn.access();
1047
1048 let read_recovery_codes = access.get(&self.recovery_db, account)?;
1049
1050 let (is_valid_recovery_code, stored_codes) =
1051 consume_code(&recovery_code[..], read_recovery_codes)?;
1052
1053 if is_valid_recovery_code {
1054 access.put(
1055 &self.recovery_db,
1056 account,
1057 &stored_codes[..],
1058 &put::Flags::empty(),
1059 )?;
1060
1061 let mfa_secret = Secret::generate_secret().to_bytes()?;
1062
1063 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1064 let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
1065
1066 let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
1067 Ok(v) => v,
1068 Err(err) => bail!("{err}"),
1069 };
1070 encrypted_mfa_secret.extend_from_slice(&nonce);
1071
1072 let account_record: &[u8] = access.get(&self.account_db, account)?;
1073 let mut account_record = account_record.to_vec();
1074
1075 account_record.splice(168..228, encrypted_mfa_secret);
1076
1077 access.put(
1078 &self.account_db,
1079 account,
1080 &account_record,
1081 &put::Flags::empty(),
1082 )?;
1083
1084 mfa_secret
1085 } else {
1086 bail!("invalid recovery code");
1087 }
1088 };
1089
1090 txn.commit()?;
1091
1092 let cipher = XChaCha20Poly1305::new(&key.into());
1093 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1094 let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
1095 Ok(es) => es,
1096 Err(err) => bail!("{err}"),
1097 };
1098
1099 encrypted_mfa_secret.extend_from_slice(&nonce);
1100
1101 Ok(Bytes::copy_from_slice(&encrypted_mfa_secret))
1102 }
1103
1104 #[instrument(skip(self, payload), err)]
1105 pub fn reset_recovery_codes_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1106 self.login_start(payload)
1107 }
1108
1109 #[instrument(skip(self, payload), err)]
1110 pub fn reset_recovery_codes_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1111 use chacha20poly1305::aead::KeyInit;
1112
1113 let (_, key, _) = self.login_finish(payload.clone(), true)?;
1114
1115 let account_len = (*payload.first().expect("payload already checked")) as usize;
1116 let account = &payload[1..=account_len];
1117
1118 let (stored_recovery_codes, recovery_codes) = crate::recovery::generate_codes()?;
1119
1120 let txn = WriteTransaction::new(self.env.clone())?;
1121
1122 {
1123 let mut access = txn.access();
1124
1125 access.put(
1126 &self.recovery_db,
1127 account,
1128 &stored_recovery_codes[..],
1129 &put::Flags::empty(),
1130 )?;
1131 }
1132
1133 txn.commit()?;
1134
1135 let cipher = XChaCha20Poly1305::new(&key.into());
1136 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1137 let mut encrypted_recovery_codes = match cipher.encrypt(&nonce, recovery_codes.as_bytes()) {
1138 Ok(rc) => rc,
1139 Err(err) => bail!("{err}"),
1140 };
1141
1142 encrypted_recovery_codes.extend_from_slice(&nonce);
1143
1144 Ok(Bytes::copy_from_slice(&encrypted_recovery_codes))
1145 }
1146
1147 #[instrument(skip(self, payload), err)]
1148 pub fn delete_account_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1149 self.login_start(payload)
1150 }
1151
1152 #[instrument(skip(self, payload), err)]
1153 pub fn delete_account_finish(&self, payload: Bytes) -> anyhow::Result<()> {
1154 self.login_finish(payload.clone(), true)?;
1155
1156 let account_len = (*payload.first().expect("payload already checked")) as usize;
1157 let account = &payload[1..=account_len];
1158
1159 let txn = WriteTransaction::new(self.env.clone())?;
1160
1161 {
1162 let mut access = txn.access();
1163 access.del_key(&self.account_db, account)?;
1164 }
1165
1166 txn.commit()?;
1167
1168 Ok(())
1169 }
1170
1171 #[instrument(skip(self), err)]
1173 pub fn list_accounts(&self) -> anyhow::Result<Bytes> {
1174 let txn = ReadTransaction::new(self.env.clone())?;
1175
1176 let access = txn.access();
1177 let mut cursor = txn.cursor(self.account_db.clone())?;
1178
1179 let (first_account, first_user) = cursor.first::<[u8], [u8]>(&access)?;
1180
1181 let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1182 let mut builder_vec = builder.start_vector();
1183
1184 let mut first_user_vec = builder_vec.start_vector();
1185
1186 first_user_vec.push(first_account);
1187
1188 if first_user.len() > 460 {
1189 let claims_reader = flexbuffers::Reader::get_root(&first_user[460..])?;
1190
1191 for claims_field in &self.config.access_token.claims {
1192 claims_field.kind.copy_to(
1193 &claims_reader.as_vector().idx(claims_field.idx as usize),
1194 &mut first_user_vec,
1195 None,
1196 )?;
1197 }
1198 }
1199
1200 first_user_vec.end_vector();
1201
1202 while let Ok((account, user)) = cursor.next::<[u8], [u8]>(&access) {
1203 let mut user_vec = builder_vec.start_vector();
1204
1205 user_vec.push(account);
1206
1207 if user.len() > 460 {
1208 let claims_reader = flexbuffers::Reader::get_root(&user[460..])?;
1209
1210 for claims_field in &self.config.access_token.claims {
1211 claims_field.kind.copy_to(
1212 &claims_reader.as_vector().idx(claims_field.idx as usize),
1213 &mut user_vec,
1214 None,
1215 )?;
1216 }
1217 }
1218
1219 user_vec.end_vector();
1220 }
1221
1222 builder_vec.end_vector();
1223
1224 Ok(Bytes::copy_from_slice(builder.view()))
1225 }
1226
1227 #[instrument(skip(self, claims, account), err)]
1228 pub fn set_claims(&self, account: &[u8], claims: &[u8]) -> anyhow::Result<()> {
1229 let str_account = std::str::from_utf8(account)?;
1230
1231 let claims_root = flexbuffers::Reader::get_root(claims)?;
1232
1233 let mut claims_builder =
1234 flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1235 let mut dest = claims_builder.start_vector();
1236
1237 let system_claims_gap = dest.start_vector();
1238 system_claims_gap.end_vector();
1239
1240 let claims_vec = claims_root.as_vector();
1241
1242 for claim in &self.config.access_token.claims {
1243 let src = claims_vec.idx(claim.idx as usize);
1244
1245 claim.kind.copy_to(&src, &mut dest, None)?;
1246 }
1247
1248 dest.end_vector();
1249
1250 let txn = WriteTransaction::new(self.env.clone())?;
1252 {
1253 let mut access = txn.access();
1254 let account_record: &[u8] = access.get(&self.account_db, account)?;
1255 let mut account_record = account_record.to_vec();
1256
1257 if account_record.len() > 460 {
1258 account_record.truncate(460);
1259 }
1260
1261 account_record.extend_from_slice(claims_builder.view());
1262
1263 access.put(
1264 &self.account_db,
1265 account,
1266 &account_record[..],
1267 &put::Flags::empty(),
1268 )?;
1269 }
1270
1271 txn.commit()?;
1272
1273 tracing::info!(account = %str_account);
1274
1275 Ok(())
1276 }
1277
1278 fn create_invite(
1279 &self,
1280 invite_config: &InviteConfig,
1281 inviter_account: &str,
1282 custom_claims: Option<Bytes>,
1283 ) -> anyhow::Result<Bytes> {
1284 let mut invite_claims = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1285 let mut invite_claims_vec = invite_claims.start_vector();
1286
1287 let mut system_claims_vec = invite_claims_vec.start_vector();
1288
1289 let token_uuid = Uuid::now_v7();
1290 let token_uuid_bytes = token_uuid.as_bytes();
1291
1292 tracing::info!(account = %inviter_account, token = %token_uuid);
1293
1294 system_claims_vec.push(flexbuffers::Blob(&token_uuid_bytes[..]));
1295 system_claims_vec.push(self.domain.as_str());
1296 system_claims_vec.push(inviter_account);
1297
1298 system_claims_vec.end_vector();
1299
1300 if let Some(config_claims) = &invite_config.claims
1301 && let Some(custom_claims) = custom_claims
1302 {
1303 let custom_claims_root = flexbuffers::Reader::get_root(custom_claims.as_ref())?;
1304 let custom_claims = custom_claims_root.as_vector();
1305
1306 for claim in config_claims {
1307 claim.kind.copy_to(
1308 &custom_claims.idx(claim.idx as usize),
1309 &mut invite_claims_vec,
1310 None,
1311 )?;
1312 }
1313 }
1314
1315 invite_claims_vec.end_vector();
1316
1317 match &invite_config.mode {
1318 InviteMode::Root => {
1319 if inviter_account != format!("root@{}", self.domain) {
1320 bail!("inviter must be root");
1321 }
1322 }
1323 InviteMode::Admin => {
1324 }
1326 InviteMode::Viral => (),
1327 }
1328
1329 let invite_token = generate_hmac(
1330 invite_claims.view(),
1331 invite_config.lifetime,
1332 &self.invite_token_key,
1333 )?;
1334
1335 let txn = WriteTransaction::new(self.env.clone())?;
1336 {
1337 let mut access = txn.access();
1338 access.put(
1339 &self.invite_db,
1340 &token_uuid_bytes[..],
1341 &[] as &[u8],
1342 &put::Flags::empty(),
1343 )?;
1344 }
1345
1346 txn.commit()?;
1347
1348 Ok(invite_token)
1349 }
1350
1351 pub fn account_exists(&self, account: &str) -> anyhow::Result<bool> {
1352 let txn = ReadTransaction::new(self.env.clone())?;
1353 let access = txn.access();
1354
1355 if access
1356 .get::<[u8], [u8]>(&self.account_db, account.as_bytes())
1357 .is_ok()
1358 {
1359 return Ok(true);
1360 }
1361
1362 if access
1363 .get::<[u8], [u8]>(&self.state_db, account.as_bytes())
1364 .is_ok()
1365 {
1366 return Ok(true);
1367 }
1368
1369 Ok(false)
1370 }
1371
1372 #[instrument(skip(self, inviter_domain, inviter_account, custom_claims), err)]
1373 pub fn api_invite_get(
1374 &self,
1375 inviter_domain: &str,
1376 inviter_account: &str,
1377 custom_claims: Option<Bytes>,
1378 ) -> anyhow::Result<Bytes> {
1379 if let Some(invite_config) = &self.config.invite {
1380 let account = format!("{inviter_account}@{inviter_domain}");
1381
1382 self.create_invite(invite_config, account.as_str(), custom_claims)
1383 } else {
1384 bail!("no invite config")
1385 }
1386 }
1387
1388 #[instrument(skip(self, access_token), err)]
1389 pub fn invite_get(&self, access_token: Bytes) -> anyhow::Result<Bytes> {
1390 if let Some(invite_config) = &self.config.invite {
1391 let (account, _) = self.verify_access_token(&access_token)?;
1392
1393 self.create_invite(invite_config, account, None)
1395 } else {
1396 bail!("no invite config")
1397 }
1398 }
1399
1400 #[allow(clippy::type_complexity)]
1401 #[instrument(skip(self, invite_token), err)]
1402 pub fn invite_check<'a>(
1403 &self,
1404 invite_token: &'a [u8],
1405 ) -> anyhow::Result<(VectorReader<&'a [u8]>, &'a [u8])> {
1406 if let Some(invite_config) = &self.config.invite {
1407 let claims = crate::token::verify_hmac(invite_token, &self.invite_token_key)?;
1408
1409 let claims_root = flexbuffers::Reader::get_root(claims)?;
1410 let claims_vec = claims_root.as_vector();
1411 let system_claims = claims_vec.idx(0).as_vector();
1412
1413 let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
1414
1415 let token_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1416 let api_or_site_domain = system_claims.idx(1).as_str();
1417 let inviter_account = system_claims.idx(2).as_str();
1418
1419 if api_or_site_domain == self.domain {
1420 match invite_config.mode {
1421 InviteMode::Root => {
1422 if inviter_account != format!("root@{}", self.domain) {
1423 bail!("inviter must be root");
1424 }
1425 }
1426 InviteMode::Admin => {
1427 }
1429 InviteMode::Viral => (),
1430 }
1431
1432 let txn = WriteTransaction::new(self.env.clone())?;
1434 {
1435 let mut access = txn.access();
1436
1437 if access
1438 .del_key(&self.invite_db, &token_uuid_bytes[..])
1439 .is_err()
1440 {
1441 bail!("token id does not exist");
1442 }
1443 }
1444
1445 txn.commit()?;
1446
1447 tracing::info!(account = %inviter_account, token = %token_uuid_str);
1448
1449 Ok((claims_vec, claims))
1450 } else {
1451 bail!(
1452 "domain {api_or_site_domain} does not match for invite {token_uuid_str} from user {inviter_account}"
1453 )
1454 }
1455 } else {
1456 bail!("no invite config")
1457 }
1458 }
1459
1460 pub fn access_get(&self, refresh_token: &Bytes) -> anyhow::Result<Bytes> {
1461 let span = tracing::info_span!("auth");
1462 let span = span.in_scope(|| tracing::info_span!("token"));
1463 let span = span.in_scope(|| tracing::info_span!("access"));
1464 let span = span.in_scope(|| tracing::info_span!("get"));
1465
1466 span.in_scope(|| {
1467 let (account, client_verifying_key) = self.verify_refresh_token(&refresh_token[..])?;
1469
1470 let mut claims_builder =
1471 flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1472 let mut dest = claims_builder.start_vector();
1473
1474 let txn = ReadTransaction::new(self.env.clone())?;
1475 let access = txn.access();
1476
1477 let account_record: &[u8] = access.get(&self.account_db, account.as_bytes())?;
1478
1479 let user_claims = flexbuffers::Reader::get_root(&account_record[460..])?.as_vector();
1480
1481 let mut system_claims = dest.start_vector();
1482
1483 let token_id = Uuid::new_v4();
1484
1485 system_claims.push(flexbuffers::Blob(&token_id.as_bytes()[..]));
1486 system_claims.push(self.domain.as_str());
1487 system_claims.push(account);
1488
1489 if let Some(client_verifying_key) = client_verifying_key {
1490 system_claims.push(flexbuffers::Blob(&client_verifying_key[..]));
1491 }
1492
1493 system_claims.end_vector();
1494
1495 for claim in &self.config.access_token.claims {
1496 let src = user_claims.idx(claim.idx as usize);
1497 claim.kind.copy_to(&src, &mut dest, None)?;
1498 }
1499
1500 dest.end_vector();
1501
1502 let access_token = generate_hmac(
1503 claims_builder.view(),
1504 self.config.access_token.lifetime,
1505 &self.access_token_key,
1506 )?;
1507
1508 tracing::info!(token = %token_id, "generated");
1509 Ok(access_token)
1510 })
1511 }
1512
1513 #[allow(clippy::type_complexity)]
1515 pub fn verify_access_token<'a>(
1516 &self,
1517 token: &'a [u8],
1518 ) -> anyhow::Result<(&'a str, VectorReader<&'a [u8]>)> {
1519 let span = tracing::info_span!("auth");
1520 let span = span.in_scope(|| tracing::info_span!("token"));
1521 let span = span.in_scope(|| tracing::info_span!("access"));
1522 let span = span.in_scope(|| tracing::info_span!("verify"));
1523
1524 span.in_scope(|| {
1525 let claims: &'a [u8] = crate::token::extract_hmac_no_check(token)?;
1526
1527 let claims_vec = match flexbuffers::Reader::get_root(
1528 &claims[..claims.len().checked_sub(EXP_LEN + SIG_LEN).unwrap_or(claims.len())],
1529 ) {
1530 Ok(v) => v.as_vector(),
1531 Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1532 };
1533
1534 let system_claims = claims_vec.idx(0).as_vector();
1535
1536 let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
1537
1538 let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1539 let claims_domain = system_claims.idx(1).as_str();
1540 let claims_account = system_claims.idx(2).as_str();
1541
1542 if claims_domain == self.domain {
1543 let claims_verifier = system_claims.idx(3).as_blob().0;
1544
1545 if claims_verifier.is_empty() {
1546 crate::token::verify_hmac(token, &self.access_token_key)?;
1547 } else {
1548 crate::token::verify_client_signature(token, claims_verifier.try_into()?)?;
1549 crate::token::verify_hmac(&token[..token.len() - (EXP_LEN + SIG_LEN)], &self.access_token_key)?;
1550 }
1551
1552 tracing::info!(
1553 account = %claims_account,
1554 token = %claims_uuid_str,
1555 "verified"
1556 );
1557
1558 Ok((claims_account, claims_vec))
1559 } else {
1560 bail!(
1561 "domain {claims_domain} does not match for user {claims_account} with token {claims_uuid_str}"
1562 )
1563 }
1564 })
1565 }
1566
1567 pub(crate) fn generate_refresh_token(
1568 &self,
1569 account: &[u8],
1570 verifier_claim: Option<&[u8]>,
1571 ) -> anyhow::Result<Bytes> {
1572 let span = tracing::info_span!("auth");
1573 let span = span.in_scope(|| tracing::info_span!("token"));
1574 let span = span.in_scope(|| tracing::info_span!("refresh"));
1575 let span = span.in_scope(|| tracing::info_span!("generate"));
1576
1577 span.in_scope(|| {
1578 let str_account = std::str::from_utf8(account)?;
1579
1580 let mut claims_builder =
1581 flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1582 let mut system_claims = claims_builder.start_vector();
1583
1584 let token_id = Uuid::new_v4();
1585
1586 system_claims.push(flexbuffers::Blob(&token_id.as_bytes()[..]));
1587 system_claims.push(self.domain.as_str());
1588 system_claims.push(str_account);
1589
1590 if let Some(verifier_claim) = verifier_claim {
1591 system_claims.push(flexbuffers::Blob(verifier_claim));
1592 }
1593
1594 system_claims.end_vector();
1595
1596 let refresh_token = crate::token::generate_signed(
1597 claims_builder.view(),
1598 self.config.refresh_token.lifetime,
1599 &self.token_signing_key,
1600 )?;
1601
1602 tracing::info!(token = token_id.to_string(), "generated");
1603
1604 Ok(refresh_token)
1605 })
1606 }
1607
1608 #[allow(clippy::type_complexity)]
1610 fn verify_refresh_token<'a>(
1611 &self,
1612 token: &'a [u8],
1613 ) -> anyhow::Result<(&'a str, Option<&'a [u8; 32]>)> {
1614 let span = tracing::info_span!("auth");
1615 let span = span.in_scope(|| tracing::info_span!("token"));
1616 let span = span.in_scope(|| tracing::info_span!("refresh"));
1617 let span = span.in_scope(|| tracing::info_span!("verify"));
1618
1619 span.in_scope(|| {
1620 let claims = extract_signed_no_check(token)?;
1621 let claims_vec = match flexbuffers::Reader::get_root(
1622 &claims[..claims
1623 .len()
1624 .checked_sub(EXP_LEN + SIG_LEN)
1625 .unwrap_or(claims.len())],
1626 ) {
1627 Ok(v) => v.as_vector(),
1628 Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1629 };
1630
1631 let token_uuid_bytes: [u8; 16] = claims_vec.idx(0).as_blob().0.try_into()?;
1632
1633 let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1634 let claims_domain = claims_vec.idx(1).as_str();
1635 let claims_account = claims_vec.idx(2).as_str();
1636
1637 let claims_verifier = claims_vec.idx(3).as_blob().0;
1638
1639 let mut client_verifying_key = None;
1640
1641 if claims_verifier.is_empty() {
1642 crate::token::verify_signed(token, &self.token_verifying_key)?;
1644 } else {
1645 let verifying_key_bytes = claims_verifier.try_into()?;
1646
1647 client_verifying_key = Some(verifying_key_bytes);
1648
1649 crate::token::verify_client_signature(token, verifying_key_bytes)?;
1650
1651 crate::token::verify_signed(
1653 &token[..token.len() - (EXP_LEN + SIG_LEN)],
1654 &self.token_verifying_key,
1655 )?;
1656 }
1657
1658 tracing::info!(
1659 domain = %claims_domain,
1660 account = %claims_account,
1661 token = %claims_uuid_str,
1662 "verified"
1663 );
1664
1665 Ok((claims_account, client_verifying_key))
1666 })
1667 }
1668
1669 #[instrument(skip(self, token) err)]
1670 pub fn verify_password_reset_token<'a>(&self, token: &'a [u8]) -> anyhow::Result<&'a str> {
1671 let claims: &'a [u8] = crate::token::extract_hmac_no_check(token)?;
1672
1673 let claims_vec = match flexbuffers::Reader::get_root(
1674 &claims[..claims
1675 .len()
1676 .checked_sub(EXP_LEN + SIG_LEN)
1677 .unwrap_or(claims.len())],
1678 ) {
1679 Ok(v) => v.as_vector(),
1680 Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1681 };
1682
1683 let token_uuid_bytes: [u8; 16] = claims_vec.idx(0).as_blob().0.try_into()?;
1684
1685 let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1686 let claims_domain = claims_vec.idx(1).as_str();
1687 let claims_account = claims_vec.idx(2).as_str();
1688
1689 if claims_domain == self.domain {
1690 let claims_verifier = claims_vec.idx(3).as_blob().0;
1691
1692 if claims_verifier.is_empty() {
1693 crate::token::verify_hmac(token, &self.reset_password_token_key)?;
1694 } else {
1695 crate::token::verify_client_signature(token, claims_verifier.try_into()?)?;
1696 crate::token::verify_hmac(
1697 &token[..token.len() - (EXP_LEN + SIG_LEN)],
1698 &self.reset_password_token_key,
1699 )?;
1700 }
1701
1702 tracing::info!(
1703 account = %claims_account,
1704 token = %claims_uuid_str,
1705 "verified"
1706 );
1707
1708 Ok(claims_account)
1709 } else {
1710 bail!(
1711 "domain {claims_domain} does not match for user {claims_account} with token {claims_uuid_str}"
1712 )
1713 }
1714 }
1715}