gsm_core/platforms/webchat/
auth.rs1use std::sync::Arc;
2
3use anyhow::Context;
4use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
5use once_cell::sync::OnceCell;
6use serde::{Deserialize, Serialize};
7use time::{Duration as TimeDuration, OffsetDateTime};
8
9use super::config::SigningKeys;
10
11const ISSUER: &str = "greentic.webchat";
12const AUDIENCE: &str = "directline";
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct TenantClaims {
17 pub env: String,
18 pub tenant: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub team: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct Claims {
26 pub iss: String,
27 pub aud: String,
28 pub sub: String,
29 pub exp: i64,
30 pub iat: i64,
31 pub nbf: i64,
32 pub ctx: TenantClaims,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub conv: Option<String>,
35}
36
37impl Claims {
38 pub fn new(sub: String, ctx: TenantClaims, ttl: TimeDuration) -> Self {
40 let now = OffsetDateTime::now_utc();
41 let exp = now + ttl;
42 Self {
43 iss: ISSUER.into(),
44 aud: AUDIENCE.into(),
45 sub,
46 exp: exp.unix_timestamp(),
47 iat: now.unix_timestamp(),
48 nbf: now.unix_timestamp(),
49 ctx,
50 conv: None,
51 }
52 }
53
54 pub fn with_conversation(mut self, conversation_id: impl Into<String>) -> Self {
56 self.conv = Some(conversation_id.into());
57 self
58 }
59
60 pub fn has_conversation(&self, conversation_id: &str) -> bool {
62 self.conv
63 .as_ref()
64 .map(|conv| conv.eq_ignore_ascii_case(conversation_id))
65 .unwrap_or(false)
66 }
67}
68
69#[derive(Clone)]
71pub struct JwtKeys {
72 encoding: EncodingKey,
73 decoding: DecodingKey,
74}
75
76impl JwtKeys {
77 fn from_config(keys: &SigningKeys) -> anyhow::Result<Self> {
78 let encoding = EncodingKey::from_secret(keys.secret.as_bytes());
79 let decoding = DecodingKey::from_secret(keys.secret.as_bytes());
80 Ok(Self { encoding, decoding })
81 }
82}
83
84static ACTIVE_KEYS: OnceCell<Arc<JwtKeys>> = OnceCell::new();
85
86pub fn install_keys(keys: SigningKeys) -> anyhow::Result<()> {
88 ACTIVE_KEYS
89 .set(Arc::new(JwtKeys::from_config(&keys)?))
90 .map_err(|_| anyhow::anyhow!("JWT keys have already been installed"))
91}
92
93fn active_keys() -> anyhow::Result<Arc<JwtKeys>> {
94 ACTIVE_KEYS
95 .get()
96 .cloned()
97 .context("JWT signing keys not initialised")
98}
99
100pub fn sign(claims: &Claims) -> anyhow::Result<String> {
102 let keys = active_keys()?;
103 let header = Header {
104 alg: Algorithm::HS256,
105 ..Header::default()
106 };
107 let token = jsonwebtoken::encode(&header, claims, &keys.encoding)?;
108 Ok(token)
109}
110
111pub fn verify(token: &str) -> anyhow::Result<Claims> {
113 let keys = active_keys()?;
114 let mut validation = Validation::new(Algorithm::HS256);
115 validation.set_audience(&[AUDIENCE]);
116 validation.set_issuer(&[ISSUER]);
117 validation.leeway = 5; let data = jsonwebtoken::decode::<Claims>(token, &keys.decoding, &validation)?;
119 Ok(data.claims)
120}
121
122pub fn ttl(seconds: u64) -> TimeDuration {
124 TimeDuration::seconds(seconds as i64)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn install_test_keys() {
132 static INSTALL: once_cell::sync::OnceCell<()> = once_cell::sync::OnceCell::new();
133 INSTALL.get_or_init(|| {
134 let _ = install_keys(SigningKeys {
135 secret: "test-signing-key".into(),
136 });
137 });
138 }
139
140 #[test]
141 fn sign_and_verify_round_trip() {
142 install_test_keys();
143 let claims = Claims::new(
144 "user-1".into(),
145 TenantClaims {
146 env: "dev".into(),
147 tenant: "acme".into(),
148 team: Some("support".into()),
149 },
150 ttl(600),
151 )
152 .with_conversation("conv-42");
153 let token = sign(&claims).expect("token");
154 let parsed = verify(&token).expect("verify");
155 assert_eq!(parsed.sub, claims.sub);
156 assert_eq!(parsed.ctx.tenant, claims.ctx.tenant);
157 assert_eq!(parsed.conv, Some("conv-42".into()));
158 assert!(parsed.exp >= parsed.iat);
159 }
160}