1use crate::api;
3use crate::api::OpenError;
4use crate::attr::{Attr, AttrState};
5use crate::common::elgamal;
6use crate::common::secret::DigestibleSecret as _;
7use crate::handle;
8use crate::hub;
9use crate::id::Id;
10use crate::misc::crypto;
11use crate::misc::error::{OPAQUE, Opaque};
12use crate::misc::jwt;
13
14use std::collections::{HashMap, HashSet};
15use std::ops::Deref;
16use std::rc::Rc;
17
18use actix_web::web;
19use sha2::digest::Digest as _;
20
21use super::server::*;
22use api::phc::user::*;
23
24use api::phc::user::UserState as ApiUserState;
25
26impl App {
27 pub(super) fn cached_handle_user_welcome(app: &Self) -> api::Result<WelcomeResp> {
29 let running_state = app.running_state_or_please_retry()?;
30
31 let hubs: HashMap<handle::Handle, hub::BasicInfo> = app
32 .shared
33 .hubs
34 .values()
35 .map(|hub| (hub.handles.preferred().clone(), hub.clone()))
36 .collect();
37
38 Ok(WelcomeResp {
39 constellation: (*running_state.constellation).clone(),
40 hubs,
41 })
42 }
43
44 pub(super) async fn handle_cached_hub_info(
46 app: web::Data<Rc<App>>,
47 ) -> impl actix_web::Responder {
48 app.cached_hub_info.borrow().clone()
49 }
50
51 pub(super) async fn handle_user_state(
53 app: Rc<Self>,
54 auth_token: actix_web::web::Header<AuthToken>,
55 ) -> api::Result<StateResp> {
56 let Ok((user_state, _)) = app
57 .open_auth_token_and_get_user_state(auth_token.into_inner())
58 .await?
59 else {
60 return Ok(StateResp::RetryWithNewAuthToken);
61 };
62
63 Ok(StateResp::State(user_state.into_user_version(&app)))
64 }
65
66 pub(super) async fn handle_user_enter(
68 app: Rc<Self>,
69 req: web::Json<EnterReq>,
70 auth_token: Option<actix_web::web::Header<AuthToken>>,
71 ) -> api::Result<EnterResp> {
72 let running_state = &app.running_state_or_please_retry()?;
73
74 let EnterReq {
75 identifying_attr,
76 mode,
77 add_attrs,
78 register_only_with_unique_attrs,
79 } = req.into_inner();
80
81 let auth_token_inner = if let Some(auth_token) = auth_token {
82 let Ok(inner) = app.open_auth_token_inner(auth_token.into_inner(), false) else {
83 return Ok(EnterResp::RetryWithNewAuthToken);
84 };
85
86 if !matches!(mode, EnterMode::Login) {
87 log::debug!("a user tried to enter with an auth token, but not in the login mode");
88 return Err(api::ErrorCode::BadRequest);
89 }
90
91 Some(inner)
92 } else {
93 None
94 };
95 let auth_token_user_id = auth_token_inner.as_ref().map(|inner| inner.user_id);
96
97 let session_start = match (&identifying_attr, &auth_token_inner) {
98 (None, Some(inner)) => inner.session_start,
99 _ => jwt::NumericDate::now(),
100 };
101
102 if auth_token_user_id.is_none() && identifying_attr.is_none() {
103 log::debug!("entry request with neither auth token nor identifying attribute");
104 return Err(api::ErrorCode::BadRequest);
105 }
106
107 if register_only_with_unique_attrs {
108 match mode {
109 EnterMode::Login => {
110 log::debug!(
111 "entry request with `register_only_with_unique_attrs` set, but mode `Login`"
112 );
113 }
114 EnterMode::Register | EnterMode::LoginOrRegister => { }
115 }
116 }
117
118 let identifying_attr = if let Some(identifying_attr) = identifying_attr {
120 let identifying_attr = app.id_attr(
121 match identifying_attr.open(&running_state.attr_signing_key, None) {
122 Ok(identifying_attr) => identifying_attr,
123 Err(OpenError::OtherConstellation(..)) | Err(OpenError::InternalError) => {
124 return Err(api::ErrorCode::InternalError);
125 }
126 Err(OpenError::OtherwiseInvalid) => {
127 return Err(api::ErrorCode::BadRequest);
128 }
129 Err(OpenError::Expired) | Err(OpenError::InvalidSignature) => {
130 return Ok(EnterResp::RetryWithNewIdentifyingAttr);
131 }
132 },
133 );
134
135 if identifying_attr.not_identifying {
136 log::warn!(
137 "supposed attribute {} of type {} is not identifying",
138 identifying_attr.value,
139 identifying_attr.attr_type
140 );
141 return Err(api::ErrorCode::BadRequest);
142 }
143
144 Some(identifying_attr)
145 } else {
146 None
147 };
148
149 let attrs: HashMap<Id, IdedAttr> = {
150 let mut attrs: HashMap<Id, IdedAttr> = HashMap::with_capacity(add_attrs.len());
151
152 if let Some(ref identifying_attr) = identifying_attr {
153 attrs.insert(identifying_attr.id, identifying_attr.clone());
154 }
155
156 for (add_attr_index, add_attr) in add_attrs.into_iter().enumerate() {
157 let ided_attr =
158 app.id_attr(match add_attr.open(&running_state.attr_signing_key, None) {
159 Ok(attr) => attr,
160 Err(OpenError::OtherConstellation(..)) | Err(OpenError::InternalError) => {
161 return Err(api::ErrorCode::InternalError);
162 }
163 Err(OpenError::OtherwiseInvalid) => {
164 return Err(api::ErrorCode::BadRequest);
165 }
166 Err(OpenError::Expired) | Err(OpenError::InvalidSignature) => {
167 return Ok(EnterResp::RetryWithNewAddAttr {
168 index: add_attr_index,
169 });
170 }
171 });
172
173 if ided_attr.not_addable {
174 log::warn!(
175 "entry: someone tried to add unaddable attribute of type {}",
176 ided_attr.attr_type
177 );
178 return Err(api::ErrorCode::BadRequest);
179 }
180
181 let previous_value = attrs.insert(ided_attr.id, ided_attr);
182
183 if let Some(attr) = previous_value {
184 log::warn!(
185 "entry: attribute {} of type {} provided twice",
186 attr.value,
187 attr.attr_type
188 );
189 return Err(api::ErrorCode::BadRequest);
190 }
191 }
192
193 attrs
194 };
195
196 let mut attr_states: std::collections::HashMap<
198 Id,
199 (AttrState, object_store::UpdateVersion),
200 > = Default::default();
201
202 let mut retrieved_attr_states = false;
207
208 let mut attr_add_status: HashMap<Id, AttrAddStatus> = Default::default();
210
211 let ((user_state, mut user_state_version), new_account) = 'found_user: {
213 if let Some(auth_token_user_id) = auth_token_user_id {
214 let user_and_version = app
215 .get_object::<UserState>(&auth_token_user_id)
216 .await?
217 .ok_or_else(|| {
218 log::error!(
219 "a valid auth token passed during entry refers to a user with user_id {auth_token_user_id} that does not exist",
220 );
221 api::ErrorCode::InternalError
222 })?;
223
224 break 'found_user (user_and_version, false);
225 }
226
227 let identifying_attr = identifying_attr.expect(
228 "we should (but don't) have either an auth token or an identifying attribute",
229 );
230
231 if matches!(mode, EnterMode::Login | EnterMode::LoginOrRegister) {
232 if let Some((ias, ias_v)) =
234 app.get_object::<AttrState>(&identifying_attr.id).await?
235 {
236 log::trace!(
237 "enter: account exists for attribute {} of type {}",
238 identifying_attr.value,
239 identifying_attr.attr_type,
240 );
241
242 let user_id = ias.may_identify_user.ok_or_else(|| {
243 log::error!(
244 "identifying attribute {} of type {} has may_identify_user set to None",
245 identifying_attr.value,
246 identifying_attr.attr_type
247 );
248 api::ErrorCode::InternalError
249 })?;
250
251 attr_states.insert(identifying_attr.id, (ias, ias_v));
252
253 let user_and_version = app
254 .get_object::<UserState>(&user_id)
255 .await?
256 .ok_or_else(|| {
257 log::error!(
258 "identifying attribute {} of type {} refers to a user \
259 account {user_id} that does not exist",
260 identifying_attr.value,
261 identifying_attr.attr_type
262 );
263 api::ErrorCode::InternalError
264 })?;
265
266 break 'found_user (user_and_version, false);
267 }
268
269 log::trace!(
270 "enter: no account exists for attribute {} of type {}",
271 identifying_attr.value,
272 identifying_attr.attr_type,
273 );
274 }
275
276 if mode == EnterMode::Login {
277 return Ok(EnterResp::AccountDoesNotExist);
278 }
279
280 assert!(matches!(
281 mode,
282 EnterMode::LoginOrRegister | EnterMode::Register
283 ));
284
285 if let Some(resp) = app
286 .precheck_attrs_for_registration(
287 &attrs,
288 &mut attr_states,
289 &mut retrieved_attr_states,
290 register_only_with_unique_attrs,
291 )
292 .await?
293 {
294 return Ok(resp);
295 }
296
297 let Some(master_enc_key) = running_state.master_enc_key.as_ref() else {
320 log::info!("cannot register a new user yet: master encryption key not available");
321 return Err(api::ErrorCode::PleaseRetry);
322 };
323
324 let user_state = UserState {
325 id: Id::random(),
326 card_id: Some(CardPseud(Id::random())),
327 registration_date: Some(api::NumericDate::now()),
328 polymorphic_pseudonym: master_enc_key.encrypt_random(),
329 banned: false,
330 allow_login_by: attrs
331 .values()
332 .filter_map(|attr| {
333 if attr.not_identifying {
334 None
335 } else {
336 Some(attr.id)
337 }
338 })
339 .collect(),
340 could_be_banned_by: Default::default(),
341 stored_objects: Default::default(),
343 };
344
345 let user_state_version = app
346 .put_object::<UserState>(&user_state, None)
347 .await?
348 .ok_or_else(|| {
349 log::error!("User with id {} already exists - very odd", user_state.id);
350 api::ErrorCode::InternalError
351 })?;
352
353 assert!(!attr_states.contains_key(&identifying_attr.id));
356
357 let identifying_attr_state =
358 AttrState::new(identifying_attr.id, &identifying_attr, user_state.id);
359
360 if let Some(identifying_attr_state_version) = app
361 .put_object::<AttrState>(&identifying_attr_state, None)
362 .await
363 .inspect_err(|err| {
364 log::warn!(
365 "orphaned user account {} due to error with putting \
366 identifying attribute: {err}",
367 user_state.id
368 );
369 })?
370 {
371 assert!(
372 attr_states
373 .insert(
374 identifying_attr.id,
375 (identifying_attr_state, identifying_attr_state_version),
376 )
377 .is_none()
378 );
379
380 assert!(
381 attr_add_status
382 .insert(identifying_attr.id, AttrAddStatus::Added)
383 .is_none()
384 );
385 } else {
386 log::warn!(
387 "possibly orphaned user account {} because identifying \
388 attribute {} was just added before our noses",
389 user_state.id,
390 identifying_attr.id
391 );
392 return Ok(EnterResp::AttributeAlreadyTaken {
393 attr: identifying_attr.attr,
394 bans_other_user: false,
395 });
396 }
397
398 log::debug!("created user account {}", user_state.id);
399 break 'found_user ((user_state, user_state_version), true);
400 };
401
402 if user_state.banned {
403 return Ok(EnterResp::Banned);
404 }
405
406 if !retrieved_attr_states {
407 for attr in attrs.values() {
408 if attr_states.contains_key(&attr.id) {
409 continue;
410 }
411
412 if let Some(attr_state_and_version) = app.get_object::<AttrState>(&attr.id).await? {
413 attr_states.insert(attr.id, attr_state_and_version);
414 }
415 }
416
417 retrieved_attr_states = true;
418 }
419
420 assert!(retrieved_attr_states);
421
422 for attr in attrs.values() {
424 if attr_states.contains_key(&attr.id) {
425 attr_add_status
426 .entry(attr.id)
427 .or_insert(AttrAddStatus::AlreadyThere);
428 continue;
429 }
430
431 let attr_state = AttrState::new(attr.id, attr, user_state.id);
432
433 match app.put_object::<AttrState>(&attr_state, None).await {
434 Ok(Some(attr_state_version)) => {
435 assert!(
436 attr_states
437 .insert(attr.id, (attr_state, attr_state_version))
438 .is_none()
439 );
440 assert!(
441 attr_add_status
442 .insert(attr.id, AttrAddStatus::Added)
443 .is_none()
444 );
445 }
446 problem => {
447 log::warn!("problem adding attribute state {}: {problem:?}", attr.value);
448 assert!(
449 attr_add_status
450 .insert(attr.id, AttrAddStatus::PleaseTryAgain)
451 .is_none()
452 );
453 }
454 }
455 }
456
457 for attr in attrs.values() {
459 let (attr_state, attr_state_version) =
460 if let Some(attr_state_and_version) = attr_states.get(&attr.id) {
461 attr_state_and_version
462 } else {
463 continue;
464 };
465
466 if !attr.bannable || attr_state.bans_users.contains(&user_state.id) {
467 continue;
468 }
469
470 let mut attr_state = attr_state.clone();
471 assert!(attr_state.bans_users.insert(user_state.id));
472
473 match app
474 .put_object::<AttrState>(&attr_state, Some(attr_state_version.clone()))
475 .await
476 {
477 Ok(Some(attr_state_version)) => {
478 assert!(
479 attr_states
480 .insert(attr.id, (attr_state.clone(), attr_state_version))
481 .is_some()
482 );
483 assert!(attr_add_status.contains_key(&attr.id));
484 }
485 _ => {
486 assert!(
487 attr_add_status
488 .insert(attr.id, AttrAddStatus::PleaseTryAgain)
489 .is_none()
490 );
491 }
492 }
493 }
494
495 let mut new_user_state = user_state.clone();
496 let mut added_attrs: HashSet<Id> = Default::default();
497
498 for (attr_id, (attr_state, ..)) in attr_states {
500 if *attr_add_status.get(&attr_id).unwrap() == AttrAddStatus::PleaseTryAgain {
501 continue;
502 }
503
504 if let Some(identifies_user_id) = attr_state.may_identify_user {
505 assert_eq!(identifies_user_id, user_state.id);
506
507 if new_user_state.allow_login_by.insert(attr_id) {
508 added_attrs.insert(attr_id);
509 }
510 }
511
512 if attr_state.bans_users.contains(&user_state.id)
513 && new_user_state.could_be_banned_by.insert(attr_id)
514 {
515 added_attrs.insert(attr_id);
516 }
517 }
518
519 if !added_attrs.is_empty() {
520 match app
521 .put_object::<UserState>(&new_user_state, Some(user_state_version))
522 .await
523 {
524 Ok(Some(new_user_state_version)) => {
525 #[expect(unused_assignments)]
526 {
527 user_state_version = new_user_state_version;
528 }
529
530 for added_attr_id in added_attrs {
531 attr_add_status.insert(added_attr_id, AttrAddStatus::Added);
532 }
533 }
534
535 problem => {
536 log::warn!("failed to update user state to add attributes: {problem:?}");
537
538 for added_attr_id in added_attrs {
539 attr_add_status.insert(added_attr_id, AttrAddStatus::PleaseTryAgain);
540 }
541 }
542 }
543 }
544 let user_state = new_user_state;
545
546 let auth_token_package = app.issue_auth_token(&user_state, session_start)?;
547
548 Ok(EnterResp::Entered {
549 new_account,
550 auth_token_package,
551 attr_status: attr_add_status
552 .iter()
553 .map(|(attr_id, attr_add_status)| {
554 (attrs.get(attr_id).unwrap().attr.clone(), *attr_add_status)
555 })
556 .collect(),
557 })
558 }
559
560 fn id_attr(&self, attr: Attr) -> IdedAttr {
562 IdedAttr {
563 id: attr.id(&*self.attr_id_secret),
564 attr,
565 }
566 }
567
568 async fn precheck_attrs_for_registration(
586 &self,
587 attrs: &HashMap<Id, IdedAttr>,
588 attr_states: &mut HashMap<Id, (AttrState, object_store::UpdateVersion)>,
589 retrieved_attr_states: &mut bool,
590 register_only_with_unique_attrs: bool,
591 ) -> api::Result<Option<EnterResp>> {
592 if !attrs.values().any(|attr| attr.bannable) {
595 return Ok(Some(EnterResp::NoBannableAttribute));
596 }
597
598 assert!(!*retrieved_attr_states, "not expecting double work here");
599
600 for (attr_id, attr) in attrs {
602 if attr_states.contains_key(attr_id) {
604 continue;
605 }
606
607 if let Some(attr_state_and_version) = self.get_object::<AttrState>(attr_id).await? {
608 attr_states.insert(attr.id, attr_state_and_version);
609 }
610 }
611
612 *retrieved_attr_states = true;
613
614 for (attr_id, (attr_state, ..)) in attr_states.iter() {
615 if attr_state.banned {
616 return Ok(Some(EnterResp::AttributeBanned(
617 attrs.get(attr_id).unwrap().attr.clone(),
618 )));
619 }
620
621 if attr_state.may_identify_user.is_some() {
622 return Ok(Some(EnterResp::AttributeAlreadyTaken {
623 attr: attrs.get(attr_id).unwrap().attr.clone(),
624 bans_other_user: false,
625 }));
626 }
627
628 if register_only_with_unique_attrs && !attr_state.bans_users.is_empty() {
629 return Ok(Some(EnterResp::AttributeAlreadyTaken {
630 attr: attrs.get(attr_id).unwrap().attr.clone(),
631 bans_other_user: true,
632 }));
633 }
634 }
635
636 Ok(None)
637 }
638
639 pub(super) async fn handle_user_refresh(
641 app: Rc<Self>,
642 auth_token: actix_web::web::Header<AuthToken>,
643 ) -> api::Result<RefreshResp> {
644 let Ok(inner) = app.open_auth_token_inner(auth_token.into_inner(), true) else {
646 return Ok(RefreshResp::ReobtainAuthToken);
647 };
648
649 if !inner.may_be_refreshed(
650 jwt::NumericDate::now(),
651 app.auth_token_max_idle,
652 app.auth_session_max_duration,
653 ) {
654 return Ok(RefreshResp::ReobtainAuthToken);
655 }
656
657 let (user_state, _) = app.get_auth_token_user_state(inner.user_id).await?;
658
659 Ok(
660 match app.issue_auth_token(&user_state, inner.session_start)? {
661 Ok(atp) => RefreshResp::Success(atp),
662 Err(atdr) => RefreshResp::Denied(atdr),
663 },
664 )
665 }
666
667 fn issue_auth_token(
671 &self,
672 user_state: &UserState,
673 session_start: jwt::NumericDate,
674 ) -> api::Result<Result<AuthTokenPackage, AuthTokenDeniedReason>> {
675 if user_state.could_be_banned_by.is_empty() {
676 return Ok(Err(AuthTokenDeniedReason::NoBannableAttribute));
677 }
678
679 if user_state.banned {
680 return Ok(Err(AuthTokenDeniedReason::Banned));
681 }
682
683 let iat = jwt::NumericDate::now();
684 let exp = iat.add_clamp(self.auth_token_validity.as_secs());
685 Ok(Ok(AuthTokenPackage {
686 expires: exp,
687 auth_token: AuthTokenInner {
688 user_id: user_state.id,
689 iat,
690 exp,
691 session_start,
692 }
693 .seal(&self.auth_token_secret)?,
694 }))
695 }
696}
697
698#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
700pub(super) struct AuthTokenInner {
701 user_id: Id,
703
704 exp: jwt::NumericDate,
706
707 iat: jwt::NumericDate,
709
710 session_start: jwt::NumericDate,
713}
714
715impl AuthTokenInner {
716 fn seal(&self, key: &crypto::SealingKey) -> api::Result<AuthToken> {
717 Ok(AuthToken {
718 inner: serde_bytes::ByteBuf::from(crypto::seal(&self, key, b"").map_err(|err| {
719 log::warn!("failed to seal AuthTokenInner: {err}");
720 api::ErrorCode::InternalError
721 })?)
722 .into(),
723 })
724 }
725
726 fn unseal(sealed: &AuthToken, key: &crypto::SealingKey) -> Result<AuthTokenInner, Opaque> {
727 crypto::unseal(&*sealed.inner, key, b"")
728 }
729
730 fn open(self, accept_expired: bool) -> Result<Self, Opaque> {
732 if !accept_expired && self.exp < jwt::NumericDate::now() {
733 return Err(OPAQUE);
734 }
735
736 Ok(self)
737 }
738
739 fn may_be_refreshed(
742 &self,
743 now: jwt::NumericDate,
744 max_idle: core::time::Duration,
745 max_session: core::time::Duration,
746 ) -> bool {
747 if self.exp.add_clamp(max_idle.as_secs()) < now {
748 log::debug!(
749 "refusing to refresh auth token of idle user {}",
750 self.user_id
751 );
752 return false;
753 }
754
755 if self.session_start.add_clamp(max_session.as_secs()) < now {
756 log::debug!(
757 "refusing to refresh auth token of user {}: session too old",
758 self.user_id
759 );
760 return false;
761 }
762
763 true
764 }
765}
766
767impl App {
768 pub(super) fn open_auth_token(&self, auth_token: AuthToken) -> Result<Id, Opaque> {
770 Ok(self.open_auth_token_inner(auth_token, false)?.user_id)
771 }
772
773 pub(super) fn open_auth_token_inner(
776 &self,
777 auth_token: AuthToken,
778 accept_expired: bool,
779 ) -> Result<AuthTokenInner, Opaque> {
780 AuthTokenInner::unseal(&auth_token, &self.auth_token_secret)?.open(accept_expired)
781 }
782
783 pub(super) async fn open_auth_token_and_get_user_state(
787 &self,
788 auth_token: AuthToken,
789 ) -> api::Result<Result<(UserState, object_store::UpdateVersion), Opaque>> {
790 let Ok(user_id) = self.open_auth_token(auth_token) else {
791 return Ok(Err(OPAQUE));
792 };
793
794 Ok(Ok(self.get_auth_token_user_state(user_id).await?))
795 }
796
797 async fn get_auth_token_user_state(
799 &self,
800 user_id: Id,
801 ) -> api::Result<(UserState, object_store::UpdateVersion)> {
802 self.get_object::<UserState>(&user_id)
803 .await?
804 .ok_or_else(|| {
805 log::error!(
806 "auth token refers to non- (or no longer) existing user with id {user_id}",
807 );
808 api::ErrorCode::InternalError
809 })
810 }
811}
812
813#[derive(Clone)]
815struct IdedAttr {
816 id: Id,
817 attr: Attr,
818}
819
820impl IdedAttr {
821 #[expect(dead_code)]
822 pub fn id(&self) -> Id {
823 unimplemented!("use the field `id` instead")
824 }
825}
826
827impl Deref for IdedAttr {
828 type Target = Attr;
829
830 fn deref(&self) -> &Attr {
831 &self.attr
832 }
833}
834
835#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
837pub struct UserState {
838 pub id: Id,
840
841 #[serde(default)]
847 card_id: Option<CardPseud>,
848
849 #[serde(default)]
853 pub registration_date: Option<api::NumericDate>,
854
855 pub polymorphic_pseudonym: elgamal::Triple,
858
859 pub banned: bool,
861
862 pub allow_login_by: HashSet<Id>,
868
869 pub could_be_banned_by: HashSet<Id>,
877
878 pub stored_objects: HashMap<handle::Handle, super::user_object_store::UserObjectDetails>,
880}
881
882impl UserState {
883 pub fn card_id(&self) -> CardPseud {
885 if let Some(card_id) = self.card_id {
886 return card_id;
887 }
888
889 CardPseud(b"".as_slice().derive_id(
890 sha2::Sha256::new().chain_update(self.id.as_slice()),
891 "pubhubs-card-id",
892 ))
893 }
894
895 pub(crate) fn update_quota(&self, mut quota: Quota) -> Result<Quota, QuotumName> {
898 quota.object_count = quota
899 .object_count
900 .checked_sub(self.stored_objects.len().try_into().unwrap_or(u16::MAX))
901 .ok_or_else(|| {
902 let quotum = QuotumName::ObjectCount;
903 log::warn!("user {} has reached quotum {quotum}", self.id);
904 quotum
905 })?;
906
907 for sod in self.stored_objects.values() {
908 quota.object_bytes_total =
909 quota
910 .object_bytes_total
911 .checked_sub(sod.size)
912 .ok_or_else(|| {
913 let quotum = QuotumName::ObjectBytesTotal;
914 log::warn!("user {} has reached quotum {quotum}", self.id);
915 quotum
916 })?;
917 }
918
919 Ok(quota)
920 }
921
922 pub(crate) fn into_user_version(self: UserState, app: &App) -> ApiUserState {
924 ApiUserState {
925 allow_login_by: self.allow_login_by,
926 could_be_banned_by: self.could_be_banned_by,
927 stored_objects: self
928 .stored_objects
929 .into_iter()
930 .map(|(handle, uod)| (handle, uod.into_user_version(&app.user_object_hmac_secret)))
931 .collect(),
932 }
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 const DAY: u64 = 24 * 60 * 60;
941 const MAX_IDLE: core::time::Duration = core::time::Duration::from_secs(90 * DAY);
942 const MAX_SESSION: core::time::Duration = core::time::Duration::from_secs(365 * DAY);
943
944 fn token(session_start: u64, exp: u64) -> AuthTokenInner {
945 AuthTokenInner {
946 user_id: Id::random(),
947 exp: jwt::NumericDate::new_clamp(exp),
948 iat: jwt::NumericDate::new_clamp(exp - 3600),
949 session_start: jwt::NumericDate::new_clamp(session_start),
950 }
951 }
952
953 fn may_be_refreshed_at(token: &AuthTokenInner, now: u64) -> bool {
954 token.may_be_refreshed(jwt::NumericDate::new_clamp(now), MAX_IDLE, MAX_SESSION)
955 }
956
957 #[test]
958 fn refresh_limits() {
959 let start = 1_000_000_000;
960
961 assert!(may_be_refreshed_at(
963 &token(start, start + 3600),
964 start + 7200
965 ));
966
967 let t = token(start, start + 3600);
969 assert!(may_be_refreshed_at(&t, start + 3600 + 90 * DAY));
970 assert!(!may_be_refreshed_at(&t, start + 3600 + 90 * DAY + 1));
971
972 let t = token(start, start + 365 * DAY - 60);
974 assert!(may_be_refreshed_at(&t, start + 365 * DAY));
975 assert!(!may_be_refreshed_at(&t, start + 365 * DAY + 1));
976 }
977
978 #[test]
979 fn auth_token_keeps_session_start() {
980 let key: crypto::SealingKey = b"test"
981 .as_slice()
982 .derive_sealing_key(sha2::Sha256::new(), "test");
983
984 let t = token(1_000_000_000, 1_000_003_600);
985 let inner = AuthTokenInner::unseal(&t.seal(&key).unwrap(), &key).unwrap();
986 assert_eq!(inner.session_start, t.session_start);
987 }
988}