Skip to main content

typesec_integrations/jwt/
claims.rs

1//! JWT claim models and the verified-subject projection.
2
3use serde::{Deserialize, Serialize};
4
5/// Claims Typesec cares about from an access token.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct JwtClaims {
8    /// Subject identifier.
9    pub sub: String,
10    /// Issuer.
11    pub iss: String,
12    /// Audience. Some providers encode this as a string, others as a list.
13    pub aud: Audience,
14    /// Expiration timestamp.
15    pub exp: usize,
16    /// Optional organization identifier.
17    #[serde(default)]
18    pub org_id: Option<String>,
19    /// Optional organization membership identifier.
20    #[serde(default)]
21    pub organization_membership_id: Option<String>,
22    /// Optional role.
23    #[serde(default)]
24    pub role: Option<String>,
25    /// Optional permission list.
26    #[serde(default)]
27    pub permissions: Vec<String>,
28}
29
30/// JWT audience represented as either a string or list.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(untagged)]
33pub enum Audience {
34    /// Single audience.
35    Single(String),
36    /// Multiple audiences.
37    Multiple(Vec<String>),
38}
39
40impl Audience {
41    pub(super) fn contains(&self, needle: &str) -> bool {
42        match self {
43            Self::Single(value) => value == needle,
44            Self::Multiple(values) => values.iter().any(|value| value == needle),
45        }
46    }
47}
48
49/// Verified identity extracted from an OIDC/JWT access token.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct VerifiedSubject {
52    /// Subject identifier.
53    pub subject: String,
54    /// Optional organization identifier.
55    pub org_id: Option<String>,
56    /// Optional organization membership identifier.
57    pub organization_membership_id: Option<String>,
58    /// Role names carried by the token.
59    pub roles: Vec<String>,
60    /// Permission names carried by the token.
61    pub permissions: Vec<String>,
62}
63
64impl VerifiedSubject {
65    /// Return the best subject identifier for WorkOS FGA checks.
66    pub fn workos_membership_subject(&self) -> &str {
67        self.organization_membership_id
68            .as_deref()
69            .unwrap_or(&self.subject)
70    }
71}
72
73impl From<JwtClaims> for VerifiedSubject {
74    fn from(claims: JwtClaims) -> Self {
75        Self {
76            subject: claims.sub,
77            org_id: claims.org_id,
78            organization_membership_id: claims.organization_membership_id,
79            roles: claims.role.into_iter().collect(),
80            permissions: claims.permissions,
81        }
82    }
83}