@@ -6,6 +6,7 @@ use crate::storage::storable::email_recovery_credential::StorableEmailRecoveryCr
66use crate :: storage:: storable:: fixed_anchor:: StorableFixedAnchor ;
77use crate :: storage:: storable:: passkey_credential:: StorablePasskeyCredential ;
88use crate :: storage:: storable:: recovery_key:: StorableRecoveryKey ;
9+ use crate :: storage:: storable:: session_device:: StorableSessionDevice ;
910use crate :: storage:: storable:: special_device_migration:: SpecialDeviceMigration ;
1011use crate :: storage:: storable:: verified_email:: StorableVerifiedEmail ;
1112use crate :: { IC0_APP_ORIGIN , ID_AI_ORIGIN , INTERNETCOMPUTER_ORG_ORIGIN } ;
@@ -38,11 +39,47 @@ pub struct Anchor {
3839 pub ( crate ) email_recovery : Vec < EmailRecoveryCredential > ,
3940 /// Capped by `MAX_VERIFIED_EMAILS_PER_ANCHOR`.
4041 pub ( crate ) verified_emails : Vec < VerifiedEmail > ,
42+ /// Capped by `MAX_SESSION_DEVICES`.
43+ pub ( crate ) session_devices : Vec < SessionDevice > ,
44+ pub ( crate ) next_session_device_id : SessionDeviceId ,
4145 pub ( crate ) metadata : Option < HashMap < String , MetadataEntry > > ,
4246 pub ( crate ) name : Option < String > ,
4347 pub ( crate ) created_at : Option < Timestamp > ,
4448}
4549
50+ /// Bounds the device list, because the anchor blob is read on nearly every
51+ /// authenticated path.
52+ pub const MAX_SESSION_DEVICES : usize = 20 ;
53+
54+ /// A browser this anchor has signed in from. The name is self-reported by the client,
55+ /// so it is a label for the user rather than evidence about where a session came from.
56+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
57+ pub struct SessionDevice {
58+ pub id : SessionDeviceId ,
59+ pub name : String ,
60+ pub created_at : Timestamp ,
61+ }
62+
63+ impl From < StorableSessionDevice > for SessionDevice {
64+ fn from ( value : StorableSessionDevice ) -> Self {
65+ SessionDevice {
66+ id : value. id ,
67+ name : value. name ,
68+ created_at : value. created_at ,
69+ }
70+ }
71+ }
72+
73+ impl From < SessionDevice > for StorableSessionDevice {
74+ fn from ( value : SessionDevice ) -> Self {
75+ StorableSessionDevice {
76+ id : value. id ,
77+ name : value. name ,
78+ created_at : value. created_at ,
79+ }
80+ }
81+ }
82+
4683impl Device {
4784 /// Applies the values of `device_data` to self while leaving the other fields intact.
4885 pub fn apply_device_data ( & mut self , device_data : DeviceData ) {
@@ -175,6 +212,8 @@ impl From<Anchor> for (StorableFixedAnchor, StorableAnchor) {
175212 openid_credentials,
176213 email_recovery,
177214 verified_emails,
215+ session_devices,
216+ next_session_device_id,
178217 metadata,
179218 name,
180219 created_at,
@@ -194,6 +233,13 @@ impl From<Anchor> for (StorableFixedAnchor, StorableAnchor) {
194233 . map ( StorableVerifiedEmail :: from)
195234 . collect ( ) ,
196235 ) ;
236+ let next_session_device_id = Some ( next_session_device_id) ;
237+ let session_devices = Some (
238+ session_devices
239+ . into_iter ( )
240+ . map ( StorableSessionDevice :: from)
241+ . collect ( ) ,
242+ ) ;
197243
198244 let ( mut passkey_credentials, mut recovery_keys, mut recovery_devices) =
199245 ( vec ! [ ] , vec ! [ ] , vec ! [ ] ) ;
@@ -433,6 +479,8 @@ impl From<Anchor> for (StorableFixedAnchor, StorableAnchor) {
433479 recovery_keys,
434480 email_recovery,
435481 verified_emails,
482+ session_devices,
483+ next_session_device_id,
436484 } ,
437485 )
438486 }
@@ -448,6 +496,8 @@ impl From<(AnchorNumber, StorableAnchor)> for Anchor {
448496 recovery_keys,
449497 email_recovery,
450498 verified_emails,
499+ session_devices,
500+ next_session_device_id,
451501 } = storable_anchor;
452502
453503 let name = name. clone ( ) ;
@@ -466,6 +516,12 @@ impl From<(AnchorNumber, StorableAnchor)> for Anchor {
466516 . into_iter ( )
467517 . map ( VerifiedEmail :: from)
468518 . collect ( ) ;
519+ let session_devices = session_devices
520+ . unwrap_or_default ( )
521+ . into_iter ( )
522+ . map ( SessionDevice :: from)
523+ . collect ( ) ;
524+ let next_session_device_id = next_session_device_id. unwrap_or_default ( ) ;
469525
470526 let mut devices = passkey_credentials
471527 . unwrap_or_default ( )
@@ -560,6 +616,8 @@ impl From<(AnchorNumber, StorableAnchor)> for Anchor {
560616 openid_credentials,
561617 email_recovery,
562618 verified_emails,
619+ session_devices,
620+ next_session_device_id,
563621 devices,
564622 metadata,
565623 }
@@ -586,6 +644,8 @@ impl From<(AnchorNumber, StorableFixedAnchor, Option<StorableAnchor>)> for Ancho
586644 openid_credentials : vec ! [ ] ,
587645 email_recovery : vec ! [ ] ,
588646 verified_emails : vec ! [ ] ,
647+ session_devices : vec ! [ ] ,
648+ next_session_device_id : 0 ,
589649 anchor_number,
590650 devices,
591651 metadata,
@@ -612,13 +672,21 @@ impl From<(AnchorNumber, StorableFixedAnchor, Option<StorableAnchor>)> for Ancho
612672 . into_iter ( )
613673 . map ( VerifiedEmail :: from)
614674 . collect ( ) ;
675+ let session_devices = storable_anchor
676+ . session_devices
677+ . unwrap_or_default ( )
678+ . into_iter ( )
679+ . map ( SessionDevice :: from)
680+ . collect ( ) ;
615681
616682 Anchor {
617683 anchor_number,
618684 devices,
619685 openid_credentials,
620686 email_recovery,
621687 verified_emails,
688+ session_devices,
689+ next_session_device_id : storable_anchor. next_session_device_id . unwrap_or_default ( ) ,
622690 metadata,
623691 name,
624692 created_at,
@@ -627,6 +695,61 @@ impl From<(AnchorNumber, StorableFixedAnchor, Option<StorableAnchor>)> for Ancho
627695}
628696
629697impl Anchor {
698+ pub fn session_devices ( & self ) -> & [ SessionDevice ] {
699+ & self . session_devices
700+ }
701+
702+ /// Resolves the browser a sign-in came from, registering it when the client has no
703+ /// id yet or presents one this anchor does not know.
704+ ///
705+ /// The client never chooses the id: an id it does not own resolves to a fresh
706+ /// registration rather than to somebody else's device. At the cap the oldest records
707+ /// are dropped rather than the registration failing, which never costs anyone a
708+ /// sign-in. Their ids are returned so the caller can end their sessions too: a
709+ /// session whose device is no longer listed could not be signed out from settings.
710+ pub fn resolve_session_device (
711+ & mut self ,
712+ device_id : Option < SessionDeviceId > ,
713+ name : String ,
714+ now : Timestamp ,
715+ ) -> ( SessionDeviceId , Vec < SessionDeviceId > ) {
716+ if let Some ( device_id) = device_id {
717+ if self
718+ . session_devices
719+ . iter ( )
720+ . any ( |device| device. id == device_id)
721+ {
722+ return ( device_id, vec ! [ ] ) ;
723+ }
724+ }
725+
726+ let id = self . next_session_device_id ;
727+ self . next_session_device_id = self . next_session_device_id . saturating_add ( 1 ) ;
728+ self . session_devices . push ( SessionDevice {
729+ id,
730+ name,
731+ created_at : now,
732+ } ) ;
733+
734+ let mut dropped = vec ! [ ] ;
735+ while self . session_devices . len ( ) > MAX_SESSION_DEVICES {
736+ let oldest = self
737+ . session_devices
738+ . iter ( )
739+ . enumerate ( )
740+ . min_by_key ( |( _, device) | ( device. created_at , device. id ) )
741+ . map ( |( index, _) | index) ;
742+ match oldest {
743+ Some ( index) => {
744+ dropped. push ( self . session_devices . remove ( index) . id ) ;
745+ }
746+ None => break ,
747+ }
748+ }
749+
750+ ( id, dropped)
751+ }
752+
630753 /// Creation of new anchors is restricted in order to make sure that the device checks are
631754 /// not accidentally bypassed.
632755 pub fn new ( anchor_number : AnchorNumber , created_at : Timestamp ) -> Anchor {
@@ -637,6 +760,8 @@ impl Anchor {
637760 openid_credentials : vec ! [ ] ,
638761 email_recovery : vec ! [ ] ,
639762 verified_emails : vec ! [ ] ,
763+ session_devices : vec ! [ ] ,
764+ next_session_device_id : 0 ,
640765 metadata : None ,
641766 name : None ,
642767 }
0 commit comments