1use crate::{
4 AuthError, Clock, IdSource, RandomIdSource, RefreshJti, Result, SessionId, Subject, SystemClock,
5};
6use jsonwebtoken::{
7 Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, decode_header, encode,
8};
9use serde::{Deserialize, Serialize};
10use std::{fmt, sync::Arc};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum AuthTokenType {
16 Access,
18 Refresh,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum Restriction {
26 None,
28 MustChangePassword,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct SessionVersion(u64);
36impl SessionVersion {
37 #[must_use]
39 pub const fn new(value: u64) -> Self {
40 Self(value)
41 }
42 #[must_use]
44 pub const fn value(self) -> u64 {
45 self.0
46 }
47 pub fn next(self) -> Result<Self> {
52 self.0
53 .checked_add(1)
54 .map(Self)
55 .ok_or(AuthError::InvalidPolicy)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct TokenClaims {
63 pub sub: Subject,
65 pub sid: SessionId,
67 pub session_version: SessionVersion,
69 pub token_type: AuthTokenType,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub jti: Option<RefreshJti>,
74 pub restriction: Restriction,
76 pub iat: i64,
78 pub exp: i64,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub nbf: Option<i64>,
83 pub iss: String,
85 pub aud: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct VerifiedToken(TokenClaims);
93impl VerifiedToken {
94 #[must_use]
96 pub const fn claims(&self) -> &TokenClaims {
97 &self.0
98 }
99 #[must_use]
101 pub fn into_claims(self) -> TokenClaims {
102 self.0
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum JwtAlgorithm {
109 Hs256,
111 Hs384,
113 Hs512,
115}
116impl JwtAlgorithm {
117 const fn upstream(self) -> Algorithm {
118 match self {
119 Self::Hs256 => Algorithm::HS256,
120 Self::Hs384 => Algorithm::HS384,
121 Self::Hs512 => Algorithm::HS512,
122 }
123 }
124 const fn minimum_key_bytes(self) -> usize {
125 match self {
126 Self::Hs256 => 32,
127 Self::Hs384 => 48,
128 Self::Hs512 => 64,
129 }
130 }
131}
132
133#[derive(Clone)]
135pub struct JwtKey(Arc<[u8]>);
136impl JwtKey {
137 pub fn hmac(secret: impl AsRef<[u8]>) -> Result<Self> {
142 let secret = secret.as_ref();
143 if secret.len() < 32 {
144 return Err(AuthError::InvalidPolicy);
145 }
146 Ok(Self(Arc::from(secret)))
147 }
148 fn check(&self, algorithm: JwtAlgorithm) -> Result<()> {
149 if self.0.len() < algorithm.minimum_key_bytes() {
150 return Err(AuthError::InvalidPolicy);
151 }
152 Ok(())
153 }
154}
155impl fmt::Debug for JwtKey {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 f.write_str("JwtKey(<redacted>)")
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct JwtPolicy {
165 issuer: String,
166 audience: String,
167 access_ttl_secs: u64,
168 refresh_ttl_secs: u64,
169 algorithm: JwtAlgorithm,
170 leeway_secs: u64,
171 max_token_bytes: usize,
172}
173impl JwtPolicy {
174 pub fn new(
179 issuer: impl Into<String>,
180 audience: impl Into<String>,
181 access_ttl_secs: u64,
182 refresh_ttl_secs: u64,
183 ) -> Result<Self> {
184 let issuer = issuer.into();
185 let audience = audience.into();
186 crate::ids::validate_identifier(&issuer)?;
187 crate::ids::validate_identifier(&audience)?;
188 for ttl in [access_ttl_secs, refresh_ttl_secs] {
189 if ttl == 0 || i64::try_from(ttl).is_err() {
190 return Err(AuthError::InvalidPolicy);
191 }
192 }
193 Ok(Self {
194 issuer,
195 audience,
196 access_ttl_secs,
197 refresh_ttl_secs,
198 algorithm: JwtAlgorithm::Hs256,
199 leeway_secs: 0,
200 max_token_bytes: 16 * 1024,
201 })
202 }
203 #[must_use]
205 pub const fn with_algorithm(mut self, algorithm: JwtAlgorithm) -> Self {
206 self.algorithm = algorithm;
207 self
208 }
209 pub fn with_leeway(mut self, seconds: u64) -> Result<Self> {
214 i64::try_from(seconds).map_err(|_| AuthError::InvalidPolicy)?;
215 self.leeway_secs = seconds;
216 Ok(self)
217 }
218 pub fn with_max_token_bytes(mut self, bytes: usize) -> Result<Self> {
223 if bytes == 0 {
224 return Err(AuthError::InvalidPolicy);
225 }
226 self.max_token_bytes = bytes;
227 Ok(self)
228 }
229 #[must_use]
231 pub fn issuer(&self) -> &str {
232 &self.issuer
233 }
234 #[must_use]
236 pub fn audience(&self) -> &str {
237 &self.audience
238 }
239 #[must_use]
241 pub const fn access_ttl_secs(&self) -> u64 {
242 self.access_ttl_secs
243 }
244 #[must_use]
246 pub const fn refresh_ttl_secs(&self) -> u64 {
247 self.refresh_ttl_secs
248 }
249 #[must_use]
251 pub const fn leeway_secs(&self) -> u64 {
252 self.leeway_secs
253 }
254 #[must_use]
256 pub const fn max_token_bytes(&self) -> usize {
257 self.max_token_bytes
258 }
259}
260
261#[derive(Clone, PartialEq, Eq)]
263pub struct AuthTokenPair {
264 pub access_token: String,
266 pub refresh_token: String,
268 pub refresh_jti: RefreshJti,
270 pub access_expires_at: i64,
272 pub refresh_expires_at: i64,
274}
275impl fmt::Debug for AuthTokenPair {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 f.debug_struct("AuthTokenPair")
278 .field("access_expires_at", &self.access_expires_at)
279 .field("refresh_expires_at", &self.refresh_expires_at)
280 .finish_non_exhaustive()
281 }
282}
283
284#[derive(Clone)]
286pub struct JwtSigner {
287 key: JwtKey,
288 policy: JwtPolicy,
289 clock: Arc<dyn Clock>,
290 ids: Arc<dyn IdSource>,
291}
292impl fmt::Debug for JwtSigner {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 f.debug_struct("JwtSigner")
295 .field("policy", &self.policy)
296 .finish_non_exhaustive()
297 }
298}
299impl JwtSigner {
300 #[must_use]
302 pub fn new(
303 key: JwtKey,
304 policy: JwtPolicy,
305 clock: Arc<dyn Clock>,
306 ids: Arc<dyn IdSource>,
307 ) -> Self {
308 Self {
309 key,
310 policy,
311 clock,
312 ids,
313 }
314 }
315 #[must_use]
317 pub fn with_system_clock(key: JwtKey, policy: JwtPolicy) -> Self {
318 Self::new(key, policy, Arc::new(SystemClock), Arc::new(RandomIdSource))
319 }
320 pub fn issue(
327 &self,
328 subject: Subject,
329 session: SessionId,
330 version: SessionVersion,
331 restriction: Restriction,
332 ) -> Result<AuthTokenPair> {
333 crate::provider::ensure_initialized()?;
334 self.key.check(self.policy.algorithm)?;
335 let now = self.clock.now_unix_seconds()?;
336 if now < 0 {
337 return Err(AuthError::InvalidPolicy);
338 }
339 let expiry = |ttl| {
340 now.checked_add(i64::try_from(ttl).map_err(|_| AuthError::InvalidPolicy)?)
341 .ok_or(AuthError::InvalidPolicy)
342 };
343 let access_expires_at = expiry(self.policy.access_ttl_secs)?;
344 let refresh_expires_at = expiry(self.policy.refresh_ttl_secs)?;
345 let refresh_jti = self.ids.next_id()?;
346 let mut claims = TokenClaims {
347 sub: subject,
348 sid: session,
349 session_version: version,
350 token_type: AuthTokenType::Access,
351 jti: None,
352 restriction,
353 iat: now,
354 exp: access_expires_at,
355 nbf: None,
356 iss: self.policy.issuer.clone(),
357 aud: self.policy.audience.clone(),
358 };
359 let header = Header::new(self.policy.algorithm.upstream());
360 let key = EncodingKey::from_secret(&self.key.0);
361 let access_token = encode(&header, &claims, &key).map_err(|_| AuthError::Encoding)?;
362 claims.token_type = AuthTokenType::Refresh;
363 claims.jti = Some(refresh_jti.clone());
364 claims.exp = refresh_expires_at;
365 let refresh_token = encode(&header, &claims, &key).map_err(|_| AuthError::Encoding)?;
366 if access_token.len() > self.policy.max_token_bytes
367 || refresh_token.len() > self.policy.max_token_bytes
368 {
369 return Err(AuthError::TokenTooLarge);
370 }
371 Ok(AuthTokenPair {
372 access_token,
373 refresh_token,
374 refresh_jti,
375 access_expires_at,
376 refresh_expires_at,
377 })
378 }
379}
380
381#[derive(Clone)]
383pub struct JwtVerifier {
384 key: JwtKey,
385 policy: JwtPolicy,
386 clock: Arc<dyn Clock>,
387}
388impl fmt::Debug for JwtVerifier {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.debug_struct("JwtVerifier")
391 .field("policy", &self.policy)
392 .finish_non_exhaustive()
393 }
394}
395impl JwtVerifier {
396 pub(crate) const fn leeway_secs(&self) -> u64 {
397 self.policy.leeway_secs
398 }
399 #[must_use]
401 pub fn new(key: JwtKey, policy: JwtPolicy, clock: Arc<dyn Clock>) -> Self {
402 Self { key, policy, clock }
403 }
404 #[must_use]
406 pub fn with_system_clock(key: JwtKey, policy: JwtPolicy) -> Self {
407 Self::new(key, policy, Arc::new(SystemClock))
408 }
409 pub fn verify(&self, token: &str, expected: AuthTokenType) -> Result<VerifiedToken> {
416 crate::provider::ensure_initialized()?;
417 self.key.check(self.policy.algorithm)?;
418 if token.len() > self.policy.max_token_bytes {
419 return Err(AuthError::TokenTooLarge);
420 }
421 let header = decode_header(token).map_err(|_| AuthError::InvalidToken)?;
422 if header.alg != self.policy.algorithm.upstream() {
423 return Err(AuthError::AlgorithmMismatch);
424 }
425 let mut validation = Validation::new(self.policy.algorithm.upstream());
426 validation.validate_exp = false;
428 validation.validate_nbf = false;
429 validation.validate_aud = false;
430 let claims =
431 decode::<TokenClaims>(token, &DecodingKey::from_secret(&self.key.0), &validation)
432 .map_err(|_| AuthError::InvalidToken)?
433 .claims;
434 if claims.iss != self.policy.issuer {
435 return Err(AuthError::IssuerMismatch);
436 }
437 if claims.aud != self.policy.audience {
438 return Err(AuthError::AudienceMismatch);
439 }
440 if claims.token_type != expected {
441 return Err(AuthError::WrongTokenType);
442 }
443 match (expected, &claims.jti) {
444 (AuthTokenType::Refresh, None) => return Err(AuthError::MissingClaim),
445 (AuthTokenType::Access, Some(_)) => return Err(AuthError::InvalidToken),
446 _ => {}
447 }
448 if claims.iat < 0
449 || claims.exp <= claims.iat
450 || claims.nbf.is_some_and(|nbf| nbf < 0 || nbf >= claims.exp)
451 {
452 return Err(AuthError::InvalidToken);
453 }
454 let now = self.clock.now_unix_seconds()?;
455 if now < 0 {
456 return Err(AuthError::InvalidPolicy);
457 }
458 let leeway =
459 i64::try_from(self.policy.leeway_secs).map_err(|_| AuthError::InvalidPolicy)?;
460 let latest = i128::from(now) + i128::from(leeway);
462 if i128::from(claims.iat) > latest || claims.nbf.is_some_and(|nbf| i128::from(nbf) > latest)
463 {
464 return Err(AuthError::NotYetValid);
465 }
466 if i128::from(now) >= i128::from(claims.exp) + i128::from(leeway) {
467 return Err(AuthError::Expired);
468 }
469 Ok(VerifiedToken(claims))
470 }
471}