authx_core/crypto/
signing.rs1use chrono::Utc;
2use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
3use serde::{Deserialize, Serialize};
4use tracing::instrument;
5use uuid::Uuid;
6
7use crate::error::{AuthError, Result};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Claims {
11 pub sub: String,
12 pub exp: i64,
13 pub iat: i64,
14 pub jti: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub org: Option<String>,
17 #[serde(flatten)]
18 pub extra: serde_json::Value,
19}
20
21pub struct TokenSigner {
22 encoding: EncodingKey,
23 decoding: DecodingKey,
24}
25
26impl TokenSigner {
27 pub fn from_ed25519_pem(private_pem: &[u8], public_pem: &[u8]) -> Result<Self> {
28 let encoding = EncodingKey::from_ed_pem(private_pem)
29 .map_err(|e| AuthError::Internal(format!("invalid private key: {e}")))?;
30 let decoding = DecodingKey::from_ed_pem(public_pem)
31 .map_err(|e| AuthError::Internal(format!("invalid public key: {e}")))?;
32 Ok(Self { encoding, decoding })
33 }
34
35 #[instrument(skip(self, extra))]
36 pub fn sign(
37 &self,
38 subject: Uuid,
39 ttl_seconds: i64,
40 extra: serde_json::Value,
41 ) -> Result<String> {
42 let now = Utc::now().timestamp();
43 let claims = Claims {
44 sub: subject.to_string(),
45 exp: now + ttl_seconds,
46 iat: now,
47 jti: Uuid::new_v4().to_string(),
48 org: None,
49 extra,
50 };
51 let token = encode(&Header::new(Algorithm::EdDSA), &claims, &self.encoding)
52 .map_err(|e| AuthError::Internal(format!("jwt sign failed: {e}")))?;
53
54 tracing::debug!(sub = %subject, "jwt signed");
55 Ok(token)
56 }
57
58 #[instrument(skip(self, token))]
59 pub fn verify(&self, token: &str) -> Result<Claims> {
60 let mut validation = Validation::new(Algorithm::EdDSA);
61 validation.validate_exp = true;
62
63 let data = decode::<Claims>(token, &self.decoding, &validation)
64 .map_err(|_| AuthError::InvalidToken)?;
65
66 tracing::debug!(sub = %data.claims.sub, "jwt verified");
67 Ok(data.claims)
68 }
69}