mcpkit_core/auth/
identity.rs1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct VerifiedUser {
21 pub subject: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub issuer: Option<String>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub audience: Vec<String>,
29}
30
31impl VerifiedUser {
32 #[must_use]
34 pub fn new(subject: impl Into<String>) -> Self {
35 Self {
36 subject: subject.into(),
37 issuer: None,
38 audience: Vec::new(),
39 }
40 }
41
42 #[must_use]
44 pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
45 self.issuer = Some(issuer.into());
46 self
47 }
48
49 #[must_use]
51 pub fn audience<I, S>(mut self, audience: I) -> Self
52 where
53 I: IntoIterator<Item = S>,
54 S: Into<String>,
55 {
56 self.audience = audience.into_iter().map(Into::into).collect();
57 self
58 }
59
60 #[must_use]
64 pub fn is_same_user(&self, other: &Self) -> bool {
65 self.subject == other.subject && self.issuer == other.issuer
66 }
67
68 #[cfg(feature = "jwt")]
74 #[must_use]
75 pub fn from_claims(claims: &crate::auth::jwt::TokenClaims) -> Option<Self> {
76 use crate::auth::jwt::Audience;
77 let subject = claims.sub.clone()?;
78 let audience = match &claims.aud {
79 Some(Audience::Single(a)) => vec![a.clone()],
80 Some(Audience::Multiple(v)) => v.clone(),
81 None => Vec::new(),
82 };
83 Some(Self {
84 subject,
85 issuer: claims.iss.clone(),
86 audience,
87 })
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum SessionBindingError {
94 IdentityRequired,
96 IdentityMismatch,
98 UnexpectedIdentity,
102}
103
104impl std::fmt::Display for SessionBindingError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 let msg = match self {
107 Self::IdentityRequired => "session requires a verified identity",
108 Self::IdentityMismatch => "session is bound to a different user",
109 Self::UnexpectedIdentity => "anonymous session cannot be used with a verified identity",
110 };
111 f.write_str(msg)
112 }
113}
114
115impl std::error::Error for SessionBindingError {}
116
117pub fn check_session_binding(
126 bound: Option<&VerifiedUser>,
127 presenting: Option<&VerifiedUser>,
128) -> Result<(), SessionBindingError> {
129 match (bound, presenting) {
130 (None, None) => Ok(()),
131 (None, Some(_)) => Err(SessionBindingError::UnexpectedIdentity),
132 (Some(_), None) => Err(SessionBindingError::IdentityRequired),
133 (Some(b), Some(p)) if b.is_same_user(p) => Ok(()),
134 (Some(_), Some(_)) => Err(SessionBindingError::IdentityMismatch),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn same_user_compares_issuer_and_subject_not_audience() {
144 let a = VerifiedUser::new("alice")
145 .issuer("https://idp")
146 .audience(["res-1"]);
147 let b = VerifiedUser::new("alice")
148 .issuer("https://idp")
149 .audience(["res-2"]);
150 assert!(a.is_same_user(&b), "audience must not affect identity");
151
152 let c = VerifiedUser::new("alice").issuer("https://other");
153 assert!(!a.is_same_user(&c), "different issuer is a different user");
154
155 let d = VerifiedUser::new("bob").issuer("https://idp");
156 assert!(!a.is_same_user(&d), "different subject is a different user");
157 }
158
159 #[test]
160 fn binding_rules() {
161 let alice = VerifiedUser::new("alice").issuer("https://idp");
162 let bob = VerifiedUser::new("bob").issuer("https://idp");
163
164 assert!(check_session_binding(None, None).is_ok());
166 assert!(check_session_binding(Some(&alice), Some(&alice)).is_ok());
168 assert_eq!(
170 check_session_binding(Some(&alice), None),
171 Err(SessionBindingError::IdentityRequired)
172 );
173 assert_eq!(
175 check_session_binding(Some(&alice), Some(&bob)),
176 Err(SessionBindingError::IdentityMismatch)
177 );
178 assert_eq!(
180 check_session_binding(None, Some(&alice)),
181 Err(SessionBindingError::UnexpectedIdentity)
182 );
183 }
184}