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}