Skip to main content

plexus_auth_core/
verified_user.rs

1//! `VerifiedUser` — sealed proof that an IdP-signed token was verified.
2//!
3//! Possessing a `VerifiedUser` value is *proof* the framework verified an
4//! IdP-signed token. The constructor is `pub(crate)` to plexus-auth-core,
5//! so no other crate can fabricate one — the only path to producing a
6//! `VerifiedUser` runs through the (forthcoming) verifier inside this crate.
7//!
8//! Per AUTHZ-0 §"The sealed-type pattern":
9//!
10//! - **No fabrication.** Constructor is crate-private.
11//! - **No backdoor `From` / `Into`.** Orphan rules forbid foreign-trait
12//!   impls for this foreign type from a third crate.
13//! - **No accidental `Default`.** Not derived; a default would be
14//!   anonymous-with-no-claims, easy to confuse with verified-anonymous.
15//! - **No leaky `Deserialize`.** Not derived; raw JSON cannot fabricate a
16//!   sealed value.
17//! - **No mutation.** Fields are private; no setters.
18
19use serde::Serialize;
20
21/// Sealed proof that an IdP-signed token was verified.
22///
23/// Carries the verified claims that the framework extracted from the signed
24/// token (`user_id`, `issuer`, `issued_at`, `expires_at`). The presence of a
25/// `VerifiedUser` value is itself the proof — there is no way to construct
26/// one from outside `plexus-auth-core`.
27///
28/// # Sealing
29///
30/// The constructor is `pub(crate)`. Only the verifier inside this crate
31/// (which validates the IdP signature) is able to mint a `VerifiedUser`.
32/// `tests/compile_fail/seal_verified_user_construct.rs` asserts that no
33/// external crate can construct one.
34#[derive(Debug, Clone, Serialize)]
35pub struct VerifiedUser {
36    user_id: String,
37    issuer: String,
38    issued_at: i64,
39    expires_at: i64,
40}
41
42impl VerifiedUser {
43    /// Mint a new `VerifiedUser`.
44    ///
45    /// This is `pub(crate)` — only the verifier inside `plexus-auth-core`
46    /// can call it. External crates that need to produce a `VerifiedUser`
47    /// must route through the (forthcoming) verifier API; they cannot
48    /// construct one directly.
49    ///
50    /// `dead_code` is allowed because the verifier code that consumes this
51    /// constructor lands in a follow-up ticket (AUTHZ-FLOWS-DEVIDP-1 / the
52    /// JWT verifier work). The constructor itself must exist now so the
53    /// trybuild compile-fail tests can assert external crates can't reach
54    /// it; suppressing the warning here is preferable to a fake call site.
55    #[allow(dead_code)]
56    pub(crate) fn new_sealed(
57        user_id: String,
58        issuer: String,
59        issued_at: i64,
60        expires_at: i64,
61    ) -> Self {
62        Self {
63            user_id,
64            issuer,
65            issued_at,
66            expires_at,
67        }
68    }
69
70    /// The verified user identifier (e.g., the `sub` claim of the JWT).
71    pub fn user_id(&self) -> &str {
72        &self.user_id
73    }
74
75    /// The token issuer (e.g., the `iss` claim).
76    pub fn issuer(&self) -> &str {
77        &self.issuer
78    }
79
80    /// Token-issued-at unix timestamp.
81    pub fn issued_at(&self) -> i64 {
82        self.issued_at
83    }
84
85    /// Token expiry unix timestamp.
86    pub fn expires_at(&self) -> i64 {
87        self.expires_at
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn verified_user_carries_claims() {
97        // Crate-private constructor is reachable from inside the crate.
98        let v = VerifiedUser::new_sealed(
99            "alice".to_string(),
100            "https://idp.example.com".to_string(),
101            1_700_000_000,
102            1_700_003_600,
103        );
104        assert_eq!(v.user_id(), "alice");
105        assert_eq!(v.issuer(), "https://idp.example.com");
106        assert_eq!(v.issued_at(), 1_700_000_000);
107        assert_eq!(v.expires_at(), 1_700_003_600);
108    }
109}