Skip to main content

tf_types/
bridge_oauth.rs

1//! OAuth/GNAP bridge — verify a JWT bearer token using `jsonwebtoken`,
2//! against a static or remote JWKS, and project the verified claims into a
3//! TrustForge actor identity + capabilities.
4//!
5//! Supports ES256 / ES384 / RS256 / RS384 / RS512 / EdDSA. Algorithm
6//! confusion attacks (alg:none, HS256-with-RSA-key) are guarded by the
7//! mandatory allow-list passed at bridge construction time.
8
9use std::collections::HashMap;
10
11use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::bridges::{Bridge, BridgeError, BridgeKind};
16use crate::generated::{
17    ActorIdentity, ActorIdentity_IdentityVersion, ActorType, AuthorityRoot, AuthorityRoot_Kind,
18    PublicKey, PublicKey_Purpose, TrustLevel,
19};
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct OAuthBridgeConfig {
23    pub bridge_id: String,
24    pub trust_domain: String,
25    pub jwks: Jwks,
26    pub allowed_algorithms: Vec<String>,
27    pub issuer: String,
28    pub audience: Vec<String>,
29    #[serde(default = "default_clock_tolerance")]
30    pub clock_tolerance_seconds: u64,
31}
32
33fn default_clock_tolerance() -> u64 {
34    60
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct Jwks {
39    pub keys: Vec<Jwk>,
40}
41
42/// Minimal JWK shape the bridge accepts. ES256/ES384 use x/y; RS* use n/e;
43/// EdDSA uses crv=Ed25519 + x.
44#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct Jwk {
46    pub kty: String,
47    #[serde(default)]
48    pub alg: Option<String>,
49    #[serde(default)]
50    pub kid: Option<String>,
51    #[serde(default)]
52    pub crv: Option<String>,
53    #[serde(default)]
54    pub x: Option<String>,
55    #[serde(default)]
56    pub y: Option<String>,
57    #[serde(default)]
58    pub n: Option<String>,
59    #[serde(default)]
60    pub e: Option<String>,
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
64pub struct OAuthClaims {
65    pub iss: Option<String>,
66    pub sub: Option<String>,
67    pub aud: Option<Value>,
68    pub exp: Option<u64>,
69    pub iat: Option<u64>,
70    pub scope: Option<Value>,
71    #[serde(rename = "tf_actor_type", default)]
72    pub tf_actor_type: Option<String>,
73    #[serde(flatten)]
74    pub extra: HashMap<String, Value>,
75}
76
77#[derive(Clone, Debug)]
78pub struct OAuthVerificationResult {
79    pub identity: ActorIdentity,
80    pub capabilities: Vec<String>,
81    pub claims: OAuthClaims,
82}
83
84pub struct OAuthBridge {
85    cfg: OAuthBridgeConfig,
86}
87
88impl OAuthBridge {
89    pub fn new(cfg: OAuthBridgeConfig) -> Self {
90        OAuthBridge { cfg }
91    }
92
93    pub fn verify_token(&self, token: &str) -> Result<OAuthVerificationResult, BridgeError> {
94        if token.is_empty() {
95            return Err(BridgeError::InvalidInput("empty token".into()));
96        }
97        let header = decode_header(token)
98            .map_err(|e| BridgeError::Rejected(format!("malformed JWT: {}", e)))?;
99        let alg_name = format!("{:?}", header.alg);
100        if !self
101            .cfg
102            .allowed_algorithms
103            .iter()
104            .any(|a| a.eq_ignore_ascii_case(&alg_name))
105        {
106            return Err(BridgeError::Rejected(format!(
107                "algorithm {} not in allow-list",
108                alg_name
109            )));
110        }
111
112        let kid = header
113            .kid
114            .clone()
115            .ok_or_else(|| BridgeError::Rejected("JWT header missing kid".into()))?;
116        let jwk = self
117            .cfg
118            .jwks
119            .keys
120            .iter()
121            .find(|k| k.kid.as_deref() == Some(&kid))
122            .ok_or_else(|| BridgeError::Rejected(format!("no JWK with kid {}", kid)))?;
123        let key = decoding_key_for(jwk)?;
124
125        let mut validation = Validation::new(header.alg);
126        validation.set_issuer(&[self.cfg.issuer.as_str()]);
127        validation.set_audience(&self.cfg.audience);
128        validation.leeway = self.cfg.clock_tolerance_seconds;
129        validation.algorithms = vec![header.alg];
130
131        let data = decode::<OAuthClaims>(token, &key, &validation)
132            .map_err(|e| BridgeError::Rejected(format!("JWT verify failed: {}", e)))?;
133        let claims = data.claims;
134        let subject = claims
135            .sub
136            .clone()
137            .ok_or_else(|| BridgeError::Rejected("JWT missing sub claim".into()))?;
138        let actor_type_str = claims.tf_actor_type.as_deref().unwrap_or("human");
139        let actor_type = match actor_type_str {
140            "human" => ActorType::Human,
141            "agent" => ActorType::Agent,
142            "device" => ActorType::Device,
143            "service" => ActorType::Service,
144            "site" => ActorType::Site,
145            "organization" => ActorType::Organization,
146            other => {
147                return Err(BridgeError::Rejected(format!(
148                    "unsupported tf_actor_type: {}",
149                    other
150                )))
151            }
152        };
153        let encoded_subject = encode_subject(&subject);
154        let actor_id = format!(
155            "tf:actor:{}:{}/{}",
156            actor_type_str, self.cfg.trust_domain, encoded_subject
157        );
158
159        let identity = ActorIdentity {
160            identity_version: ActorIdentity_IdentityVersion::V1,
161            actor_id,
162            actor_type,
163            instance_id: None,
164            public_keys: vec![project_jwk_to_public_key(jwk)?],
165            trust_levels: vec![TrustLevel::T3],
166            authority_roots: vec![AuthorityRoot {
167                kind: AuthorityRoot_Kind::Organization,
168                id: self.cfg.issuer.clone(),
169            }],
170            attestations: None,
171            valid_from: claims
172                .iat
173                .map(timestamp)
174                .unwrap_or_else(|| timestamp(now_unix())),
175            valid_until: claims.exp.map(timestamp),
176            revocation_ref: None,
177            signature: None,
178        };
179
180        let capabilities = scopes_from_claims(&claims);
181
182        Ok(OAuthVerificationResult {
183            identity,
184            capabilities,
185            claims,
186        })
187    }
188}
189
190impl Bridge for OAuthBridge {
191    fn bridge_id(&self) -> &str {
192        &self.cfg.bridge_id
193    }
194    fn kind(&self) -> BridgeKind {
195        BridgeKind::Oauth
196    }
197    fn trust_domain(&self) -> &str {
198        &self.cfg.trust_domain
199    }
200}
201
202fn decoding_key_for(jwk: &Jwk) -> Result<DecodingKey, BridgeError> {
203    match jwk.kty.as_str() {
204        "EC" => {
205            let x = jwk
206                .x
207                .as_ref()
208                .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing x".into()))?;
209            let y = jwk
210                .y
211                .as_ref()
212                .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing y".into()))?;
213            DecodingKey::from_ec_components(x, y)
214                .map_err(|e| BridgeError::InvalidInput(format!("bad EC components: {}", e)))
215        }
216        "RSA" => {
217            let n = jwk
218                .n
219                .as_ref()
220                .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing n".into()))?;
221            let e = jwk
222                .e
223                .as_ref()
224                .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing e".into()))?;
225            DecodingKey::from_rsa_components(n, e)
226                .map_err(|e| BridgeError::InvalidInput(format!("bad RSA components: {}", e)))
227        }
228        "OKP" => {
229            let x = jwk
230                .x
231                .as_ref()
232                .ok_or_else(|| BridgeError::InvalidInput("OKP JWK missing x".into()))?;
233            DecodingKey::from_ed_components(x)
234                .map_err(|e| BridgeError::InvalidInput(format!("bad OKP components: {}", e)))
235        }
236        other => Err(BridgeError::InvalidInput(format!(
237            "unsupported kty {}",
238            other
239        ))),
240    }
241}
242
243fn encode_subject(s: &str) -> String {
244    // Percent-encode anything outside the unreserved RFC 3986 set so the
245    // subject can be embedded in an actor URI path segment.
246    let mut out = String::with_capacity(s.len());
247    for b in s.bytes() {
248        match b {
249            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
250                out.push(b as char);
251            }
252            _ => out.push_str(&format!("%{:02X}", b)),
253        }
254    }
255    out
256}
257
258fn scopes_from_claims(claims: &OAuthClaims) -> Vec<String> {
259    match &claims.scope {
260        Some(Value::String(s)) => s.split_whitespace().map(str::to_string).collect(),
261        Some(Value::Array(arr)) => arr
262            .iter()
263            .filter_map(|v| v.as_str().map(str::to_string))
264            .collect(),
265        _ => Vec::new(),
266    }
267}
268
269fn timestamp(t: u64) -> String {
270    // Format as RFC 3339 UTC.
271    let datetime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(t);
272    let secs = datetime
273        .duration_since(std::time::UNIX_EPOCH)
274        .expect("post-epoch")
275        .as_secs() as i64;
276    // Build YYYY-MM-DDTHH:MM:SSZ from secs without bringing chrono in.
277    let (year, month, day, hour, minute, second) = secs_to_ymdhms(secs);
278    format!(
279        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
280        year, month, day, hour, minute, second
281    )
282}
283
284fn now_unix() -> u64 {
285    std::time::SystemTime::now()
286        .duration_since(std::time::UNIX_EPOCH)
287        .unwrap_or_default()
288        .as_secs()
289}
290
291fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
292    // Civil-from-days algorithm by Howard Hinnant.
293    let days = secs.div_euclid(86_400);
294    let time = secs.rem_euclid(86_400);
295    let hour = (time / 3600) as u32;
296    let minute = ((time % 3600) / 60) as u32;
297    let second = (time % 60) as u32;
298
299    let z = days + 719_468;
300    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
301    let doe = (z - era * 146_097) as u64; // [0, 146096]
302    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
303    let y = yoe as i64 + era * 400;
304    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
305    let mp = (5 * doy + 2) / 153;
306    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
307    let m = if mp < 10 {
308        (mp + 3) as u32
309    } else {
310        (mp - 9) as u32
311    };
312    let year = if m <= 2 { y + 1 } else { y };
313    (year as i32, m, d, hour, minute, second)
314}
315
316pub fn parse_algorithm(name: &str) -> Result<Algorithm, BridgeError> {
317    match name.to_ascii_uppercase().as_str() {
318        "ES256" => Ok(Algorithm::ES256),
319        "ES384" => Ok(Algorithm::ES384),
320        "RS256" => Ok(Algorithm::RS256),
321        "RS384" => Ok(Algorithm::RS384),
322        "RS512" => Ok(Algorithm::RS512),
323        "EDDSA" => Ok(Algorithm::EdDSA),
324        other => Err(BridgeError::InvalidInput(format!(
325            "unsupported algorithm: {}",
326            other
327        ))),
328    }
329}
330
331/// Project a JWK into the TrustForge `PublicKey` shape (raw bytes,
332/// base64-encoded, with the algorithm name normalised to TrustForge's
333/// vocabulary). Mirrors the TS `projectJwkToPublicKey`.
334pub fn project_jwk_to_public_key(jwk: &Jwk) -> Result<PublicKey, BridgeError> {
335    use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
336    use base64::Engine;
337    let key_id = jwk
338        .kid
339        .clone()
340        .unwrap_or_else(|| "oauth-bridge-bearer".to_string());
341    match jwk.kty.as_str() {
342        "OKP" => {
343            // Ed25519 — raw 32-byte x.
344            let x = jwk
345                .x
346                .as_ref()
347                .ok_or_else(|| BridgeError::InvalidInput("OKP JWK missing x".into()))?;
348            let bytes = URL_SAFE_NO_PAD
349                .decode(x)
350                .map_err(|e| BridgeError::InvalidInput(format!("base64url x: {}", e)))?;
351            Ok(PublicKey {
352                key_id,
353                algorithm: "ed25519".into(),
354                public_key: STANDARD.encode(bytes),
355                purpose: PublicKey_Purpose::Signing,
356                valid_from: None,
357                valid_until: None,
358            })
359        }
360        "EC" => {
361            let x = jwk
362                .x
363                .as_ref()
364                .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing x".into()))?;
365            let y = jwk
366                .y
367                .as_ref()
368                .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing y".into()))?;
369            let xb = URL_SAFE_NO_PAD
370                .decode(x)
371                .map_err(|e| BridgeError::InvalidInput(format!("base64url x: {}", e)))?;
372            let yb = URL_SAFE_NO_PAD
373                .decode(y)
374                .map_err(|e| BridgeError::InvalidInput(format!("base64url y: {}", e)))?;
375            let mut sec1 = Vec::with_capacity(1 + xb.len() + yb.len());
376            sec1.push(0x04);
377            sec1.extend_from_slice(&xb);
378            sec1.extend_from_slice(&yb);
379            let crv = jwk.crv.as_deref().unwrap_or("");
380            let alg = match crv {
381                "P-256" => "p256",
382                "P-384" => "p384",
383                "P-521" => "p521",
384                _ => "ec",
385            };
386            Ok(PublicKey {
387                key_id,
388                algorithm: alg.into(),
389                public_key: STANDARD.encode(sec1),
390                purpose: PublicKey_Purpose::Signing,
391                valid_from: None,
392                valid_until: None,
393            })
394        }
395        "RSA" => {
396            let n = jwk
397                .n
398                .as_ref()
399                .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing n".into()))?;
400            let e = jwk
401                .e
402                .as_ref()
403                .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing e".into()))?;
404            let nb = URL_SAFE_NO_PAD
405                .decode(n)
406                .map_err(|err| BridgeError::InvalidInput(format!("base64url n: {}", err)))?;
407            let eb = URL_SAFE_NO_PAD
408                .decode(e)
409                .map_err(|err| BridgeError::InvalidInput(format!("base64url e: {}", err)))?;
410            let der = encode_rsa_spki(&nb, &eb);
411            Ok(PublicKey {
412                key_id,
413                algorithm: "rsa".into(),
414                public_key: STANDARD.encode(der),
415                purpose: PublicKey_Purpose::Signing,
416                valid_from: None,
417                valid_until: None,
418            })
419        }
420        other => Err(BridgeError::Unsupported(format!(
421            "unsupported JWK kty: {}",
422            other
423        ))),
424    }
425}
426
427fn encode_rsa_spki(n: &[u8], e: &[u8]) -> Vec<u8> {
428    let rsa_public_key = der_sequence(&[der_integer(n), der_integer(e)]);
429    let oid_rsa_encryption: [u8; 11] = [
430        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
431    ];
432    let null_params: [u8; 2] = [0x05, 0x00];
433    let alg_id = der_sequence(&[oid_rsa_encryption.to_vec(), null_params.to_vec()]);
434    let mut bit_string_body = Vec::with_capacity(1 + rsa_public_key.len());
435    bit_string_body.push(0x00);
436    bit_string_body.extend_from_slice(&rsa_public_key);
437    let mut bit_string = Vec::with_capacity(2 + bit_string_body.len());
438    bit_string.push(0x03);
439    bit_string.extend_from_slice(&der_len(bit_string_body.len()));
440    bit_string.extend_from_slice(&bit_string_body);
441    der_sequence(&[alg_id, bit_string])
442}
443
444fn der_sequence(parts: &[Vec<u8>]) -> Vec<u8> {
445    let body: Vec<u8> = parts.iter().flat_map(|p| p.clone()).collect();
446    let mut out = Vec::with_capacity(2 + body.len());
447    out.push(0x30);
448    out.extend_from_slice(&der_len(body.len()));
449    out.extend_from_slice(&body);
450    out
451}
452
453fn der_integer(bytes: &[u8]) -> Vec<u8> {
454    let mut start = 0usize;
455    while start < bytes.len() - 1 && bytes[start] == 0 {
456        start += 1;
457    }
458    let payload = &bytes[start..];
459    let needs_pad = payload[0] & 0x80 != 0;
460    let len = payload.len() + if needs_pad { 1 } else { 0 };
461    let mut out = Vec::with_capacity(2 + len);
462    out.push(0x02);
463    out.extend_from_slice(&der_len(len));
464    if needs_pad {
465        out.push(0x00);
466    }
467    out.extend_from_slice(payload);
468    out
469}
470
471fn der_len(n: usize) -> Vec<u8> {
472    if n < 0x80 {
473        return vec![n as u8];
474    }
475    let mut bytes = Vec::new();
476    let mut v = n;
477    while v > 0 {
478        bytes.insert(0, (v & 0xff) as u8);
479        v >>= 8;
480    }
481    let mut out = Vec::with_capacity(1 + bytes.len());
482    out.push(0x80 | bytes.len() as u8);
483    out.extend_from_slice(&bytes);
484    out
485}