use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifiedUser {
pub subject: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub audience: Vec<String>,
}
impl VerifiedUser {
#[must_use]
pub fn new(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
issuer: None,
audience: Vec::new(),
}
}
#[must_use]
pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
#[must_use]
pub fn audience<I, S>(mut self, audience: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.audience = audience.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn is_same_user(&self, other: &Self) -> bool {
self.subject == other.subject && self.issuer == other.issuer
}
#[cfg(feature = "jwt")]
#[must_use]
pub fn from_claims(claims: &crate::auth::jwt::TokenClaims) -> Option<Self> {
use crate::auth::jwt::Audience;
let subject = claims.sub.clone()?;
let audience = match &claims.aud {
Some(Audience::Single(a)) => vec![a.clone()],
Some(Audience::Multiple(v)) => v.clone(),
None => Vec::new(),
};
Some(Self {
subject,
issuer: claims.iss.clone(),
audience,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionBindingError {
IdentityRequired,
IdentityMismatch,
UnexpectedIdentity,
}
impl std::fmt::Display for SessionBindingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Self::IdentityRequired => "session requires a verified identity",
Self::IdentityMismatch => "session is bound to a different user",
Self::UnexpectedIdentity => "anonymous session cannot be used with a verified identity",
};
f.write_str(msg)
}
}
impl std::error::Error for SessionBindingError {}
pub fn check_session_binding(
bound: Option<&VerifiedUser>,
presenting: Option<&VerifiedUser>,
) -> Result<(), SessionBindingError> {
match (bound, presenting) {
(None, None) => Ok(()),
(None, Some(_)) => Err(SessionBindingError::UnexpectedIdentity),
(Some(_), None) => Err(SessionBindingError::IdentityRequired),
(Some(b), Some(p)) if b.is_same_user(p) => Ok(()),
(Some(_), Some(_)) => Err(SessionBindingError::IdentityMismatch),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_user_compares_issuer_and_subject_not_audience() {
let a = VerifiedUser::new("alice")
.issuer("https://idp")
.audience(["res-1"]);
let b = VerifiedUser::new("alice")
.issuer("https://idp")
.audience(["res-2"]);
assert!(a.is_same_user(&b), "audience must not affect identity");
let c = VerifiedUser::new("alice").issuer("https://other");
assert!(!a.is_same_user(&c), "different issuer is a different user");
let d = VerifiedUser::new("bob").issuer("https://idp");
assert!(!a.is_same_user(&d), "different subject is a different user");
}
#[test]
fn binding_rules() {
let alice = VerifiedUser::new("alice").issuer("https://idp");
let bob = VerifiedUser::new("bob").issuer("https://idp");
assert!(check_session_binding(None, None).is_ok());
assert!(check_session_binding(Some(&alice), Some(&alice)).is_ok());
assert_eq!(
check_session_binding(Some(&alice), None),
Err(SessionBindingError::IdentityRequired)
);
assert_eq!(
check_session_binding(Some(&alice), Some(&bob)),
Err(SessionBindingError::IdentityMismatch)
);
assert_eq!(
check_session_binding(None, Some(&alice)),
Err(SessionBindingError::UnexpectedIdentity)
);
}
}