Skip to main content

oxihuman_core/
jwt_codec.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! JWT encode/decode stub — header.payload.signature (base64url, no crypto).
6
7/// Supported JWT algorithms (stub).
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum JwtAlgorithm {
10    HS256,
11    RS256,
12    None,
13}
14
15/// JWT header fields.
16#[derive(Clone, Debug)]
17pub struct JwtHeader {
18    pub alg: JwtAlgorithm,
19    pub typ: String,
20}
21
22/// JWT claims payload.
23#[derive(Clone, Debug)]
24pub struct JwtClaims {
25    pub sub: String,
26    pub iss: Option<String>,
27    pub aud: Option<String>,
28    pub exp: Option<u64>,
29    pub iat: Option<u64>,
30    pub jti: Option<String>,
31}
32
33/// A decoded JWT.
34#[derive(Clone, Debug)]
35pub struct DecodedJwt {
36    pub header: JwtHeader,
37    pub claims: JwtClaims,
38    pub signature: String,
39}
40
41/// Simple stub base64url encoding (not spec-compliant, for stub use only).
42pub fn base64url_encode(input: &str) -> String {
43    let encoded = input
44        .bytes()
45        .map(|b| format!("{:02x}", b))
46        .collect::<String>();
47    encoded.replace('=', "")
48}
49
50/// Encodes a JWT (stub — no real signing).
51pub fn jwt_encode(header: &JwtHeader, claims: &JwtClaims, _secret: &str) -> String {
52    let alg_str = match header.alg {
53        JwtAlgorithm::HS256 => "HS256",
54        JwtAlgorithm::RS256 => "RS256",
55        JwtAlgorithm::None => "none",
56    };
57    let h = base64url_encode(&format!(
58        r#"{{"alg":"{}","typ":"{}"}}"#,
59        alg_str, header.typ
60    ));
61    let p = base64url_encode(&format!(
62        r#"{{"sub":"{}","iss":"{}"}}"#,
63        claims.sub,
64        claims.iss.as_deref().unwrap_or("")
65    ));
66    let sig = base64url_encode(&format!("sig_{}", claims.sub));
67    format!("{}.{}.{}", h, p, sig)
68}
69
70/// Decodes a JWT string into its parts (stub — no signature verification).
71pub fn jwt_decode(token: &str) -> Result<DecodedJwt, String> {
72    let parts: Vec<&str> = token.splitn(3, '.').collect();
73    if parts.len() != 3 {
74        return Err("invalid JWT format".into());
75    }
76    Ok(DecodedJwt {
77        header: JwtHeader {
78            alg: JwtAlgorithm::HS256,
79            typ: "JWT".into(),
80        },
81        claims: JwtClaims {
82            sub: format!("stub_sub_from_{}", parts[1].len()),
83            iss: None,
84            aud: None,
85            exp: None,
86            iat: None,
87            jti: None,
88        },
89        signature: parts[2].to_owned(),
90    })
91}
92
93/// Returns true if the JWT token is structurally valid (has 3 dot-separated parts).
94pub fn jwt_is_structurally_valid(token: &str) -> bool {
95    token.splitn(4, '.').count() == 3
96}
97
98/// Returns the algorithm string from a header.
99pub fn algorithm_name(alg: JwtAlgorithm) -> &'static str {
100    match alg {
101        JwtAlgorithm::HS256 => "HS256",
102        JwtAlgorithm::RS256 => "RS256",
103        JwtAlgorithm::None => "none",
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_encode_produces_three_parts() {
113        let header = JwtHeader {
114            alg: JwtAlgorithm::HS256,
115            typ: "JWT".into(),
116        };
117        let claims = JwtClaims {
118            sub: "user1".into(),
119            iss: None,
120            aud: None,
121            exp: None,
122            iat: None,
123            jti: None,
124        };
125        let tok = jwt_encode(&header, &claims, "secret");
126        assert_eq!(tok.split('.').count(), 3 /* three-part JWT */);
127    }
128
129    #[test]
130    fn test_decode_valid_token() {
131        let header = JwtHeader {
132            alg: JwtAlgorithm::HS256,
133            typ: "JWT".into(),
134        };
135        let claims = JwtClaims {
136            sub: "user2".into(),
137            iss: Some("test".into()),
138            aud: None,
139            exp: Some(9999),
140            iat: None,
141            jti: None,
142        };
143        let tok = jwt_encode(&header, &claims, "s");
144        let decoded = jwt_decode(&tok).expect("should succeed");
145        assert!(!decoded.signature.is_empty());
146    }
147
148    #[test]
149    fn test_decode_invalid_token_returns_error() {
150        assert!(jwt_decode("bad_token").is_err());
151    }
152
153    #[test]
154    fn test_structural_validity_check() {
155        assert!(jwt_is_structurally_valid("a.b.c"));
156        assert!(!jwt_is_structurally_valid("a.b"));
157    }
158
159    #[test]
160    fn test_algorithm_name_hs256() {
161        assert_eq!(algorithm_name(JwtAlgorithm::HS256), "HS256");
162    }
163
164    #[test]
165    fn test_algorithm_name_none() {
166        assert_eq!(algorithm_name(JwtAlgorithm::None), "none");
167    }
168
169    #[test]
170    fn test_base64url_encode_not_empty() {
171        let out = base64url_encode("hello");
172        assert!(!out.is_empty());
173    }
174
175    #[test]
176    fn test_encode_with_no_algorithm() {
177        let header = JwtHeader {
178            alg: JwtAlgorithm::None,
179            typ: "JWT".into(),
180        };
181        let claims = JwtClaims {
182            sub: "anon".into(),
183            iss: None,
184            aud: None,
185            exp: None,
186            iat: None,
187            jti: None,
188        };
189        let tok = jwt_encode(&header, &claims, "");
190        assert!(tok.contains('.'));
191    }
192
193    #[test]
194    fn test_decode_preserves_signature_part() {
195        let tok = "aaa.bbb.ccc";
196        let decoded = jwt_decode(tok).expect("should succeed");
197        assert_eq!(decoded.signature, "ccc");
198    }
199}