Skip to main content

pubhubs/servers/phc/
user.rs

1//! Basic user endpoints, such as [`EnterEP`].
2use 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    /// Implements [`WelcomeEP`]
28    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    /// Implements [`CachedHubInfoEP`]
45    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    /// Implements [`StateEP`]
52    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    /// Implements [`EnterEP`]
67    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 => { /* Ok */ }
115            }
116        }
117
118        // Check attributes are valid
119        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        // will be filled while getting and putting attributes to the object store
197        let mut attr_states: std::collections::HashMap<
198            Id,
199            (AttrState, object_store::UpdateVersion),
200        > = Default::default();
201
202        // Items are added to `attr_state` incidentally until at some point we loop over all
203        // attributes in attrs that are not yet in `attr_states`.  When this happens depends on
204        // whether the user account already exists or not. To keep track of whether it happened
205        // we've added the following boolean.
206        let mut retrieved_attr_states = false;
207
208        // keeps track of which attributes have already been added
209        let mut attr_add_status: HashMap<Id, AttrAddStatus> = Default::default();
210
211        // Attributes are fine, check if we have a user account, or create it if need be
212        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                // see if account exists
233                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            // we need to be careful with the order of things here lest we leave the object
298            // store in a broken state.
299            //
300            //  1. Add the user account object.  Include the identifying attributes in the user
301            //     account already - they can be added later - but do not include the bannable
302            //     attributes. If this fails, the client just needs to register
303            //     again.
304            //
305            //  2. Add the identifying attribute pointing to the user account.  If this fails, the
306            //     client can always register again, and we're only left with an orphaned account.
307            //
308            //  3. Add the other attributes.  If this fails the user can always add the attributes
309            //     again using the identifying attribute already registered.
310            //
311            //  4. Modify the user account to register the added bannable attributes.  If this
312            //     fails, the user can always re-add those bannable attributes.
313            //
314            //  Here, we're only doing steps 1 and 2. Steps 3 and 4 are shared with regular login.
315
316            // The master encryption key is held off-wire in PHC's running state and is only set
317            // once PHC has unsealed the transcryptor's master key part; until then we cannot mint
318            // a polymorphic pseudonym, so the client should retry.
319            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                // NOTE: `could_be_banned_by` is set after the bannable attributes have been added
342                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            // Add identifying attribute.  We do not expect the attribute to exist, because
354            // otherwise we would be logging in, not registering.
355            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        // Add the missing attributes.  First the attribute states.
423        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        // Now check that all the bannable attributes ban this user
458        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        // Finally check that the attributes are added to the user's account state
499        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    /// Computes and caches the [`Id`] of an [`Attr`].
561    fn id_attr(&self, attr: Attr) -> IdedAttr {
562        IdedAttr {
563            id: attr.id(&*self.attr_id_secret),
564            attr,
565        }
566    }
567
568    /// Pre-checks whether the given attributes in `attrs` are suitable for
569    /// registering a new user.
570    ///
571    /// Potential problems:
572    ///  1. None of the given attributes is bannable.
573    ///  2. One of the attributes is banned
574    ///  3. One of the attributes already identifies another user.
575    ///  4. One if the attributes already bans another user (if
576    ///     `register_only_with_unique_attrs` is set
577    ///
578    /// Might try to retrieve attribute states for attributes not already in `attr_states`,
579    /// and will add those to `attr_states`. If it did, will set `retrieved_attr_states`.
580    ///
581    /// Returns `Ok(None)` when there are no issues.
582    ///
583    /// The situation can, of course, change between the time of the check and the time of
584    /// registration.
585    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        // Before doing potentially expensive queries to the object store, make sure a bannable
593        // attribute has been provided by the client
594        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        // Retrieve attributes states in so far they are available
601        for (attr_id, attr) in attrs {
602            // TODO: parallelize?
603            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    /// Implements [`RefreshEP`]
640    pub(super) async fn handle_user_refresh(
641        app: Rc<Self>,
642        auth_token: actix_web::web::Header<AuthToken>,
643    ) -> api::Result<RefreshResp> {
644        // the `true` means we allow expired access tokens
645        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    /// Issues auth token for given user, if allowed
668    ///
669    /// `session_start` is when the user last logged in, carried over from token to token.
670    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/// Plaintext content of [`AuthToken`].
699#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
700pub(super) struct AuthTokenInner {
701    /// The [`Id`] of the user to whom this token has been issued.
702    user_id: Id,
703
704    /// When this token expires.
705    exp: jwt::NumericDate,
706
707    /// When this token was issued.
708    iat: jwt::NumericDate,
709
710    /// When the user logged in, i.e. the `iat` of the first token in the chain of refreshes that
711    /// led to this token.
712    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    /// Checks whether this [`AuthToken`] is still valid, unless `accept_expired` is set.
731    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    /// Whether a new auth token may be issued based on this (possibly expired) one at `now`,
740    /// without the user logging in again.
741    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    /// Opens the given [`AuthToken`] returning the enclosed user's [`Id`].
769    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    /// Like [`Self::open_auth_token`], but returns the whole contents of the auth token, and has
774    /// the option to accept an expired auth token.
775    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    /// Opens the given [`AuthToken`] and retrieve the associated [`UserState`].
784    ///
785    /// Returns `Ok(Err(Opaque))` when the auth token was invalid.
786    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    /// Retrieves the [`UserState`] of the user an auth token was issued to.
798    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/// An [`Attr`] with its [`Id`].
814#[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/// Details pubhubs central stores about a user's account
836#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
837pub struct UserState {
838    /// Randomly generated identifier for this account, used for creating access tokens and such
839    pub id: Id,
840
841    /// Used as registration pseudonym on pubhubs cards issued for to this user.
842    ///
843    /// Might not be set for users that registered under v3.0.0, but will be upon entering pubhubs.
844    ///
845    /// If not set, it's derived from [`UserState::id`], which is done by [`UserState::card_id()`].
846    #[serde(default)]
847    card_id: Option<CardPseud>,
848
849    /// Registration date for this user
850    ///
851    /// Might not be set for users that registered under v3.0.0.
852    #[serde(default)]
853    pub registration_date: Option<api::NumericDate>,
854
855    /// Randomly generated identifier used to generate hub pseudonyms for this user, ElGamal
856    /// encrypted under the PubHubs master encryption key (`x_T x_PHC B`).
857    pub polymorphic_pseudonym: elgamal::Triple,
858
859    /// Whether this account is banned
860    pub banned: bool,
861
862    // TODO: limit number of allow_login_by attributes
863    /// Attributes that may be used to log in as this user,
864    /// provided that [`AttrState::may_identify_user`] also points to this account.
865    ///
866    /// The user may remove an attribute from this list.
867    pub allow_login_by: HashSet<Id>,
868
869    /// Attributes that when banned will ban this user
870    ///
871    /// The user can only add attributes to this list, but not remove them.
872    ///
873    /// This list is used to keep track of whether there is at least one attribute that would
874    /// ban this user.  If there are none, the user must add a bannable attribute before they can
875    /// login in.
876    pub could_be_banned_by: HashSet<Id>,
877
878    /// Details about the objects stored by this user at pubhubs central
879    pub stored_objects: HashMap<handle::Handle, super::user_object_store::UserObjectDetails>,
880}
881
882impl UserState {
883    /// Returns [`UserState::card_id`] when available, and otherwise an [`Id`] derived from [`UserState::id`].
884    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    /// Subtract quota usage from the given [`Quota`], returning an error when a [`QuotumName`] was
896    /// reached.
897    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    /// Turns this [`UserState`] into a [`ApiUserState`].
923    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        // recently expired token of a fresh session
962        assert!(may_be_refreshed_at(
963            &token(start, start + 3600),
964            start + 7200
965        ));
966
967        // idle limit, measured from expiry
968        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        // session limit, measured from login, regardless of activity
973        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}