use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use super::error::UcanParseError;
use super::types::{UcanHeader, UcanPayload};
pub fn parse_jwt(token: &str) -> Result<UcanPayload, UcanParseError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(UcanParseError::MalformedJwt(parts.len()));
}
let (header_b64, payload_b64, _sig_b64) = (parts[0], parts[1], parts[2]);
let header_bytes = URL_SAFE_NO_PAD
.decode(header_b64)
.map_err(|e| UcanParseError::base64("header", e))?;
let header: UcanHeader =
serde_json::from_slice(&header_bytes).map_err(|e| UcanParseError::json("header", e))?;
if header.alg != "EdDSA" {
return Err(UcanParseError::UnsupportedAlg(header.alg));
}
if header.typ != "ucan/1.0+jwt" {
return Err(UcanParseError::UnsupportedTyp(header.typ));
}
if header.ucv != "1.0" {
return Err(UcanParseError::UnsupportedUcv(header.ucv));
}
let payload_bytes = URL_SAFE_NO_PAD
.decode(payload_b64)
.map_err(|e| UcanParseError::base64("payload", e))?;
let payload: UcanPayload =
serde_json::from_slice(&payload_bytes).map_err(|e| UcanParseError::json("payload", e))?;
if payload.cmd != "atd-cap" {
return Err(UcanParseError::NonAtdCap(payload.cmd));
}
require_did_key(&payload.iss, "iss")?;
require_did_key(&payload.aud, "aud")?;
Ok(payload)
}
fn require_did_key(did: &str, field: &'static str) -> Result<(), UcanParseError> {
if did.starts_with("did:key:z") {
Ok(())
} else {
Err(UcanParseError::UnsupportedDidMethod {
field,
did: did.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use serde_json::json;
fn build_jwt(header: serde_json::Value, payload: serde_json::Value) -> String {
let h = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
let p = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
let s = URL_SAFE_NO_PAD.encode(b"signature-placeholder");
format!("{h}.{p}.{s}")
}
fn canonical_header() -> serde_json::Value {
json!({ "alg": "EdDSA", "typ": "ucan/1.0+jwt", "ucv": "1.0" })
}
fn canonical_payload() -> serde_json::Value {
json!({
"iss": "did:key:z6MkA_abbreviated",
"aud": "did:key:z6MkB_abbreviated",
"sub": "did:key:z6MkU_abbreviated",
"cmd": "atd-cap",
"args": { "caps": ["records:read"], "with": [{"patient": "Patient/X"}] },
"nonce": "Mv3K0000000000000000",
"exp": 9999999999_i64
})
}
#[test]
fn parse_well_formed_token_succeeds() {
let tok = build_jwt(canonical_header(), canonical_payload());
let parsed = parse_jwt(&tok).expect("well-formed UCAN should parse");
assert_eq!(parsed.iss, "did:key:z6MkA_abbreviated");
assert_eq!(parsed.aud, "did:key:z6MkB_abbreviated");
assert_eq!(parsed.cmd, "atd-cap");
assert_eq!(parsed.args.caps, vec!["records:read"]);
assert_eq!(parsed.exp, 9999999999_i64);
}
#[test]
fn parse_unsupported_alg_rejects() {
let mut h = canonical_header();
h["alg"] = json!("RS256");
let tok = build_jwt(h, canonical_payload());
match parse_jwt(&tok) {
Err(UcanParseError::UnsupportedAlg(alg)) => assert_eq!(alg, "RS256"),
other => panic!("expected UnsupportedAlg, got {other:?}"),
}
}
#[test]
fn parse_non_did_key_issuer_rejects() {
let mut p = canonical_payload();
p["iss"] = json!("did:web:example.org");
let tok = build_jwt(canonical_header(), p);
match parse_jwt(&tok) {
Err(UcanParseError::UnsupportedDidMethod { field, did }) => {
assert_eq!(field, "iss");
assert_eq!(did, "did:web:example.org");
}
other => panic!("expected UnsupportedDidMethod(iss), got {other:?}"),
}
}
#[test]
fn parse_malformed_jwt_rejects() {
let tok = "abc.def";
match parse_jwt(tok) {
Err(UcanParseError::MalformedJwt(n)) => assert_eq!(n, 2),
other => panic!("expected MalformedJwt(2), got {other:?}"),
}
let tok4 = "a.b.c.d";
match parse_jwt(tok4) {
Err(UcanParseError::MalformedJwt(n)) => assert_eq!(n, 4),
other => panic!("expected MalformedJwt(4), got {other:?}"),
}
}
#[test]
fn parse_non_atd_cap_command_rejects() {
let mut p = canonical_payload();
p["cmd"] = json!("/bsky/post");
let tok = build_jwt(canonical_header(), p);
match parse_jwt(&tok) {
Err(UcanParseError::NonAtdCap(c)) => assert_eq!(c, "/bsky/post"),
other => panic!("expected NonAtdCap, got {other:?}"),
}
}
#[test]
fn parse_wrong_typ_rejects() {
let mut h = canonical_header();
h["typ"] = json!("JWT");
let tok = build_jwt(h, canonical_payload());
assert!(matches!(
parse_jwt(&tok),
Err(UcanParseError::UnsupportedTyp(_))
));
}
#[test]
fn parse_wrong_ucv_rejects() {
let mut h = canonical_header();
h["ucv"] = json!("0.9");
let tok = build_jwt(h, canonical_payload());
assert!(matches!(
parse_jwt(&tok),
Err(UcanParseError::UnsupportedUcv(_))
));
}
#[test]
fn parse_garbage_base64_in_header_rejects() {
let tok = "!!!.aaaa.bbbb";
assert!(matches!(
parse_jwt(tok),
Err(UcanParseError::Base64Decode {
segment: "header",
..
})
));
}
#[test]
fn parse_audience_non_did_key_rejects() {
let mut p = canonical_payload();
p["aud"] = json!("did:plc:abcdef");
let tok = build_jwt(canonical_header(), p);
match parse_jwt(&tok) {
Err(UcanParseError::UnsupportedDidMethod { field, .. }) => assert_eq!(field, "aud"),
other => panic!("expected UnsupportedDidMethod(aud), got {other:?}"),
}
}
}