11// JWT authentication and validation module
2+ //
3+ // This module handles:
4+ // - HS256 internal token generation, signing, and validation
5+ // - Internal issuer trust verification (`KALAMDB_ISSUER`, `is_internal_issuer`, `verify_issuer`)
6+ //
7+ // The following types are defined in `kalamdb-oidc` and re-exported here so
8+ // existing call-sites continue to work unchanged:
9+ // `JwtClaims`, `TokenType`, `DEFAULT_JWT_EXPIRY_HOURS`
10+ // `extract_issuer_unverified`, `extract_algorithm_unverified`
11+ //
12+ // External OIDC token validation (RS256/ES256 via JWKS) is handled by
13+ // `kalamdb-oidc` and orchestrated in `bearer.rs`.
214
315use crate :: errors:: error:: { AuthError , AuthResult } ;
416use jsonwebtoken:: errors:: ErrorKind ;
517use jsonwebtoken:: {
618 decode, decode_header, encode, Algorithm , DecodingKey , EncodingKey , Header , Validation ,
719} ;
820use kalamdb_commons:: { Role , UserId , UserName } ;
9- use serde:: { Deserialize , Serialize } ;
1021
11- /// Default JWT expiration time in hours
12- pub const DEFAULT_JWT_EXPIRY_HOURS : i64 = 24 ;
22+ // ── Types and utilities that live in kalamdb-oidc ───────────────────────────
23+ // Re-exported here so callers using `jwt_auth::JwtClaims` etc. need no changes.
24+ pub use kalamdb_oidc:: { extract_algorithm_unverified, extract_issuer_unverified} ;
25+ pub use kalamdb_oidc:: { JwtClaims , TokenType , DEFAULT_JWT_EXPIRY_HOURS } ;
1326
14- /// Default issuer for KalamDB tokens
27+ /// Default issuer for KalamDB-issued tokens.
1528pub const KALAMDB_ISSUER : & str = "kalamdb" ;
1629
17- /// Token type for distinguishing access from refresh tokens.
18- #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
19- #[ serde( rename_all = "lowercase" ) ]
20- pub enum TokenType {
21- Access ,
22- Refresh ,
23- }
24-
25- impl std:: fmt:: Display for TokenType {
26- fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
27- match self {
28- TokenType :: Access => write ! ( f, "access" ) ,
29- TokenType :: Refresh => write ! ( f, "refresh" ) ,
30- }
31- }
32- }
33-
34- /// JWT claims structure for KalamDB tokens.
35- ///
36- /// Standard JWT claims plus custom KalamDB-specific fields.
37- #[ derive( Debug , Clone , Serialize , Deserialize ) ]
38- pub struct JwtClaims {
39- /// Subject (user ID)
40- pub sub : String ,
41- /// Issuer
42- pub iss : String ,
43- /// Expiration time (Unix timestamp)
44- pub exp : usize ,
45- /// Issued at (Unix timestamp)
46- pub iat : usize ,
47- /// Username (custom claim). Also maps provider claim `preferred_username`.
48- #[ serde( alias = "preferred_username" ) ]
49- pub username : Option < UserName > ,
50- /// Email (custom claim)
51- pub email : Option < String > ,
52- /// Role (custom claim)
53- pub role : Option < Role > ,
54- /// Token type: "access" or "refresh"
55- /// Optional for backward compatibility with tokens issued before this field existed.
56- #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
57- pub token_type : Option < TokenType > ,
58- }
59-
60- impl JwtClaims {
61- /// Create new JWT claims for a user (defaults to access token).
62- ///
63- /// # Arguments
64- /// * `user_id` - User's unique identifier
65- /// * `username` - Username
66- /// * `role` - User's role
67- /// * `email` - Optional email address
68- /// * `expiry_hours` - Token expiration in hours (defaults to DEFAULT_JWT_EXPIRY_HOURS)
69- pub fn new (
70- user_id : & UserId ,
71- username : & UserName ,
72- role : & Role ,
73- email : Option < & str > ,
74- expiry_hours : Option < i64 > ,
75- ) -> Self {
76- Self :: with_token_type ( user_id, username, role, email, expiry_hours, TokenType :: Access )
77- }
78-
79- /// Create new JWT claims with an explicit token type.
80- pub fn with_token_type (
81- user_id : & UserId ,
82- username : & UserName ,
83- role : & Role ,
84- email : Option < & str > ,
85- expiry_hours : Option < i64 > ,
86- token_type : TokenType ,
87- ) -> Self {
88- let now = chrono:: Utc :: now ( ) ;
89- let exp_hours = expiry_hours. unwrap_or ( DEFAULT_JWT_EXPIRY_HOURS ) ;
90- let exp = now + chrono:: Duration :: hours ( exp_hours) ;
91-
92- Self {
93- sub : user_id. to_string ( ) ,
94- iss : KALAMDB_ISSUER . to_string ( ) ,
95- exp : exp. timestamp ( ) as usize ,
96- iat : now. timestamp ( ) as usize ,
97- username : Some ( username. clone ( ) ) ,
98- email : email. map ( |e| e. to_string ( ) ) ,
99- role : Some ( * role) ,
100- token_type : Some ( token_type) ,
101- }
102- }
103- }
104-
10530/// Generate a new JWT token.
10631///
10732/// # Arguments
@@ -133,8 +58,9 @@ pub fn create_and_sign_token(
13358 expiry_hours : Option < i64 > ,
13459 secret : & str ,
13560) -> AuthResult < ( String , JwtClaims ) > {
136- let claims =
137- JwtClaims :: with_token_type ( user_id, username, role, email, expiry_hours, TokenType :: Access ) ;
61+ let claims = JwtClaims :: with_token_type (
62+ user_id, username, role, email, expiry_hours, TokenType :: Access , KALAMDB_ISSUER ,
63+ ) ;
13864 let token = generate_jwt_token ( & claims, secret) ?;
13965 Ok ( ( token, claims) )
14066}
@@ -158,6 +84,7 @@ pub fn create_and_sign_refresh_token(
15884 email,
15985 expiry_hours,
16086 TokenType :: Refresh ,
87+ KALAMDB_ISSUER ,
16188 ) ;
16289 let token = generate_jwt_token ( & claims, secret) ?;
16390 Ok ( ( token, claims) )
@@ -258,21 +185,7 @@ pub fn validate_jwt_token(
258185}
259186
260187/// Verify JWT issuer is in the trusted list.
261- ///
262- /// # Arguments
263- /// * `issuer` - Issuer from JWT claims
264- /// * `trusted_issuers` - List of trusted issuer domains
265- ///
266- /// # Returns
267- /// `Ok(())` if issuer is trusted
268- ///
269- /// # Errors
270- /// Returns `AuthError::UntrustedIssuer` if issuer is not in the list
271- ///
272- /// # Security Note
273- /// If no trusted issuers are configured, ALL issuers are rejected.
274- /// This is a secure-by-default approach to prevent accepting arbitrary tokens.
275- fn verify_issuer ( issuer : & str , trusted_issuers : & [ String ] ) -> AuthResult < ( ) > {
188+ pub fn verify_issuer ( issuer : & str , trusted_issuers : & [ String ] ) -> AuthResult < ( ) > {
276189 // Security: If no issuers configured, reject all (secure by default)
277190 if trusted_issuers. is_empty ( ) {
278191 return Err ( AuthError :: UntrustedIssuer ( format ! (
@@ -288,36 +201,12 @@ fn verify_issuer(issuer: &str, trusted_issuers: &[String]) -> AuthResult<()> {
288201 }
289202}
290203
291- /// Extract claims from a JWT token without full validation.
292- ///
293- /// **WARNING**: This does NOT verify the signature! Only use in tests
294- /// or when you need to inspect a token before validation.
295- ///
296- /// # Arguments
297- /// * `token` - JWT token string
204+ /// Returns true if the issuer is the internal KalamDB issuer.
298205///
299- /// # Returns
300- /// JWT claims (unverified)
301- ///
302- /// # Errors
303- /// Returns error if token structure is invalid
304- ///
305- /// # Security
306- /// This function is gated behind `#[cfg(test)]` to prevent accidental
307- /// use in production code paths.
308- #[ cfg( test) ]
309- pub fn extract_claims_unverified ( token : & str ) -> AuthResult < JwtClaims > {
310- // Decode without verification (dangerous!)
311- let mut validation = Validation :: new ( Algorithm :: HS256 ) ;
312- #[ allow( deprecated) ]
313- validation. insecure_disable_signature_validation ( ) ; // DANGEROUS - but needed for unverified claim extraction
314- validation. validate_exp = false ;
315-
316- let decoding_key = DecodingKey :: from_secret ( b"" ) ; // Empty key since we're not validating
317- let token_data = decode :: < JwtClaims > ( token, & decoding_key, & validation)
318- . map_err ( |e| AuthError :: MalformedAuthorization ( format ! ( "JWT decode error: {}" , e) ) ) ?;
319-
320- Ok ( token_data. claims )
206+ /// Internal tokens (iss = "kalamdb") are signed with the shared HS256 secret
207+ /// and never come from an external provider.
208+ pub fn is_internal_issuer ( issuer : & str ) -> bool {
209+ issuer == KALAMDB_ISSUER
321210}
322211
323212#[ cfg( test) ]
0 commit comments