Skip to main content

rust_microservice/
security.rs

1pub mod oauth2 {
2    use std::collections::{HashMap, HashSet};
3
4    use colored::Colorize;
5    use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation, decode, decode_header};
6    use regex::Regex;
7    use serde::{Deserialize, Serialize};
8    use thiserror::Error;
9    use tracing::warn;
10
11    use crate::settings::Settings;
12
13    /// A type alias for a `Result` with the `ServerError` error type.
14    pub type Result<T, E = OAuth2Error> = std::result::Result<T, E>;
15
16    /// Represents an authentication token response typically returned by an
17    /// OAuth2 / OpenID Connect authorization server.
18    ///
19    /// This structure contains access credentials and metadata required to
20    /// authenticate requests and manage token lifecycle, including expiration
21    /// and refresh information.
22    ///
23    /// All fields are optional to support partial responses from different
24    /// identity providers.
25    ///
26    /// # Fields
27    ///
28    /// * `access_token` — The token used to authenticate API requests.
29    /// * `expires_in` — Lifetime of the access token in seconds.
30    /// * `refresh_expires_in` — Lifetime of the refresh token in seconds.
31    /// * `refresh_token` — Token used to obtain a new access token when the current one expires.
32    /// * `token_type` — Type of the token (commonly `"Bearer"`).
33    /// * `id_token` — OpenID Connect ID token containing user identity claims.
34    /// * `session_state` — Identifier for the authenticated session.
35    /// * `scope` — Space-separated list of granted permissions.
36    ///
37    /// # Serialization
38    ///
39    /// This struct supports serialization and deserialization via `serde`,
40    /// making it suitable for use with JSON-based authentication responses.
41    ///
42    /// # Example
43    ///
44    /// ```no_run
45    /// use rust_microservice::Token;
46    ///
47    /// let token = Token {
48    ///     access_token: Some("abc123".to_string()),
49    ///     expires_in: Some(3600),
50    ///     refresh_expires_in: Some(7200),
51    ///     refresh_token: Some("refresh_abc123".to_string()),
52    ///     token_type: Some("Bearer".to_string()),
53    ///     id_token: None,
54    ///     session_state: None,
55    ///     scope: Some("openid profile email".to_string()),
56    /// };
57    /// ```
58    #[derive(Debug, Serialize, Deserialize)]
59    pub struct Token {
60        pub access_token: Option<String>,
61        pub expires_in: Option<u64>,
62        pub refresh_expires_in: Option<u64>,
63        pub refresh_token: Option<String>,
64        pub token_type: Option<String>,
65        pub id_token: Option<String>,
66        pub session_state: Option<String>,
67        pub scope: Option<String>,
68    }
69
70    /// Represents the payload used for authentication requests following
71    /// the OAuth2-style "password" or "client credentials" grant patterns.
72    ///
73    /// This structure is typically deserialized from an HTTP request body
74    /// with `application/x-www-form-urlencoded` or JSON content, depending
75    /// on the server configuration.
76    ///
77    /// Field names are serialized/deserialized using **kebab-case**
78    /// to match common OAuth2 conventions.
79    ///
80    /// # Fields
81    ///
82    /// - `grant_type`
83    ///   The authorization grant type that defines how the access token
84    ///   should be issued (e.g., `"password"`, `"client-credentials"`).
85    ///
86    /// - `username`
87    ///   The resource owner’s username. Required when using the `"password"`
88    ///   grant type.
89    ///
90    /// - `password`
91    ///   The resource owner’s password. Required when using the `"password"`
92    ///   grant type.
93    ///
94    /// - `client_id`
95    ///   The client application identifier issued during client registration.
96    ///   Required for client authentication.
97    ///
98    /// - `client_secret`
99    ///   The client application secret. Required for confidential clients
100    ///   when authenticating with the authorization server.
101    ///
102    /// - `scope`
103    ///   A space-delimited list of requested permission scopes that define
104    ///   the level of access being requested.
105    ///
106    /// # Serialization
107    ///
108    /// This struct derives `Serialize` and `Deserialize` and uses
109    /// `#[serde(rename_all = "kebab-case")]`, meaning a field like
110    /// `client_id` becomes `client-id` in the serialized representation.
111    ///
112    /// # Example JSON
113    ///
114    /// ```json
115    /// {
116    ///   "grant-type": "password",
117    ///   "username": "user@example.com",
118    ///   "password": "secret",
119    ///   "client-id": "my-client",
120    ///   "client-secret": "super-secret",
121    ///   "scope": "read write"
122    /// }
123    /// ```
124    #[derive(Debug, Serialize, Deserialize)]
125    #[serde(rename_all = "kebab-case")]
126    pub struct LoginForm {
127        pub grant_type: String,
128        pub username: Option<String>,
129        pub password: Option<String>,
130        pub client_id: Option<String>,
131        pub client_secret: Option<String>,
132        pub scope: Option<String>,
133    }
134
135    impl LoginForm {
136        pub fn to_urlencoded(&self) -> String {
137            let mut urlencoded = String::new();
138            urlencoded.push_str("grant_type=");
139            urlencoded.push_str(&self.grant_type);
140            urlencoded.push_str("&username=");
141            urlencoded.push_str(self.username.as_ref().unwrap_or(&String::new()));
142            urlencoded.push_str("&password=");
143            urlencoded.push_str(self.password.as_ref().unwrap_or(&String::new()));
144            urlencoded.push_str("&client_id=");
145            urlencoded.push_str(self.client_id.as_ref().unwrap_or(&String::new()));
146            urlencoded.push_str("&client_secret=");
147            urlencoded.push_str(self.client_secret.as_ref().unwrap_or(&String::new()));
148            urlencoded.push_str("&scope=");
149            urlencoded.push_str(self.scope.as_ref().unwrap_or(&String::new()));
150            urlencoded
151        }
152    }
153
154    #[derive(Debug, Error)]
155    pub enum OAuth2Error {
156        #[error("Invalid OAuth2 configuration: {0}")]
157        Configuration(String),
158
159        #[error("Invalid JWT token: {0}")]
160        InvalidJwt(String),
161
162        #[error("Invalid server public key: {0}")]
163        InvalidPublicKey(String),
164
165        #[error("JWT Decode error: {0}")]
166        JWTDecode(String),
167
168        #[error("Unauthorized: {0}")]
169        Unauthorized(String),
170
171        #[error("Error parsing authorization: {0}")]
172        RoleAuthorizationParse(String),
173
174        #[error("Invalid roles: {0}")]
175        InvalidRoles(String),
176    }
177
178    #[derive(Debug, Serialize, Deserialize)]
179    struct Claims {
180        // Optional. Audience
181        aud: Option<String>,
182
183        // Required (validate_exp defaults to true in validation).
184        // Expiration time (as UTC timestamp)
185        exp: Option<usize>,
186
187        // Optional. Issued at (as UTC timestamp)
188        iat: Option<usize>,
189
190        // Optional. Issuer
191        iss: Option<String>,
192
193        // Optional. Not Before (as UTC timestamp)
194        nbf: Option<usize>,
195
196        // Optional. Subject (whom token refers to)
197        sub: Option<String>,
198
199        // Optional. Auth Scopes
200        scope: Option<String>,
201
202        // Optional. Resource Access
203        resource_access: Option<HashMap<String, Realm>>,
204    }
205
206    impl Claims {
207        /// Returns an optional HashSet of roles if the JWT token contains a
208        /// "resource_access" claim.
209        ///
210        /// The roles are extracted from the "resource_access" claim in the
211        /// JWT token, which is a map of resource names to Realm objects.
212        /// The roles are then flattened and collected into a HashSet.
213        ///
214        /// If the JWT token does not contain a "resource_access" claim, or
215        /// if the claim is empty, an empty Option is returned.
216        pub fn get_roles(&self) -> Option<HashSet<String>> {
217            Some(
218                self.resource_access
219                    .as_ref()?
220                    .values()
221                    .flat_map(|v| v.roles.clone())
222                    .map(|role| {
223                        format!(
224                            "ROLE_{}",
225                            role.to_uppercase().replace("-", "_").replace(" ", "_")
226                        )
227                    })
228                    .collect(),
229            )
230        }
231    }
232
233    #[derive(Debug, Serialize, Deserialize)]
234    pub struct Realm {
235        // Optional Additional claims
236        roles: Vec<String>,
237    }
238
239    /// Validates a JWT token and ensures that the roles in the token match the provided list.
240    ///
241    /// # Parameters
242    /// - `token`: The JWT token to validate.
243    /// - `settings`: The configuration settings for the server.
244    /// - `roles`: The list of roles to check against the JWT token.
245    ///
246    /// # Returns
247    /// A `Result` containing a `()`` if the JWT token is valid and the roles match.
248    /// Returns an error if the JWT token is invalid or the roles do not match.
249    ///
250    /// # Errors
251    /// This method will return an error if:
252    /// - The JWT token is invalid.
253    /// - The roles in the JWT token do not match the provided list.
254    pub(crate) fn validate_jwt(token: &str, settings: &Settings, authorize: String) -> Result<()> {
255        // Validate JWT and retrieve the `kid` header
256        let (kid, algorithm) = validate_jwt_header(token)?;
257
258        validate_jwt_with_roles(token, kid.as_str(), algorithm, authorize, settings)?;
259
260        Ok(())
261    }
262
263    /// Validates the JWT header and retrieves the `kid` and `Algorithm` fields.
264    ///
265    /// # Parameters
266    /// - `token`: The JWT token to validate and extract the header fields from.
267    ///
268    /// # Returns
269    /// A `Result` containing a tuple of `(String, Algorithm)` if the header is valid.
270    /// Returns an error if the header is invalid or if the `kid` field is not present.
271    ///
272    /// # Errors
273    /// This method will return an error if:
274    /// - The JWT header is invalid.
275    /// - The `kid` field is not present in the JWT header.
276    pub(crate) fn validate_jwt_header(token: &str) -> Result<(String, Algorithm)> {
277        let header = decode_header(token)
278            .map_err(|_| OAuth2Error::InvalidJwt("Invalid JWT Header.".into()))?;
279
280        let Some(kid) = header.kid else {
281            warn!("Token doesn't have a `kid` header field.");
282            return Err(OAuth2Error::InvalidJwt(
283                "Token doesn't have a `kid` header field.".into(),
284            ));
285        };
286
287        Ok((kid, header.alg))
288    }
289
290    /// Validates a JWT token and ensures that the roles in the token match the provided list.
291    ///
292    /// This method takes in a JWT token, a kid, an algorithm, a list of roles, and a settings object.
293    /// It first retrieves the public key based on the `kid` and then uses it to decode the JWT token.
294    /// After decoding, it validates that the issuer URI within the token matches the one configured in the server settings.
295    /// Finally, it checks that the roles in the token match the provided list.
296    ///
297    /// # Parameters
298    /// - `token`: The JWT token to validate.
299    /// - `kid`: The key id to retrieve the public key for.
300    /// - `algorithm`: The algorithm used to decode the JWT token.
301    /// - `roles`: The list of roles to check against the JWT token.
302    /// - `settings`: The settings object containing the server configuration.
303    ///
304    /// # Returns
305    /// A `Result` containing a `()` if the JWT token is valid and the roles match.
306    /// Returns an error if the JWT token is invalid or the roles do not match.
307    ///
308    /// # Errors
309    /// This method will return an error if:
310    /// - The JWT token is invalid.
311    /// - The roles in the JWT token do not match the provided list.
312    /// - The public key is not found for the given `kid`.
313    /// - The issuer URI is not configured in the server settings.
314    pub(crate) fn validate_jwt_with_roles(
315        token: &str,
316        kid: &str,
317        algorithm: Algorithm,
318        authorize: String,
319        settings: &Settings,
320    ) -> Result<()> {
321        // Retrieves the public key based on the `kid`
322        let public_key = settings.get_auth2_public_key(kid).ok_or_else(|| {
323            warn!("Public key not found for key id: {kid}.");
324            OAuth2Error::InvalidPublicKey("Public key not found for key id: {kid}.".into())
325        })?;
326        let decoded_public_key = &DecodingKey::try_from(&public_key).map_err(|e| {
327            warn!("Invalid public key. \n{:?}", &public_key);
328            OAuth2Error::InvalidPublicKey(e.to_string())
329        })?;
330
331        // Retrieves the issuer URI within the server configuration
332        let issuer = settings
333            .get_oauth2_config()
334            .ok_or_else(|| {
335                warn!("Security not configured.");
336                OAuth2Error::Configuration("Security not configured..".into())
337            })?
338            .issuer_uri
339            .ok_or_else(|| {
340                warn!("Issuer URI not configured.");
341                OAuth2Error::Configuration("Issuer URI not configured.".into())
342            })?;
343
344        // Creates a validation struct for the JWT
345        let validation = {
346            let mut validation = Validation::new(algorithm);
347            validation.set_issuer(&[issuer.as_str()]);
348            validation.validate_exp = true;
349            validation
350        };
351
352        // Decodes the JWT into a HashMap
353        let decoded_token =
354            decode::<Claims>(token, decoded_public_key, &validation).map_err(|e| {
355                warn!("Invalid token. {}", e.to_string());
356                OAuth2Error::JWTDecode(e.to_string())
357            })?;
358
359        validate_jwt_roles(&decoded_token, authorize)?;
360
361        Ok(())
362    }
363
364    /// Validates the roles in the given JWT token against the provided authorization string.
365    ///
366    /// The `authorize` string must be in the following format:
367    /// `method role1,role2,...,roleN` or `ROLE1,ROLE2,...,ROLEN`
368    ///
369    /// The `method` parameter can be either "hasanyrole" or "hasallroles".
370    /// If "hasanyrole" is specified, the function will return an error if any of the required roles are not found in the JWT token.
371    /// If "hasallroles" is specified, the function will return an error if all of the required roles are not found in the JWT token.
372    ///
373    /// # Parameters
374    /// - `token`: The JWT token to validate the roles against.
375    /// - `authorize`: The authorization string to parse.
376    ///
377    /// # Returns
378    /// A `Result` containing a unit if the roles match the provided authorization string.
379    /// Returns an error if the roles do not match the provided authorization string.
380    ///
381    /// # Errors
382    /// This method will return an error if:
383    /// - The authorization string is invalid.
384    /// - The roles in the JWT token do not match the provided authorization string.
385    fn validate_jwt_roles(token: &TokenData<Claims>, authorize: String) -> Result<()> {
386        let (method, roles) = get_authorize_role_method(authorize)?;
387
388        match method.as_str() {
389            "hasanyrole" => has_any_role(token, roles)?,
390            "hasallroles" => has_all_role(token, roles)?,
391            _ => {
392                if !method.is_empty() {
393                    return Err(OAuth2Error::InvalidRoles(format!(
394                        "Invalid role authorization method: {}",
395                        method.bright_blue()
396                    )));
397                } else {
398                    // Validate Single Role
399                    has_any_role(token, roles)?;
400                }
401            }
402        }
403
404        Ok(())
405    }
406
407    /// Checks if all of the roles in the given `roles` vector are present in the JWT token.
408    ///
409    /// # Parameters
410    /// - `token`: The JWT token to check the roles against.
411    /// - `roles`: A vector of roles to check against the JWT token.
412    ///
413    /// # Returns
414    /// A `Result` containing a unit if all of the required roles are found in the JWT token.
415    /// Returns an error if any of the required roles are not found in the JWT token.
416    ///
417    /// # Errors
418    /// This method will return an error if none of the required roles are found in the JWT token.
419    fn has_all_role(token: &TokenData<Claims>, roles: Vec<String>) -> Result<()> {
420        let token_roles = token
421            .claims
422            .get_roles()
423            .ok_or_else(|| OAuth2Error::InvalidRoles("User doesn't have any roles.".into()))?;
424        for role in &roles {
425            if !token_roles.contains(role) {
426                return Err(OAuth2Error::InvalidRoles(format!(
427                    "No required role was found for the current user. Required roles: {}. Current roles: {}",
428                    role.bright_blue(),
429                    token_roles
430                        .iter()
431                        .map(|r| r.to_string())
432                        .collect::<Vec<String>>()
433                        .join(", ")
434                        .bright_green()
435                )));
436            }
437        }
438
439        Ok(())
440    }
441
442    /// Checks if any of the roles in the given `roles` vector is present in the JWT token.
443    ///
444    /// # Parameters
445    /// - `token`: The JWT token to check the roles against.
446    /// - `roles`: A vector of roles to check against the JWT token.
447    ///
448    /// # Returns
449    /// A `Result` containing a unit if any of the required roles are found in the JWT token.
450    /// Returns an error if none of the required roles are found in the JWT token.
451    ///
452    /// # Errors
453    /// This method will return an error if none of the required roles are found in the JWT token.
454    fn has_any_role(token: &TokenData<Claims>, roles: Vec<String>) -> Result<()> {
455        let token_roles = token
456            .claims
457            .get_roles()
458            .ok_or_else(|| OAuth2Error::InvalidRoles("User doesn't have any roles.".into()))?;
459        for role in &roles {
460            if token_roles.contains(role) {
461                return Ok(());
462            }
463        }
464
465        Err(OAuth2Error::InvalidRoles(format!(
466            "No required role was found for the current user. Required roles: {}. Current roles: {}",
467            roles.join(", ").bright_blue(),
468            token_roles
469                .iter()
470                .map(|r| r.to_string())
471                .collect::<Vec<String>>()
472                .join(", ")
473                .bright_green()
474        )))
475    }
476
477    /// Retrieves the authorization method and roles from the given string.
478    ///
479    /// The authorization string must be in the following format:
480    /// `method role1,role2,...,roleN` or `ROLE1,ROLE2,...,ROLEN`
481    ///
482    /// # Parameters
483    /// - `authorize`: The authorization string to parse.
484    ///
485    /// # Returns
486    /// A `Result` containing a tuple of the authorization method and roles if the string is valid.
487    /// Returns an error if the string is invalid.
488    ///
489    /// # Errors
490    /// This method will return an error if the authorization string is invalid.
491    fn get_authorize_role_method(authorize: String) -> Result<(String, Vec<String>)> {
492        let pattern = Regex::new(
493            r"(?i)^\s*(?:(\w+)\s*\(\s*(ROLE_\w+(?:\s*,\s*ROLE_\w+)*)\s*\)|(ROLE_\w+))\s*$",
494        )
495        .map_err(|e| OAuth2Error::RoleAuthorizationParse(e.to_string()))?;
496
497        let caps = pattern.captures(&authorize).ok_or_else(|| {
498            OAuth2Error::RoleAuthorizationParse("Invalid role authorization format.".into())
499        })?;
500
501        // Grup 1: method (opcional)
502        let method = caps
503            .get(1)
504            .map(|m| m.as_str().to_lowercase())
505            .unwrap_or_default();
506
507        // Grup 2: roles with method
508        // Grup 3: one role without method
509        let roles_raw = caps
510            .get(2)
511            .or_else(|| caps.get(3))
512            .map(|r| r.as_str())
513            .ok_or_else(|| OAuth2Error::RoleAuthorizationParse("Roles not found.".into()))?;
514
515        let roles = roles_raw
516            .split(',')
517            .map(|r| r.trim().to_uppercase())
518            .collect::<Vec<_>>();
519
520        // Method and roles cannot be empty at the same time
521        if method.is_empty() && roles.is_empty() {
522            return Err(OAuth2Error::RoleAuthorizationParse(
523                "Authorization method and role not found.".into(),
524            ));
525        }
526
527        // Roles cannot be empty if the method is not empty
528        if !method.is_empty() && roles.is_empty() {
529            return Err(OAuth2Error::RoleAuthorizationParse(
530                "Authorization method without roles.".into(),
531            ));
532        }
533
534        Ok((method, roles))
535    }
536
537    #[cfg(test)]
538    mod tests {
539        use super::get_authorize_role_method;
540
541        #[test]
542        fn should_parse_method_and_roles_from_authorize_string() {
543            let authorize = "hasAnyRole(ROLE_ADMIN, ROLE_USER)".to_string();
544
545            let result = get_authorize_role_method(authorize);
546
547            assert!(result.is_ok());
548            let (method, roles) = result.unwrap_or_default();
549            assert_eq!(method, "hasanyrole");
550            assert_eq!(roles, vec!["ROLE_ADMIN", "ROLE_USER"]);
551        }
552    }
553}