dogy_backend_api/middleware/auth/
core.rs

1//! This module contains the core functionality of authentication middleware.
2use jsonwebtoken::{decode, DecodingKey, TokenData, Validation};
3use serde::Deserialize;
4use tracing::debug;
5
6use crate::config::load_config;
7
8use super::error::{Error, Result};
9use super::layer::CurrentUser;
10
11/// Represents the payload after decoding a token.
12#[allow(dead_code)]
13#[derive(Debug, Deserialize)]
14pub struct Claims {
15    /// Role of the authenticated user. Can either be None or a valid clerk role such as
16    /// `org:admin`.
17    pub role: Option<String>,
18
19    /// Clerk user ID of the authenticated user.
20    pub sub: String,
21
22    /// Domain of the clerk backend.
23    pub iss: String,
24
25    /// Unique ID of the token.
26    pub jti: String,
27
28    /// Expiration timestamp of the token.
29    pub exp: usize,
30
31    /// Timestamp when the token was issued.
32    pub iat: usize,
33
34    /// Timestamp before the token is invalid.
35    pub nbf: usize,
36}
37
38/// Decodes a JWT token into a [`TokenData<Claims>`].
39fn decode_jwt(jwt_token: &str) -> Result<TokenData<Claims>> {
40    let config = load_config();
41    debug!("JWT Token: {:?}", jwt_token);
42    debug!("Config: {:?}", config);
43    let decoding_key = DecodingKey::from_rsa_components(
44        config.CLERK_RSA_MODULUS.as_str(),
45        config.CLERK_RSA_EXPONENT.as_str(),
46    )
47    .map_err(|_| Error::InvalidDecodingKey)?;
48
49    decode(
50        jwt_token,
51        &decoding_key,
52        &Validation::new(jsonwebtoken::Algorithm::RS256),
53    )
54    .map_err(|_| Error::InvalidToken)
55}
56
57/// Retrieve the user information from a JWT token. Do not include the `Bearer` prefix, only the
58/// actual JWT token.
59pub fn authenticate_user(auth_header: &str) -> Result<CurrentUser> {
60    let user_info = decode_jwt(auth_header)?;
61    Ok(CurrentUser {
62        user_id: user_info.claims.sub,
63        role: user_info.claims.role,
64        internal_id: None,
65    })
66}
67
68#[cfg(test)]
69mod test {
70    use crate::middleware::auth::core::authenticate_user;
71    use crate::middleware::auth::layer::CurrentUser;
72    use std::env;
73
74    use super::decode_jwt;
75    use jsonwebtoken::Algorithm;
76
77    #[test]
78    fn test_decode_jwt_ok() {
79        let _ = dotenv::from_filename(".env.test");
80        let jwt_token = env::var("JWT_TOKEN").unwrap();
81
82        let token_data = decode_jwt(jwt_token.as_str()).unwrap();
83        assert_eq!(token_data.header.alg, Algorithm::RS256);
84        assert_eq!(token_data.header.typ, Some("JWT".to_string()));
85        assert_eq!(token_data.claims.sub, "user_2ruHSXCzfIRreR2tpttVQBl512a");
86        assert_eq!(token_data.claims.role, None);
87    }
88
89    // This test will fail if you use `cargo test` because env vars are shared
90    // even across threads.
91    //
92    // Use `cargo nextest run` instead as it runs tests in parallel and isolation.
93    #[test]
94    fn test_decode_jwt_invalid_decoding_key_err() {
95        unsafe {
96            env::set_var("DATABASE_URL", "test_url");
97            env::set_var("LANGGRAPH_ASSISTANT_ENDPOINT", "test_url");
98            env::set_var("CLERK_RSA_MODULUS", "invalid_base64@string!");
99            env::set_var("CLERK_RSA_EXPONENT", "test827@0.");
100        }
101
102        let _ = dotenv::from_filename(".env.test");
103        let jwt_token = env::var("JWT_TOKEN").unwrap();
104        let token_data = decode_jwt(jwt_token.as_str());
105        dbg!("Token data: {:?}", &token_data);
106        assert!(
107            matches!(token_data, Err(super::Error::InvalidDecodingKey)),
108            "Expected InvalidDecodingKey error"
109        );
110    }
111
112    #[test]
113    fn test_decode_jwt_invalid_token_err() {
114        let _ = dotenv::from_filename(".env.test");
115        let jwt_token = "invalid_token";
116        let token_data = decode_jwt(jwt_token);
117        dbg!("Token data: {:?}", &token_data);
118        assert!(
119            matches!(token_data, Err(super::Error::InvalidToken)),
120            "Expected InvalidToken error"
121        );
122    }
123
124    #[test]
125    fn test_authenticate_user_ok() {
126        let _ = dotenv::from_filename(".env.test");
127        let jwt_token = env::var("JWT_TOKEN").unwrap();
128
129        let current_user = authenticate_user(jwt_token.as_str()).unwrap();
130        assert_eq!(
131            current_user,
132            CurrentUser {
133                user_id: String::from("user_2ruHSXCzfIRreR2tpttVQBl512a"),
134                role: None,
135                internal_id: None,
136            }
137        );
138    }
139
140    #[test]
141    fn test_authenticate_user_invalid_token_err() {
142        let jwt_token = "invalid_token";
143
144        let _ = dotenv::from_filename(".env.test");
145        let current_user = authenticate_user(jwt_token);
146        assert!(
147            matches!(current_user, Err(super::Error::InvalidToken)),
148            "Expected InvalidToken error"
149        );
150    }
151
152    #[test]
153    fn test_authenticate_user_invalid_decoding_key_err() {
154        unsafe {
155            env::set_var("DATABASE_URL", "test_url");
156            env::set_var("LANGGRAPH_ASSISTANT_ENDPOINT", "test_url");
157            env::set_var("CLERK_RSA_MODULUS", "invalid_base64@string!");
158            env::set_var("CLERK_RSA_EXPONENT", "test827@0.");
159        }
160
161        let _ = dotenv::from_filename(".env.test");
162        let jwt_token = env::var("JWT_TOKEN").unwrap();
163
164        let current_user = authenticate_user(jwt_token.as_str());
165        dbg!("Current user: {:?}", &current_user);
166        assert!(
167            matches!(current_user, Err(super::Error::InvalidDecodingKey)),
168            "Expected InvalidDecodingKey error"
169        );
170    }
171}