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