rskit_auth/jwt/
service.rs1use 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
10pub struct JwtService<C> {
12 codec: JwtCodec,
13 _claims: PhantomData<C>,
14}
15
16impl<C> JwtService<C> {
17 pub fn new(config: JwtConfig) -> AppResult<Self> {
22 Ok(Self {
23 codec: JwtCodec::new(config)?,
24 _claims: PhantomData,
25 })
26 }
27
28 #[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}