Skip to main content

paas_server/auth/
jwt.rs

1use crate::auth::core::{AuthInfo, TokenValidator};
2use actix_web::error::ErrorUnauthorized;
3use actix_web::Error;
4use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::future::Future;
8use std::path::Path;
9use std::pin::Pin;
10use std::sync::Arc;
11
12pub struct JWTAuthConfig {
13    pub jwt_key_path: String,
14    pub jwt_audience: String,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct Claims {
19    // Standard claims
20    pub sub: String,
21    #[serde(default)]
22    pub exp: Option<u64>,
23    #[serde(default)]
24    pub iat: Option<u64>,
25    #[serde(default)]
26    pub nbf: Option<u64>,
27    #[serde(default)]
28    pub iss: Option<String>,
29    #[serde(default)]
30    pub aud: Option<String>,
31
32    // Custom claims
33    #[serde(default)]
34    pub groups: Vec<String>,
35    #[serde(default)]
36    pub name: Option<String>,
37}
38
39#[derive(Clone)]
40pub struct JWTValidator {
41    jwt_key: Arc<DecodingKey>,
42    validation: Arc<Validation>,
43}
44
45#[derive(Default)]
46pub struct JWTValidatorBuilder {
47    jwt_key: Option<DecodingKey>,
48    audiences: Vec<String>,
49    issuer: Option<String>,
50    algorithm: Algorithm,
51    leeway: u64,
52    validate_exp: bool,
53    validate_nbf: bool,
54    validate_aud: bool,
55}
56
57impl JWTValidatorBuilder {
58    /// Create a new JWT validator builder with default values
59    pub fn new() -> Self {
60        Self {
61            jwt_key: None,
62            audiences: Vec::new(),
63            issuer: None,
64            algorithm: Algorithm::RS256,
65            leeway: 0,
66            validate_exp: true,
67            validate_nbf: true,
68            validate_aud: true,
69        }
70    }
71
72    /// Set the JWT key using a PEM-formatted RSA public key from file
73    pub fn with_rsa_pem_key_file<P: AsRef<Path>>(mut self, file_path: P) -> Result<Self, Error> {
74        let key_content = fs::read_to_string(file_path)
75            .map_err(|e| ErrorUnauthorized(format!("Failed to read JWT key file: {}", e)))?;
76
77        self.jwt_key = Some(
78            DecodingKey::from_rsa_pem(key_content.as_bytes())
79                .map_err(|e| ErrorUnauthorized(format!("Invalid RSA public key: {}", e)))?,
80        );
81
82        Ok(self)
83    }
84
85    /// Set the JWT key using a PEM-formatted RSA public key from string
86    pub fn with_rsa_pem_key(mut self, key_content: &str) -> Result<Self, Error> {
87        self.jwt_key = Some(
88            DecodingKey::from_rsa_pem(key_content.as_bytes())
89                .map_err(|e| ErrorUnauthorized(format!("Invalid RSA public key: {}", e)))?,
90        );
91
92        Ok(self)
93    }
94
95    /// Set the JWT key directly
96    pub fn with_key(mut self, key: DecodingKey) -> Self {
97        self.jwt_key = Some(key);
98        self
99    }
100
101    /// Add an audience that the JWT must be intended for
102    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
103        self.audiences.push(audience.into());
104        self
105    }
106
107    /// Add multiple audiences that the JWT may be intended for
108    pub fn with_audiences(
109        mut self,
110        audiences: impl IntoIterator<Item = impl Into<String>>,
111    ) -> Self {
112        self.audiences
113            .extend(audiences.into_iter().map(|a| a.into()));
114        self
115    }
116
117    /// Set the required issuer for the JWT
118    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
119        self.issuer = Some(issuer.into());
120        self
121    }
122
123    /// Set the algorithm used to sign the JWT
124    pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
125        self.algorithm = algorithm;
126        self
127    }
128
129    /// Set the leeway (in seconds) to account for clock skew
130    pub fn with_leeway(mut self, seconds: u64) -> Self {
131        self.leeway = seconds;
132        self
133    }
134
135    /// Enable or disable expiration validation
136    pub fn validate_expiration(mut self, validate: bool) -> Self {
137        self.validate_exp = validate;
138        self
139    }
140
141    /// Enable or disable not-before validation
142    pub fn validate_not_before(mut self, validate: bool) -> Self {
143        self.validate_nbf = validate;
144        self
145    }
146
147    /// Enable or disable audience validation
148    pub fn validate_audience(mut self, validate: bool) -> Self {
149        self.validate_aud = validate;
150        self
151    }
152
153    /// Build the JWTValidator instance
154    pub fn build(self) -> Result<JWTValidator, Error> {
155        let jwt_key = self
156            .jwt_key
157            .ok_or_else(|| ErrorUnauthorized("JWT key is required"))?;
158
159        let mut validation = Validation::new(self.algorithm);
160
161        // Set audiences if provided
162        if !self.audiences.is_empty() {
163            validation.set_audience(&self.audiences);
164        }
165
166        // Set issuer if provided
167        if let Some(issuer) = self.issuer {
168            validation.set_issuer(&[issuer]);
169        }
170
171        // Set validation options
172        validation.leeway = self.leeway;
173        validation.validate_exp = self.validate_exp;
174        validation.validate_nbf = self.validate_nbf;
175        validation.validate_aud = self.validate_aud;
176
177        Ok(JWTValidator {
178            jwt_key: Arc::new(jwt_key),
179            validation: Arc::new(validation),
180        })
181    }
182}
183
184impl JWTValidator {
185    /// Create a new builder for configuring the JWT validator
186    pub fn builder() -> JWTValidatorBuilder {
187        JWTValidatorBuilder::new()
188    }
189
190    /// Create a simple JWT validator with minimal configuration
191    pub fn new<P: AsRef<Path>>(
192        jwt_key_file: P,
193        audience: impl Into<String>,
194    ) -> Result<Self, Error> {
195        Self::builder()
196            .with_rsa_pem_key_file(jwt_key_file)?
197            .with_audience(audience)
198            .build()
199    }
200}
201
202impl TokenValidator for JWTValidator {
203    fn validate_token<'a>(
204        &'a self,
205        token: &'a str,
206    ) -> Pin<Box<dyn Future<Output = Result<AuthInfo, Error>> + Send + 'a>> {
207        Box::pin(async move {
208            // Decode and validate the token
209            match decode::<Claims>(token, &self.jwt_key, &self.validation) {
210                Ok(token_data) => {
211                    // Convert claims to AuthInfo
212                    // Use name if available, otherwise use sub
213                    let name = token_data
214                        .claims
215                        .name
216                        .clone()
217                        .unwrap_or_else(|| token_data.claims.sub.clone());
218                    Ok(AuthInfo {
219                        name,
220                        sub: token_data.claims.sub,
221                        groups: token_data.claims.groups,
222                    })
223                }
224                Err(e) => {
225                    // Return specific error based on validation failure
226                    Err(ErrorUnauthorized(format!("Invalid JWT: {}", e)))
227                }
228            }
229        })
230    }
231}