arcly_http_identity/federation/
idtoken.rs1use async_trait::async_trait;
16use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
17use serde::Deserialize;
18
19use crate::error::{IdentityError, Result};
20
21#[derive(Debug, Clone)]
23pub struct VerifiedIdToken {
24 pub iss: String,
25 pub sub: String,
26 pub aud: String,
27 pub email: Option<String>,
28 pub email_verified: bool,
29 pub name: Option<String>,
30 pub nonce: Option<String>,
31 pub extra: serde_json::Map<String, serde_json::Value>,
33}
34
35#[async_trait]
38pub trait IdTokenVerifier: Send + Sync + 'static {
39 async fn verify(&self, id_token: &str, expected_nonce: Option<&str>)
42 -> Result<VerifiedIdToken>;
43}
44
45#[derive(Debug, Deserialize)]
46struct RawIdClaims {
47 iss: String,
48 sub: String,
49 #[serde(default)]
52 email: Option<String>,
53 #[serde(default)]
54 email_verified: Option<serde_json::Value>,
55 #[serde(default)]
56 name: Option<String>,
57 #[serde(default)]
58 nonce: Option<String>,
59 #[serde(flatten)]
60 extra: serde_json::Map<String, serde_json::Value>,
61}
62
63struct KeyEntry {
65 kid: Option<String>,
66 alg: Algorithm,
67 key: DecodingKey,
68}
69
70pub struct JwtIdTokenVerifier {
73 issuer: String,
74 audience: String,
75 keys: Vec<KeyEntry>,
76 leeway_secs: u64,
77}
78
79impl JwtIdTokenVerifier {
80 pub fn new(issuer: impl Into<String>, audience: impl Into<String>) -> Self {
83 Self {
84 issuer: issuer.into(),
85 audience: audience.into(),
86 keys: Vec::new(),
87 leeway_secs: 60,
88 }
89 }
90
91 pub fn add_rsa_pem(mut self, kid: Option<String>, alg: Algorithm, pem: &str) -> Result<Self> {
93 let key = DecodingKey::from_rsa_pem(pem.as_bytes())
94 .map_err(|e| IdentityError::Internal(format!("id_token key: {e}")))?;
95 self.keys.push(KeyEntry { kid, alg, key });
96 Ok(self)
97 }
98
99 pub fn add_ec_pem(mut self, kid: Option<String>, alg: Algorithm, pem: &str) -> Result<Self> {
101 let key = DecodingKey::from_ec_pem(pem.as_bytes())
102 .map_err(|e| IdentityError::Internal(format!("id_token key: {e}")))?;
103 self.keys.push(KeyEntry { kid, alg, key });
104 Ok(self)
105 }
106
107 fn select_key(&self, kid: Option<&str>) -> Option<&KeyEntry> {
108 self.keys
111 .iter()
112 .find(|k| k.kid.as_deref() == kid && kid.is_some())
113 .or_else(|| {
114 if self.keys.len() == 1 {
115 self.keys.first()
116 } else {
117 self.keys.iter().find(|k| k.kid.is_none())
118 }
119 })
120 }
121}
122
123#[async_trait]
124impl IdTokenVerifier for JwtIdTokenVerifier {
125 async fn verify(
126 &self,
127 id_token: &str,
128 expected_nonce: Option<&str>,
129 ) -> Result<VerifiedIdToken> {
130 let header = decode_header(id_token).map_err(|_| IdentityError::InvalidToken)?;
131 let entry = self
132 .select_key(header.kid.as_deref())
133 .ok_or(IdentityError::InvalidToken)?;
134
135 let mut validation = Validation::new(entry.alg);
136 validation.set_issuer(&[self.issuer.as_str()]);
137 validation.set_audience(&[self.audience.as_str()]);
138 validation.validate_exp = true;
139 validation.leeway = self.leeway_secs;
140
141 let data = decode::<RawIdClaims>(id_token, &entry.key, &validation)
142 .map_err(|_| IdentityError::InvalidToken)?;
143 let claims = data.claims;
144
145 if let Some(expected) = expected_nonce {
147 match &claims.nonce {
148 Some(n) if n == expected => {}
149 _ => return Err(IdentityError::InvalidToken),
150 }
151 }
152
153 let email_verified = matches!(
156 claims.email_verified,
157 Some(serde_json::Value::Bool(true)) | Some(serde_json::Value::String(_)) ) && !matches!(
159 claims.email_verified,
160 Some(serde_json::Value::String(ref s)) if s != "true"
161 );
162
163 Ok(VerifiedIdToken {
164 iss: claims.iss,
165 sub: claims.sub,
166 aud: self.audience.clone(),
167 email: claims.email,
168 email_verified,
169 name: claims.name,
170 nonce: claims.nonce,
171 extra: claims.extra,
172 })
173 }
174}