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}