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";
17pub const GUEST_SUBJECT_PREFIX: &str = "guest:";
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct IdentityTokenHeader {
24 pub alg: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub kid: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct IdentityTokenClaims {
31 pub sub: String,
32 #[serde(default)]
33 pub handle: String,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub qualification: Option<String>,
36 pub iss: String,
37 pub aud: String,
38 pub iat: i64,
39 pub exp: i64,
40}
41
42#[derive(Debug, Clone)]
43pub struct DecodedIdentityToken {
44 pub header: IdentityTokenHeader,
45 pub claims: IdentityTokenClaims,
46 pub signing_input: String,
47 pub signature: Vec<u8>,
48}
49
50impl DecodedIdentityToken {
51 pub fn is_unsigned(&self) -> bool {
52 self.header.alg == UNSIGNED_ALG && self.signature.is_empty()
53 }
54}
55
56pub fn decode(token: &str) -> Option<DecodedIdentityToken> {
57 let mut parts = token.split('.');
58 let encoded_header = parts.next()?;
59 let encoded_claims = parts.next()?;
60 let encoded_signature = parts.next()?;
61 if parts.next().is_some() {
62 return None;
63 }
64
65 let header: IdentityTokenHeader =
66 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(encoded_header).ok()?).ok()?;
67 let claims: IdentityTokenClaims =
68 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(encoded_claims).ok()?).ok()?;
69 let signature = if encoded_signature.is_empty() {
70 Vec::new()
71 } else {
72 URL_SAFE_NO_PAD.decode(encoded_signature).ok()?
73 };
74
75 Some(DecodedIdentityToken {
76 header,
77 claims,
78 signing_input: format!("{encoded_header}.{encoded_claims}"),
79 signature,
80 })
81}
82
83pub fn mint_unsigned(subject: &str, handle: &str, iat: i64, ttl_s: i64) -> String {
84 let header = IdentityTokenHeader {
85 alg: UNSIGNED_ALG.to_string(),
86 kid: None,
87 };
88 let claims = IdentityTokenClaims {
89 sub: subject.to_string(),
90 handle: handle.to_string(),
91 qualification: None,
92 iss: SELF_ISSUER.to_string(),
93 aud: RELAY_AUDIENCE.to_string(),
94 iat,
95 exp: iat + ttl_s,
96 };
97 let encoded_header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).expect("serialize"));
98 let encoded_claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).expect("serialize"));
99 format!("{encoded_header}.{encoded_claims}.")
100}