nextera_jwt/
lib.rs

1//! # Next Era Actix Web Authentication Macro
2//!
3//! Next Era Solutions JWT base Procedural Macro Attribute for Actix Web.
4
5extern crate proc_macro;
6use proc_macro::TokenStream;
7
8use quote::quote;
9use syn::{parse_macro_input, ItemFn};
10
11//// A procedural macro attribute for adding authentication to"Valid Token".to_string(ng(///
12/// This macro automatically injects an `actix_web::HttpRequest` parameter into the decorated function
13/// and perform"Valid Refresh Token".to_string(ng(authentication. It expects a Bearer token in the
14/// `Authorization` header of the incoming request.
15///
16/// # How it Works
17///
18/// 1.  **Header Extraction:** It extracts the `Authorization` header from the incoming request.
19/// 2.  **Bearer Token Parsing:** It expects the header value to be in the format "Bearer <token>" and extracts the token.
20/// 3.  **Environment Variables:** It retrieves the JWT secret key from environment variables named  `ACCESS_TOKEN_SECRET`,`REFRESH_TOKEN_SECRET` and `JWT_AUDIENCE`, respectively. Make sure these are set before running your application.
21/// 4.  **Token Validation:** It uses the `nextera_utils::jwt::validate_jwt` function to validate the token against the provided secret.
22/// 5.  **Authorization:** If the token is valid, the original handler function is executed. Otherwise, an `HttpResponse::Unauthorized` (401) response is returned.
23///
24/// # Usage
25///
26/// ```rust
27/// use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
28/// use nextera_jwt::authentication;
29/// use nextera_jwt::refresh_authentication;
30/// use nextera_jwt::x_api_key;
31///
32/// #[authentication]
33/// pub async fn auth() -> impl Responder {
34///     HttpResponse::Ok().body("Valid Token")
35/// }
36///
37/// #[refresh_authentication]
38/// async fn refresh_auth() -> impl Responder {
39///     HttpResponse::Ok().body("Valid Refresh Token")
40/// }
41///
42/// #[x_api_key]
43/// async fn x_api_key() -> impl Responder {
44///     HttpResponse::Ok().body("Valid X API Key")
45/// }
46///
47///
48/// #[actix_web::test]
49/// async fn test_access_and_refresh_token() {
50///     use nextera_utils::jwt;
51///     use actix_web::test;
52///
53///     std::env::set_var("ACCESS_TOKEN_SECRET", "test_secret");
54///     std::env::set_var("REFRESH_TOKEN_SECRET", "test_secret");
55///     std::env::set_var("JWT_AUDIENCE", "test_jwt_audience");
56///
57///     let app =
58///         test::init_service(App::new().route("/auth", web::get().to(auth)).route("/refresh", web::get().to(refresh_auth))).await;
59///
60///     let (token, _) =
61///         jwt::generate_jwt(1, 1, "test_secret", 3600, "session_uuid", "test_jwt_audience")
62///             .expect("Failed to generate access token");
63///
64///     let req = test::TestRequest::get()
65///         .uri("/auth")
66///         .insert_header(("Authorization", format!("Bearer {}", token)))
67///         .to_request();
68///
69///     let resp = test::call_service(&app, req).await;
70///     assert_eq!(resp.status(), 200);
71///
72///     let ref_req = test::TestRequest::get()
73///         .uri("/refresh")
74///         .insert_header(("Authorization", format!("Bearer {}", token)))
75///         .to_request();
76///
77///     let ref_resp = test::call_service(&app, ref_req).await;
78///     assert_eq!(ref_resp.status(), 200);
79/// }
80///
81/// #[actix_web::test]
82/// async fn test_api_key() {
83///     use actix_web::test;
84///
85///     std::env::set_var("X_API_KEY", "test_api_key");
86///
87///     let app = test::init_service(App::new().route("/apikey", web::get().to(x_api_key))).await;
88///
89///     let req = test::TestRequest::get()
90///         .uri("/apikey")
91///         .insert_header(("X-API-Key", "test_api_key"))
92///         .to_request();
93///
94///     let resp = test::call_service(&app, req).await;
95///     assert_eq!(resp.status(), 200);
96/// }
97/// ```
98///
99/// # Important Considerations
100///
101/// *   **Error Handling:** The current implementation uses `unwrap_or("")` on the header value and `expect` for environment variables. For production, more robust error handling should be implemented (e.g., returning `HttpResponse::BadRequest` for malformed headers).
102/// *   **Dependency:** This macro depends on the `nextera_utils::jwt` crate, which needs to be included in your `Cargo.toml`.
103/// *   **Environment Variables:** Ensure `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `JWT_AUDIENCE` and `X_API_KEY` are set in your environment. Never hardcode secrets in your source code. Consider using a secrets management solution for production.
104/// *   **HttpRequest Injection:** The macro automatically injects `actix_web::HttpRequest` as the first argument of the decorated function. Make sure your handler function signature is compatible.
105/// *   **Async Functions:** This macro supports `async` functions.
106#[proc_macro_attribute]
107pub fn authentication(_args: TokenStream, input: TokenStream) -> TokenStream {
108    let input_fn = parse_macro_input!(input as ItemFn);
109
110    let fn_name = &input_fn.sig.ident;
111    let fn_async_ness = &input_fn.sig.asyncness;
112    let fn_visibility = &input_fn.vis;
113    let fn_inputs = &input_fn.sig.inputs;
114    let fn_output = &input_fn.sig.output;
115    let fn_body = &input_fn.block;
116
117    // Wrap the function logic with authentication logic
118    let expanded = quote! {
119
120        #fn_visibility #fn_async_ness fn #fn_name(
121            actix_web_req: actix_web::HttpRequest, // Automatically inject HttpRequest
122            #fn_inputs
123        ) #fn_output {
124            use actix_web::HttpResponse;
125
126            // Get response language
127            let default = actix_web::http::header::HeaderValue::from_static("en");
128            let c = actix_web_req.headers().get("Content-Language").unwrap_or(&default).to_str().unwrap_or("en");
129            let invalid_credentials = match c {
130                "zh-CN" => "凭证无效",
131                "th" => "ข้อมูลเข้าสู่ระบบไม่ถูกต้อง",
132                "mm" => "အထောက်အထားများ မှားယွင်းနေပါသည်",
133                _ => "Invalid credentials"
134            };
135
136            let session_expired = match c {
137                "zh-CN" => "会话已过期",
138                "th" => "เซสชั่นหมดอายุแล้ว",
139                "mm" => "စက်ရှင် သက်တမ်းကုန်သွားပါပြီ",
140                _ => "Session Expired"
141            };
142            // Extract and validate the Authorization header
143            if let Some(auth_header) = actix_web_req.headers().get("Authorization") {
144                let token = auth_header.to_str().unwrap_or("").trim();
145
146                // Ensure token starts with "Bearer "
147                let token = token.trim_start_matches("Bearer ").trim();
148
149                // Load environment variables
150                let access_token_secret = std::env::var("ACCESS_TOKEN_SECRET")
151                    .expect("Failed to get ACCESS_TOKEN_SECRET from environment");
152                let audience = std::env::var("JWT_AUDIENCE")
153                    .expect("Failed to get JWT_AUDIENCE from environment");
154
155                // Validate the token
156                if let Err(e) = nextera_utils::jwt::validate_jwt(token, &access_token_secret, &audience) {
157                    // You've already got the error 'e' here, no need to call unwrap_err()
158                    return if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature {
159                        HttpResponse::build(actix_web::http::StatusCode::from_u16(419).unwrap())
160                            .json(nextera_utils::models::response_message::ResponseMessage {
161                                message: String::from(session_expired)
162                            })
163                    } else {
164                        HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage {
165                            message: String::from(invalid_credentials)
166                        })
167                    };
168                }
169            } else {
170                // Respond with Unauthorized if no Authorization header is present
171                return HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage { message: String::from(invalid_credentials)});
172            }
173
174
175            // Proceed with the original function body
176            #fn_body
177        }
178    };
179
180    TokenStream::from(expanded)
181}
182
183#[proc_macro_attribute]
184pub fn refresh_authentication(_args: TokenStream, input: TokenStream) -> TokenStream {
185    let input_fn = parse_macro_input!(input as ItemFn);
186
187    let fn_name = &input_fn.sig.ident;
188    let fn_async_ness = &input_fn.sig.asyncness;
189    let fn_visibility = &input_fn.vis;
190    let fn_inputs = &input_fn.sig.inputs;
191    let fn_output = &input_fn.sig.output;
192    let fn_body = &input_fn.block;
193
194    // Wrap the function logic with refresh token authentication logic
195    let expanded = quote! {
196
197        #fn_visibility #fn_async_ness fn #fn_name(
198            actix_web_req: actix_web::HttpRequest, // Automatically inject HttpRequest
199            #fn_inputs
200        ) #fn_output {
201            use actix_web::HttpResponse;
202
203            // Get response language
204            let default = actix_web::http::header::HeaderValue::from_static("en");
205            let c = actix_web_req.headers().get("Content-Language").unwrap_or(&default).to_str().unwrap_or("en");
206            let invalid_credentials = match c {
207                "zh-CN" => "凭证无效",
208                "th" => "ข้อมูลเข้าสู่ระบบไม่ถูกต้อง",
209                "mm" => "အထောက်အထားများ မှားယွင်းနေပါသည်",
210                _ => "Invalid credentials"
211            };
212
213            let session_expired = match c {
214                "zh-CN" => "会话已过期",
215                "th" => "เซสชั่นหมดอายุแล้ว",
216                "mm" => "စက်ရှင် သက်တမ်းကုန်သွားပါပြီ",
217                _ => "Session Expired"
218            };
219
220            // Extract and validate the Authorization header
221            if let Some(auth_header) = actix_web_req.headers().get("Authorization") {
222                let token = auth_header.to_str().unwrap_or("").trim();
223
224                // Ensure token starts with "Bearer "
225                let token = token.trim_start_matches("Bearer ").trim();
226
227                // Load environment variables
228                let refresh_token_secret = std::env::var("REFRESH_TOKEN_SECRET")
229                    .expect("Failed to get REFRESH_TOKEN_SECRET from environment");
230                let audience = std::env::var("JWT_AUDIENCE")
231                    .expect("Failed to get JWT_AUDIENCE from environment");
232
233                // Validate the token
234                if let Err(e) = nextera_utils::jwt::validate_jwt(token, &refresh_token_secret, &audience) {
235                    // You've already got the error 'e' here, no need to call unwrap_err()
236                    return if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature {
237                        HttpResponse::build(actix_web::http::StatusCode::from_u16(419).unwrap())
238                            .json(nextera_utils::models::response_message::ResponseMessage {
239                                message: String::from(session_expired)
240                            })
241                    } else {
242                        HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage {
243                            message: String::from(invalid_credentials)
244                        })
245                    };
246                }
247            } else {
248                // Respond with Unauthorized if no Authorization header is present
249                return HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage { message: String::from(invalid_credentials)});
250            }
251
252            // Proceed with the original function body
253            #fn_body
254        }
255    };
256
257    TokenStream::from(expanded)
258}
259
260#[proc_macro_attribute]
261pub fn x_api_key(_args: TokenStream, input: TokenStream) -> TokenStream {
262    let input_fn = parse_macro_input!(input as ItemFn);
263
264    let fn_name = &input_fn.sig.ident;
265    let fn_async_ness = &input_fn.sig.asyncness;
266    let fn_visibility = &input_fn.vis;
267    let fn_inputs = &input_fn.sig.inputs;
268    let fn_output = &input_fn.sig.output;
269    let fn_body = &input_fn.block;
270
271    // Wrap the function logic with refresh token authentication logic
272    let expanded = quote! {
273
274        #fn_visibility #fn_async_ness fn #fn_name(
275            actix_web_req: actix_web::HttpRequest, // Automatically inject HttpRequest
276            #fn_inputs
277        ) #fn_output {
278            use actix_web::HttpResponse;
279
280            // Get response language
281            let default = actix_web::http::header::HeaderValue::from_static("en");
282            let c = actix_web_req.headers().get("Content-Language").unwrap_or(&default).to_str().unwrap_or("en");
283            let invalid_credentials = match c {
284                "zh-CN" => "凭证无效",
285                "th" => "ข้อมูลเข้าสู่ระบบไม่ถูกต้อง",
286                "mm" => "အထောက်အထားများ မှားယွင်းနေပါသည်",
287                _ => "Invalid API Key"
288            };
289
290            // Extract and validate the Authorization header
291            if let Some(auth_header) = actix_web_req.headers().get("X-API-Key") {
292                let xapikey = auth_header.to_str().unwrap_or("").trim();
293
294                // Load environment variables
295                let env_xapikey = std::env::var("X_API_KEY").expect("Failed to get X_API_KEY from environment");
296
297                // Validate the token
298                if xapikey.ne(&env_xapikey) {
299                    return HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage { message: String::from(invalid_credentials)});
300                }
301            } else {
302                // Respond with Unauthorized if no Authorization header is present
303                return HttpResponse::Unauthorized().json(nextera_utils::models::response_message::ResponseMessage { message: String::from(invalid_credentials)});
304            }
305
306            // Proceed with the original function body
307            #fn_body
308        }
309    };
310
311    TokenStream::from(expanded)
312}