Skip to main content

rskit_auth/jwt/
service.rs

1use std::marker::PhantomData;
2
3use async_trait::async_trait;
4use rskit_errors::AppResult;
5use serde::{Serialize, de::DeserializeOwned};
6
7use super::{JwtCodec, JwtConfig};
8use crate::traits::{TokenGenerator, TokenValidator};
9
10/// JWT sign/verify service generic over the claims type `C`.
11pub struct JwtService<C> {
12    codec: JwtCodec,
13    _claims: PhantomData<C>,
14}
15
16impl<C> JwtService<C> {
17    /// Create a new [`JwtService`] from the given configuration.
18    ///
19    /// # Errors
20    /// Returns an error when key material or validation policy is invalid.
21    pub fn new(config: JwtConfig) -> AppResult<Self> {
22        Ok(Self {
23            codec: JwtCodec::new(config)?,
24            _claims: PhantomData,
25        })
26    }
27
28    /// Return the reusable JWT codec backing this service.
29    #[must_use]
30    pub const fn codec(&self) -> &JwtCodec {
31        &self.codec
32    }
33}
34
35#[async_trait]
36impl<C: Serialize + DeserializeOwned + Send + Sync> TokenGenerator<C> for JwtService<C> {
37    async fn generate(&self, claims: &C) -> AppResult<String> {
38        self.codec.encode(claims)
39    }
40}
41
42#[async_trait]
43impl<C: Serialize + DeserializeOwned + Send + Sync> TokenValidator<C> for JwtService<C> {
44    async fn validate(&self, token: &str) -> AppResult<C> {
45        self.codec.decode(token)
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use serde::{Deserialize, Serialize};
53
54    const ISSUER: &str = "https://issuer.example";
55    const AUDIENCE: &str = "rskit-tests";
56
57    const RSA_PRIVATE_KEY: &str = include_str!(concat!(
58        env!("CARGO_MANIFEST_DIR"),
59        "/testdata/rsa_private_key.pem"
60    ));
61    const RSA_PUBLIC_KEY: &str = include_str!(concat!(
62        env!("CARGO_MANIFEST_DIR"),
63        "/testdata/rsa_public_key.pem"
64    ));
65
66    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67    struct TestClaims {
68        sub: String,
69        iss: String,
70        aud: Vec<String>,
71        exp: u64,
72        nbf: u64,
73        iat: u64,
74    }
75
76    fn future_timestamp() -> u64 {
77        (std::time::SystemTime::now()
78            .duration_since(std::time::UNIX_EPOCH)
79            .unwrap()
80            .as_secs())
81            + 3600
82    }
83
84    fn current_timestamp() -> u64 {
85        std::time::SystemTime::now()
86            .duration_since(std::time::UNIX_EPOCH)
87            .unwrap()
88            .as_secs()
89    }
90
91    fn symmetric_service() -> JwtService<TestClaims> {
92        JwtService::new(JwtConfig::hs256_internal(
93            "test-secret-key-32-bytes-minimum!",
94            ISSUER,
95            vec![AUDIENCE.to_string()],
96        ))
97        .unwrap()
98    }
99
100    #[tokio::test]
101    async fn roundtrip_sign_and_verify() {
102        let svc = symmetric_service();
103        let now = current_timestamp();
104        let claims = TestClaims {
105            sub: "user-123".into(),
106            iss: ISSUER.into(),
107            aud: vec![AUDIENCE.into()],
108            exp: future_timestamp(),
109            nbf: now.saturating_sub(1),
110            iat: now,
111        };
112
113        let token = svc.generate(&claims).await.unwrap();
114        let decoded = svc.validate(&token).await.unwrap();
115        assert_eq!(decoded, claims);
116    }
117
118    #[tokio::test]
119    async fn expired_token_returns_token_expired_error() {
120        let svc = symmetric_service();
121        let now = current_timestamp();
122        let claims = TestClaims {
123            sub: "user-123".into(),
124            iss: ISSUER.into(),
125            aud: vec![AUDIENCE.into()],
126            exp: 1,
127            nbf: now.saturating_sub(1),
128            iat: now,
129        };
130        let token = svc.generate(&claims).await.unwrap();
131        let result = svc.validate(&token).await;
132        assert!(result.is_err());
133    }
134
135    #[tokio::test]
136    async fn rs256_roundtrip_is_supported() {
137        let svc = JwtService::<TestClaims>::new(JwtConfig::rs256(
138            RSA_PRIVATE_KEY,
139            RSA_PUBLIC_KEY,
140            ISSUER,
141            vec![AUDIENCE.to_string()],
142        ))
143        .unwrap();
144        let now = current_timestamp();
145        let claims = TestClaims {
146            sub: "user-123".into(),
147            iss: ISSUER.into(),
148            aud: vec![AUDIENCE.into()],
149            exp: future_timestamp(),
150            nbf: now.saturating_sub(1),
151            iat: now,
152        };
153        let token = svc.generate(&claims).await.unwrap();
154        let decoded = svc.validate(&token).await.unwrap();
155        assert_eq!(decoded.sub, "user-123");
156    }
157
158    #[tokio::test]
159    async fn alg_none_is_rejected() {
160        let svc = symmetric_service();
161        let token = concat!(
162            "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.",
163            "eyJzdWIiOiJ1c2VyLTEyMyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUiLCJhdWQiOlsicnNraXQtdGVzdHMiXSwiZXhwIjo0MTAyNDQ0ODAwLCJuYmYiOjE3MDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMH0.",
164            ""
165        );
166
167        let result = svc.validate(token).await;
168        assert!(result.is_err());
169    }
170}