stynx_code_bridge/infrastructure/
jwt_auth.rs1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use stynx_code_errors::{AppError, AppResult};
5use serde::{Deserialize, Serialize};
6use subtle::ConstantTimeEq;
7
8type HmacSha256 = Hmac<Sha256>;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct JwtClaims {
12 pub sub: String,
13 pub exp: u64,
14 pub iat: u64,
15}
16
17#[derive(Debug, Clone, Deserialize)]
18struct JwtHeader {
19 alg: String,
20}
21
22pub struct JwtValidator {
23 secret: Vec<u8>,
24}
25
26impl JwtValidator {
27 pub fn from_env() -> AppResult<Self> {
28 let secret = std::env::var("STYNX_BRIDGE_SECRET")
29 .or_else(|_| std::env::var("CLAUDE_SERVER_TOKEN"))
30 .map_err(|_| AppError::Provider(
31 "STYNX_BRIDGE_SECRET (or legacy CLAUDE_SERVER_TOKEN) must be set for JWT signature verification".into(),
32 ))?;
33 if secret.len() < 32 {
34 return Err(AppError::Provider(
35 "STYNX_BRIDGE_SECRET must be at least 32 bytes for HS256 safety".into(),
36 ));
37 }
38 Ok(Self { secret: secret.into_bytes() })
39 }
40
41 pub fn with_secret(secret: impl Into<Vec<u8>>) -> Self {
42 Self { secret: secret.into() }
43 }
44
45 pub fn validate_token(&self, token: &str) -> AppResult<JwtClaims> {
46 let parts: Vec<&str> = token.splitn(3, '.').collect();
47 if parts.len() != 3 {
48 return Err(AppError::Unauthorized);
49 }
50
51 let header_bytes = URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| AppError::Unauthorized)?;
52 let header: JwtHeader = serde_json::from_slice(&header_bytes).map_err(|_| AppError::Unauthorized)?;
53 if header.alg != "HS256" {
54 return Err(AppError::Unauthorized);
55 }
56
57 let signing_input = format!("{}.{}", parts[0], parts[1]);
58 let expected_sig = {
59 let mut mac = HmacSha256::new_from_slice(&self.secret)
60 .map_err(|_| AppError::Unauthorized)?;
61 mac.update(signing_input.as_bytes());
62 mac.finalize().into_bytes()
63 };
64
65 let provided_sig = URL_SAFE_NO_PAD
66 .decode(parts[2])
67 .map_err(|_| AppError::Unauthorized)?;
68
69 if expected_sig.ct_eq(&provided_sig).unwrap_u8() != 1 {
70 return Err(AppError::Unauthorized);
71 }
72
73 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| AppError::Unauthorized)?;
74 let claims: JwtClaims = serde_json::from_slice(&payload_bytes).map_err(|_| AppError::Unauthorized)?;
75
76 let now = std::time::SystemTime::now()
77 .duration_since(std::time::UNIX_EPOCH)
78 .map(|d| d.as_secs())
79 .unwrap_or(0);
80 if claims.exp <= now {
81 return Err(AppError::Unauthorized);
82 }
83 if claims.iat > now + 60 {
84 return Err(AppError::Unauthorized);
85 }
86
87 Ok(claims)
88 }
89}