@@ -25,9 +25,20 @@ const SECRET_BLACKLIST: &[&str] = &[
2525const MIN_SECRET_LENGTH : usize = 43 ; // ~256 bits when base64 encoded
2626const MIN_UNIQUE_CHARS : usize = 10 ;
2727const MIN_CHAR_CLASSES : usize = 3 ;
28+
29+ /// The name of the authentication cookie.
2830pub const AUTH_COOKIE_NAME : & str = "ltcms_session" ;
2931const AUTH_COOKIE_TTL_SECONDS : i64 = 24 * 60 * 60 ; // 24 hours
3032
33+ /// Initializes the JWT secret from the `JWT_SECRET` environment variable.
34+ ///
35+ /// This function performs critical security checks to ensure the secret is not a placeholder
36+ /// and meets minimum entropy requirements. It must be called successfully at startup.
37+ ///
38+ /// # Returns
39+ ///
40+ /// * `Ok(())` if the secret is valid and initialized.
41+ /// * `Err(String)` if the secret is missing, empty, a placeholder, or too weak.
3142pub fn init_jwt_secret ( ) -> Result < ( ) , String > {
3243 let secret = env:: var ( "JWT_SECRET" )
3344 . map_err ( |_| "JWT_SECRET environment variable not set" . to_string ( ) ) ?;
@@ -55,21 +66,36 @@ pub fn init_jwt_secret() -> Result<(), String> {
5566 Ok ( ( ) )
5667}
5768
69+ /// Retrieves the initialized JWT secret.
70+ ///
71+ /// # Panics
72+ ///
73+ /// Panics if `init_jwt_secret` has not been called.
5874fn get_jwt_secret ( ) -> & ' static str {
5975 JWT_SECRET
6076 . get ( )
6177 . expect ( "JWT_SECRET not initialized. Call init_jwt_secret() first." )
6278 . as_str ( )
6379}
6480
81+ /// Represents the claims contained within a JWT.
6582#[ derive( Debug , Serialize , Deserialize , Clone ) ]
6683pub struct Claims {
67- pub sub : String , // username
84+ /// The subject of the token (username).
85+ pub sub : String ,
86+ /// The role of the user.
6887 pub role : String ,
88+ /// The expiration timestamp.
6989 pub exp : usize ,
7090}
7191
7292impl Claims {
93+ /// Creates new `Claims` for a user with a 24-hour expiration.
94+ ///
95+ /// # Arguments
96+ ///
97+ /// * `username` - The username to encode in the token.
98+ /// * `role` - The user's role.
7399 pub fn new ( username : String , role : String ) -> Self {
74100 // Use checked arithmetic to prevent overflow
75101 let expiration = Utc :: now ( )
@@ -85,6 +111,16 @@ impl Claims {
85111 }
86112}
87113
114+ /// Creates a JWT for the given user.
115+ ///
116+ /// # Arguments
117+ ///
118+ /// * `username` - The username.
119+ /// * `role` - The user's role.
120+ ///
121+ /// # Returns
122+ ///
123+ /// A `Result` containing the signed JWT string or a `jsonwebtoken::errors::Error`.
88124pub fn create_jwt ( username : String , role : String ) -> Result < String , jsonwebtoken:: errors:: Error > {
89125 let claims = Claims :: new ( username, role) ;
90126 let secret = get_jwt_secret ( ) ;
@@ -96,6 +132,15 @@ pub fn create_jwt(username: String, role: String) -> Result<String, jsonwebtoken
96132 )
97133}
98134
135+ /// Verifies a JWT and returns its claims.
136+ ///
137+ /// # Arguments
138+ ///
139+ /// * `token` - The JWT string to verify.
140+ ///
141+ /// # Returns
142+ ///
143+ /// A `Result` containing the decoded `Claims` or a `jsonwebtoken::errors::Error`.
99144pub fn verify_jwt ( token : & str ) -> Result < Claims , jsonwebtoken:: errors:: Error > {
100145 let secret = get_jwt_secret ( ) ;
101146
@@ -112,6 +157,17 @@ pub fn verify_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
112157 Ok ( token_data. claims )
113158}
114159
160+ /// Builds an authentication cookie containing the JWT.
161+ ///
162+ /// The cookie is configured with `HttpOnly`, `SameSite=Lax`, and a secure flag if not in a development environment.
163+ ///
164+ /// # Arguments
165+ ///
166+ /// * `token` - The JWT string to embed in the cookie.
167+ ///
168+ /// # Returns
169+ ///
170+ /// A `Cookie` struct ready to be added to a response.
115171pub fn build_auth_cookie ( token : & str ) -> Cookie < ' static > {
116172 let mut builder = Cookie :: build ( ( AUTH_COOKIE_NAME , token. to_owned ( ) ) )
117173 . path ( "/" )
@@ -126,6 +182,13 @@ pub fn build_auth_cookie(token: &str) -> Cookie<'static> {
126182 builder. build ( )
127183}
128184
185+ /// Builds a cookie that instructs the client to remove the authentication cookie.
186+ ///
187+ /// This is achieved by setting an immediate expiration date.
188+ ///
189+ /// # Returns
190+ ///
191+ /// A `Cookie` struct for removal.
129192pub fn build_cookie_removal ( ) -> Cookie < ' static > {
130193 let mut builder = Cookie :: build ( ( AUTH_COOKIE_NAME , "" ) )
131194 . path ( "/" )
@@ -141,6 +204,10 @@ pub fn build_cookie_removal() -> Cookie<'static> {
141204 builder. build ( )
142205}
143206
207+ /// Axum extractor for `Claims`.
208+ ///
209+ /// This allows handlers to easily require authentication by including `Claims` in their arguments.
210+ /// It extracts the token from the `Authorization` header or the auth cookie.
144211impl < S > FromRequestParts < S > for Claims
145212where
146213 S : Send + Sync ,
@@ -163,6 +230,12 @@ where
163230 }
164231}
165232
233+ /// Appends a `Set-Cookie` header to a `HeaderMap`.
234+ ///
235+ /// # Arguments
236+ ///
237+ /// * `headers` - The `HeaderMap` to modify.
238+ /// * `cookie` - The `Cookie` to append.
166239pub fn append_auth_cookie ( headers : & mut HeaderMap , cookie : Cookie < ' static > ) {
167240 if let Ok ( value) = HeaderValue :: from_str ( & cookie. to_string ( ) ) {
168241 headers. append ( SET_COOKIE , value) ;
@@ -171,6 +244,20 @@ pub fn append_auth_cookie(headers: &mut HeaderMap, cookie: Cookie<'static>) {
171244 }
172245}
173246
247+ /// Checks if a secret meets minimum entropy requirements.
248+ ///
249+ /// A secret is considered high-entropy if it:
250+ /// - Is at least `MIN_SECRET_LENGTH` characters long.
251+ /// - Contains at least `MIN_CHAR_CLASSES` character classes (lower, upper, digit, symbol).
252+ /// - Has at least `MIN_UNIQUE_CHARS` unique characters.
253+ ///
254+ /// # Arguments
255+ ///
256+ /// * `secret` - The secret string to check.
257+ ///
258+ /// # Returns
259+ ///
260+ /// `true` if the secret meets the criteria, `false` otherwise.
174261fn secret_has_min_entropy ( secret : & str ) -> bool {
175262 if secret. len ( ) < MIN_SECRET_LENGTH {
176263 return false ;
@@ -198,6 +285,13 @@ fn secret_has_min_entropy(secret: &str) -> bool {
198285 unique_chars. len ( ) >= MIN_UNIQUE_CHARS
199286}
200287
288+ /// Determines if the `Secure` flag should be set on cookies.
289+ ///
290+ /// The flag is set unless the `AUTH_COOKIE_SECURE` environment variable is explicitly "false".
291+ ///
292+ /// # Returns
293+ ///
294+ /// `true` if cookies should be secure, `false` otherwise.
201295fn cookies_should_be_secure ( ) -> bool {
202296 match env:: var ( "AUTH_COOKIE_SECURE" ) {
203297 Ok ( value) if value. trim ( ) . eq_ignore_ascii_case ( "false" ) => {
@@ -210,6 +304,18 @@ fn cookies_should_be_secure() -> bool {
210304 }
211305}
212306
307+ /// Extracts a JWT from request headers.
308+ ///
309+ /// It first checks for an `Authorization: Bearer <token>` header, falling back
310+ /// to the authentication cookie if not found.
311+ ///
312+ /// # Arguments
313+ ///
314+ /// * `headers` - The `HeaderMap` from the incoming request.
315+ ///
316+ /// # Returns
317+ ///
318+ /// An `Option<String>` containing the token if found.
213319fn extract_token ( headers : & HeaderMap ) -> Option < String > {
214320 if let Some ( header_value) = headers. get ( AUTHORIZATION ) {
215321 if let Ok ( value_str) = header_value. to_str ( ) {
@@ -224,6 +330,15 @@ fn extract_token(headers: &HeaderMap) -> Option<String> {
224330 . map ( |cookie| cookie. value ( ) . to_string ( ) )
225331}
226332
333+ /// Parses a token from an `Authorization: Bearer <token>` header value.
334+ ///
335+ /// # Arguments
336+ ///
337+ /// * `value` - The raw string from the `Authorization` header.
338+ ///
339+ /// # Returns
340+ ///
341+ /// An `Option<String>` containing the token if parsing is successful.
227342fn parse_bearer_token ( value : & str ) -> Option < String > {
228343 let trimmed = value. trim ( ) ;
229344 let ( scheme, token) = trimmed. split_once ( ' ' ) ?;
0 commit comments