manabrew_relay_protocol/
identity_token.rs1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
9use base64::Engine;
10use serde::{Deserialize, Serialize};
11
12pub const HUB_ISSUER: &str = "manabrew-hub";
13pub const SELF_ISSUER: &str = "manabrew-client";
14pub const RELAY_AUDIENCE: &str = "manabrew-relay";
15pub const SIGNED_ALG: &str = "EdDSA";
16pub const UNSIGNED_ALG: &str = "none";
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct IdentityTokenHeader {
20 pub alg: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub kid: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct IdentityTokenClaims {
27 pub sub: String,
28 #[serde(default)]
29 pub handle: String,
30 pub iss: String,
31 pub aud: String,
32 pub iat: i64,
33 pub exp: i64,
34}
35
36#[derive(Debug, Clone)]
37pub struct DecodedIdentityToken {
38 pub header: IdentityTokenHeader,
39 pub claims: IdentityTokenClaims,
40 pub signing_input: String,
41 pub signature: Vec<u8>,
42}
43
44impl DecodedIdentityToken {
45 pub fn is_unsigned(&self) -> bool {
46 self.header.alg == UNSIGNED_ALG && self.signature.is_empty()
47 }
48}
49
50pub fn decode(token: &str) -> Option<DecodedIdentityToken> {
51 let mut parts = token.split('.');
52 let encoded_header = parts.next()?;
53 let encoded_claims = parts.next()?;
54 let encoded_signature = parts.next()?;
55 if parts.next().is_some() {
56 return None;
57 }
58
59 let header: IdentityTokenHeader =
60 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(encoded_header).ok()?).ok()?;
61 let claims: IdentityTokenClaims =
62 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(encoded_claims).ok()?).ok()?;
63 let signature = if encoded_signature.is_empty() {
64 Vec::new()
65 } else {
66 URL_SAFE_NO_PAD.decode(encoded_signature).ok()?
67 };
68
69 Some(DecodedIdentityToken {
70 header,
71 claims,
72 signing_input: format!("{encoded_header}.{encoded_claims}"),
73 signature,
74 })
75}
76
77pub fn mint_unsigned(subject: &str, handle: &str, iat: i64, ttl_s: i64) -> String {
78 let header = IdentityTokenHeader {
79 alg: UNSIGNED_ALG.to_string(),
80 kid: None,
81 };
82 let claims = IdentityTokenClaims {
83 sub: subject.to_string(),
84 handle: handle.to_string(),
85 iss: SELF_ISSUER.to_string(),
86 aud: RELAY_AUDIENCE.to_string(),
87 iat,
88 exp: iat + ttl_s,
89 };
90 let encoded_header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).expect("serialize"));
91 let encoded_claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).expect("serialize"));
92 format!("{encoded_header}.{encoded_claims}.")
93}