Skip to main content

snap_tokens/
lib.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! SNAP token library.
15
16pub mod v0;
17pub mod v1;
18
19use std::time::SystemTime;
20
21use scion_sdk_token_validator::validator::Token;
22use serde::{Deserialize, Serialize};
23
24/// A wrapper that can handle any version of SNAP token claims.
25///
26/// It uses a custom deserializer to inspect the `ver` field:
27/// - `ver` matches a known version (e.g., 1): Deserializes into that version.
28/// - `ver` is missing: Falls back to V0 (legacy).
29/// - `ver` is unknown: Returns an error.
30#[derive(Debug, Clone, Serialize)]
31#[serde(untagged)]
32pub enum AnyClaims {
33    /// Version 1 SNAP token claims.
34    V1(v1::SnapTokenClaims),
35    /// Legacy Version 0 SNAP token claims.
36    V0(v0::SnapTokenClaims),
37}
38
39impl<'de> Deserialize<'de> for AnyClaims {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: serde::Deserializer<'de>,
43    {
44        let value = serde_json::Value::deserialize(deserializer)?;
45
46        // Inspect for "ver" to determine version explicitly.
47        // This ensures forward compatibility (we won't accidentally parse V2 as V1).
48        if let Some(ver) = value.get("ver") {
49            match ver.as_u64() {
50                Some(1) => {
51                    let claims: v1::SnapTokenClaims =
52                        serde_json::from_value(value).map_err(serde::de::Error::custom)?;
53                    Ok(AnyClaims::V1(claims))
54                }
55                Some(n) => {
56                    Err(serde::de::Error::custom(format!(
57                        "unsupported SNAP token version: {}",
58                        n
59                    )))
60                }
61                None => {
62                    Err(serde::de::Error::custom(
63                        "invalid SNAP token: 'ver' claim must be a number",
64                    ))
65                }
66            }
67        } else {
68            // No version claim -> Legacy V0
69            let claims: v0::SnapTokenClaims =
70                serde_json::from_value(value).map_err(serde::de::Error::custom)?;
71            Ok(AnyClaims::V0(claims))
72        }
73    }
74}
75impl AnyClaims {
76    /// Returns the PSSID as string from the token claims.
77    pub fn pssid(&self) -> String {
78        match self {
79            AnyClaims::V1(c) => c.pssid.to_string(),
80            AnyClaims::V0(c) => c.pssid.to_string(),
81        }
82    }
83
84    /// Returns the JWT ID.
85    pub fn jti(&self) -> String {
86        match self {
87            AnyClaims::V1(c) => c.jti.clone(),
88            AnyClaims::V0(c) => c.jti.clone(),
89        }
90    }
91}
92
93impl Token for AnyClaims {
94    fn id(&self) -> String {
95        match self {
96            Self::V1(c) => c.id(),
97            Self::V0(c) => c.id(),
98        }
99    }
100
101    fn exp_time(&self) -> SystemTime {
102        match self {
103            Self::V1(c) => c.exp_time(),
104            Self::V0(c) => c.exp_time(),
105        }
106    }
107
108    fn required_claims() -> Vec<&'static str> {
109        // We only enforce the intersection of claims required by *all* versions
110        // at the generic JWT validation layer.
111        vec!["exp", "pssid"]
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use serde_json::json;
118
119    use super::*;
120
121    #[test]
122    fn test_any_claims_dispatch() {
123        // Legacy V0 (No ver)
124        let v0_json = json!({
125            "jti": "jti_v0",
126            "exp": 2000000000,
127            "pssid": "ef16640f-0fa9-4360-be74-dbeec7ab4f9a"
128        });
129        let c: AnyClaims = serde_json::from_value(v0_json).expect("should parse as V0");
130        assert!(matches!(c, AnyClaims::V0(_)));
131
132        // V1 (ver = 1)
133        let v1_json = json!({
134            "ver": 1,
135            "jti": "jti_v1",
136            "iss": "ssr",
137            "aud": "snap",
138            "exp": 2000000000,
139            "nbf": 1000,
140            "iat": 1000,
141            "pssid": "AAAAAAAAAAAAAAAAAAAAAAA",
142            "aa_acc_subject_id": "subj",
143            "aa_acc_allowed_dst": "[]"
144        });
145        let c: AnyClaims = serde_json::from_value(v1_json).expect("should parse as V1");
146        assert!(matches!(c, AnyClaims::V1(_)));
147
148        // V2 (Future/Unsupported)
149        let v2_json = json!({
150            "ver": 2,
151            "exp": 2000000000
152        });
153        let err = serde_json::from_value::<AnyClaims>(v2_json).unwrap_err();
154        assert!(
155            err.to_string()
156                .contains("unsupported SNAP token version: 2")
157        );
158    }
159}