architect_api/auth/
jwt.rs

1//! JWT authentication claims for upstream gRPC services
2
3use super::Grants;
4use derive_more::Deref;
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7
8#[derive(Debug, Deref, Clone, Serialize, Deserialize)]
9pub struct Claims<'a> {
10    pub aud: Cow<'a, str>,
11    pub exp: i64,
12    pub iat: i64,
13    pub iss: Cow<'a, str>,
14    pub nbf: i64,
15    pub sub: Cow<'a, str>,
16    #[serde(flatten)]
17    #[deref]
18    pub grants: Grants,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Jwk<'a> {
23    /// JWT key id; understood to be SHAKE256(issuer/subject/domain),
24    /// truncated to 128 bits, and hex-encoded
25    pub kid: Cow<'a, str>,
26    /// Base64-encoded RSA modulus (big-endian)
27    pub n: Cow<'a, str>,
28    /// Base64-encoded RSA exponent (big-endian)
29    pub e: Cow<'a, str>,
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn test_claims_serialization() {
38        // legacy JWT
39        let json = r#"
40        {
41            "aud": "*.architect.co",
42            "exp": 1751396859,
43            "iat": 1719859200,
44            "iss": "app.architect.co",
45            "nbf": 1751310459,
46            "sub": "test@architect.co"
47        }
48        "#;
49        let claims: Claims = serde_json::from_str(json).unwrap();
50        assert_eq!(claims.is_scoped(), false);
51        assert_eq!(claims.allowed_to_marketdata(&"CME".into()), true);
52        // new scoped JWT
53        let json = r#"
54        {
55            "aud": "*.architect.co",
56            "exp": 1751396859,
57            "iat": 1719859200,
58            "iss": "app.architect.co",
59            "nbf": 1751310459,
60            "sub": "test@architect.co",
61            "scoped": true,
62            "marketdata": ["CME"]
63        }
64        "#;
65        let claims: Claims = serde_json::from_str(json).unwrap();
66        assert_eq!(claims.is_scoped(), true);
67        assert_eq!(claims.allowed_to_marketdata(&"CME".into()), true);
68        let json = r#"
69        {
70            "aud": "*.architect.co",
71            "exp": 1751396859,
72            "iat": 1719859200,
73            "iss": "app.architect.co",
74            "nbf": 1751310459,
75            "sub": "test@architect.co",
76            "scoped": true
77        }
78        "#;
79        let claims: Claims = serde_json::from_str(json).unwrap();
80        assert_eq!(claims.is_scoped(), true);
81        assert_eq!(claims.allowed_to_marketdata(&"CME".into()), false);
82        let json = r#"
83        {
84            "aud": "*.architect.co",
85            "exp": 1751396859,
86            "iat": 1719859200,
87            "iss": "app.architect.co",
88            "nbf": 1751310459,
89            "sub": "test@architect.co",
90            "scoped": true,
91            "marketdata": true
92        }
93        "#;
94        let claims: Claims = serde_json::from_str(json).unwrap();
95        assert_eq!(claims.is_scoped(), true);
96        assert_eq!(claims.allowed_to_marketdata(&"CME".into()), true);
97    }
98}