1use base64::Engine as _;
14use serde::Deserialize;
15
16#[derive(Debug, Deserialize)]
19pub struct JwtHeader {
20 pub alg: String,
22 #[serde(default)]
24 pub typ: Option<String>,
25 #[serde(default)]
28 pub kid: Option<String>,
29}
30
31#[derive(Debug, Deserialize)]
36pub struct JwtPayload {
37 pub iss: String,
39 pub aud: String,
41 pub exp: i64,
43 pub iat: i64,
45 pub jti: String,
47 pub lxm: String,
50}
51
52#[derive(Debug)]
55pub struct ParsedJwt {
56 pub header: JwtHeader,
58 pub payload: JwtPayload,
60 pub signing_input: Vec<u8>,
63 pub signature: Vec<u8>,
66}
67
68pub fn parse(token: &str) -> Result<ParsedJwt, JwtParseError> {
72 let mut it = token.split('.');
73 let header_b64 = it.next().ok_or(JwtParseError::Structure)?;
74 let payload_b64 = it.next().ok_or(JwtParseError::Structure)?;
75 let sig_b64 = it.next().ok_or(JwtParseError::Structure)?;
76 if it.next().is_some() {
77 return Err(JwtParseError::Structure);
78 }
79 if header_b64.is_empty() || payload_b64.is_empty() || sig_b64.is_empty() {
80 return Err(JwtParseError::Structure);
81 }
82
83 let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
84 let header_bytes = engine
85 .decode(header_b64)
86 .map_err(|_| JwtParseError::Base64)?;
87 let payload_bytes = engine
88 .decode(payload_b64)
89 .map_err(|_| JwtParseError::Base64)?;
90 let signature = engine.decode(sig_b64).map_err(|_| JwtParseError::Base64)?;
91
92 let header: JwtHeader =
93 serde_json::from_slice(&header_bytes).map_err(|_| JwtParseError::HeaderJson)?;
94 let payload: JwtPayload =
95 serde_json::from_slice(&payload_bytes).map_err(|_| JwtParseError::PayloadJson)?;
96
97 let mut signing_input = Vec::with_capacity(header_b64.len() + 1 + payload_b64.len());
99 signing_input.extend_from_slice(header_b64.as_bytes());
100 signing_input.push(b'.');
101 signing_input.extend_from_slice(payload_b64.as_bytes());
102
103 Ok(ParsedJwt {
104 header,
105 payload,
106 signing_input,
107 signature,
108 })
109}
110
111#[derive(Debug, thiserror::Error)]
115pub enum JwtParseError {
116 #[error("JWT structure invalid (not three non-empty segments)")]
118 Structure,
119 #[error("JWT segment failed base64url decode")]
121 Base64,
122 #[error("JWT header is not valid JSON or is missing required fields")]
125 HeaderJson,
126 #[error("JWT payload is not valid JSON or is missing required claims")]
130 PayloadJson,
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
137
138 fn encode_jwt(header: &str, payload: &str, sig: &[u8]) -> String {
139 format!(
140 "{}.{}.{}",
141 URL_SAFE_NO_PAD.encode(header),
142 URL_SAFE_NO_PAD.encode(payload),
143 URL_SAFE_NO_PAD.encode(sig)
144 )
145 }
146
147 #[test]
148 fn parses_well_formed_jwt() {
149 let token = encode_jwt(
150 r#"{"alg":"ES256K","typ":"JWT"}"#,
151 r#"{"iss":"did:plc:a","aud":"did:plc:b","exp":100,"iat":50,"jti":"j","lxm":"m"}"#,
152 &[0x42; 64],
153 );
154 let jwt = parse(&token).expect("parse");
155 assert_eq!(jwt.header.alg, "ES256K");
156 assert_eq!(jwt.payload.iss, "did:plc:a");
157 assert_eq!(jwt.payload.aud, "did:plc:b");
158 assert_eq!(jwt.payload.exp, 100);
159 assert_eq!(jwt.payload.lxm, "m");
160 assert_eq!(jwt.signature.len(), 64);
161 assert!(jwt.signing_input.contains(&b'.'));
163 }
164
165 #[test]
166 fn rejects_wrong_segment_count() {
167 assert!(matches!(
168 parse("only.two").unwrap_err(),
169 JwtParseError::Structure
170 ));
171 assert!(matches!(
172 parse("a.b.c.d").unwrap_err(),
173 JwtParseError::Structure
174 ));
175 assert!(matches!(parse("").unwrap_err(), JwtParseError::Structure));
176 }
177
178 #[test]
179 fn rejects_empty_segments() {
180 assert!(matches!(
181 parse("a..c").unwrap_err(),
182 JwtParseError::Structure
183 ));
184 assert!(matches!(
185 parse(".b.c").unwrap_err(),
186 JwtParseError::Structure
187 ));
188 }
189
190 #[test]
191 fn rejects_non_base64() {
192 let token = "!!!.???.@@@";
193 assert!(matches!(parse(token).unwrap_err(), JwtParseError::Base64));
194 }
195
196 #[test]
197 fn rejects_missing_claims() {
198 let token = encode_jwt(
199 r#"{"alg":"ES256K"}"#,
200 r#"{"iss":"x"}"#, &[0; 64],
202 );
203 assert!(matches!(
204 parse(&token).unwrap_err(),
205 JwtParseError::PayloadJson
206 ));
207 }
208
209 #[test]
210 fn rejects_missing_alg_in_header() {
211 let token = encode_jwt(
212 r#"{"typ":"JWT"}"#,
213 r#"{"iss":"did:plc:a","aud":"did:plc:b","exp":100,"iat":50,"jti":"j","lxm":"m"}"#,
214 &[0; 64],
215 );
216 assert!(matches!(
217 parse(&token).unwrap_err(),
218 JwtParseError::HeaderJson
219 ));
220 }
221
222 #[test]
223 fn signing_input_is_un_decoded_bytes() {
224 let header = r#"{"alg":"ES256K"}"#;
225 let payload =
226 r#"{"iss":"did:plc:a","aud":"did:plc:b","exp":100,"iat":50,"jti":"j","lxm":"m"}"#;
227 let token = encode_jwt(header, payload, &[0; 64]);
228 let jwt = parse(&token).expect("parse");
229
230 assert!(jwt.signing_input != format!("{header}.{payload}").as_bytes());
234 assert_eq!(
235 jwt.signing_input,
236 format!(
237 "{}.{}",
238 URL_SAFE_NO_PAD.encode(header),
239 URL_SAFE_NO_PAD.encode(payload)
240 )
241 .into_bytes()
242 );
243 }
244}