1pub mod error;
2
3use clap::ValueEnum;
4use jsonwebtoken::{Algorithm, EncodingKey, Header};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::vec;
8use uuid::Uuid;
9use validator::{Validate, ValidationError};
10
11#[cfg(feature = "server-side")]
12use base64::Engine;
13#[cfg(feature = "server-side")]
14use glob::Pattern;
15#[cfg(feature = "server-side")]
16use jsonwebtoken::{DecodingKey, Validation};
17
18use crate::error::AuthError;
19
20#[derive(Clone)]
21pub struct JwtEncoder {
22 pub encoding_key: HashMap<String, (EncodingKey, Algorithm)>,
24
25 kids: Vec<String>
26}
27
28#[cfg(feature = "server-side")]
29#[derive(Clone)]
30pub struct JwtDecoder {
31 #[cfg(feature = "server-side")]
35 decoding_keys: HashMap<(String, String), DecodingKey>,
36
37 #[cfg(feature = "server-side")]
41 validation: Validation,
42}
43
44#[derive(Serialize, Deserialize, Clone, Debug)]
48#[serde(rename_all = "camelCase")]
49pub struct Jwt<P> {
50 pub iss: String,
52
53 pub aud: Vec<String>,
55
56 pub exp: i64,
58
59 pub nbf: i64,
61
62 pub iat: i64,
64
65 pub jti: Uuid,
67
68 pub load: P,
70}
71
72#[derive(Serialize, Deserialize, Validate, Clone, Debug, PartialEq)]
74#[serde(rename_all = "camelCase")]
75pub struct Permission {
76 pub methods: Vec<HttpMethod>,
80
81 #[validate(length(max = 128))]
87 pub resource_pattern: Option<String>,
88
89 pub max_size: Option<usize>,
93
94 #[validate(custom(function = "Self::validate_content_type_pattern"))]
100 pub allowed_content_types: Vec<String>,
101}
102
103#[cfg(feature = "server-side")]
104#[derive(Clone)]
105pub struct CompiledPermission {
106 pub methods: Vec<HttpMethod>,
107 pub resource_pattern: Option<String>,
108 pub max_size: Option<usize>,
109 pub allowed_content_types: Vec<String>,
110 resource_pattern_cache: Option<Pattern>,
111 allowed_content_types_cache: Vec<Pattern>,
112}
113
114#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ValueEnum)]
118#[serde(rename_all = "UPPERCASE")]
119pub enum HttpMethod {
120 Get,
121 Post,
122 Put,
123 Patch,
124 Delete,
125 Head,
126 Options,
127 Trace,
128 Connect,
129 Other,
131 All,
133 Safe,
135 Unsafe,
137}
138
139impl JwtEncoder {
140 #[inline]
141 pub fn new(encoding_key: HashMap<String, (EncodingKey, Algorithm)>) -> Self {
142 let kids = encoding_key.keys().cloned().collect();
143 Self { encoding_key, kids }
144 }
145
146 #[inline]
150 pub fn encode<P: Serialize>(
151 &self,
152 claims: &Jwt<P>,
153 kid: &str,
154 ) -> Result<String, AuthError> {
155 use AuthError::InternalError;
156
157 let (key, alg) = self
158 .encoding_key
159 .get(kid)
160 .ok_or(InternalError("No such kid found in your encoder".into()))?;
161
162 let mut header = Header::new(*alg);
163 header.kid = Some(kid.to_string());
164
165 Ok(jsonwebtoken::encode(&header, claims, key)?)
166 }
167
168 pub fn encode_randomly<P: Serialize>(&self, claims: &Jwt<P>) -> Result<String, AuthError> {
169 let random_kid = &self.kids[rand::random_range(..self.kids.len())];
170 self.encode(claims, random_kid)
171 }
172}
173
174#[cfg(feature = "server-side")]
175impl JwtDecoder {
176 pub fn new<T: ToString, U: ToString>(
201 mapping: HashMap<(String, String), DecodingKey>,
202 algorithms: &[Algorithm],
203 iss: &[T],
204 aud: &[U],
205 ) -> Self {
206 let mut validation =
207 Validation::new(*algorithms.first().expect(
208 "You should provide at least one algorithm in your accepted algorithm slice!",
209 ));
210 validation.validate_aud = true;
211 validation.validate_exp = true;
212 validation.validate_nbf = true;
213 validation.algorithms = algorithms.to_vec();
214 validation.reject_tokens_expiring_in_less_than = 0;
215 validation.leeway = 60;
216 validation.set_issuer(iss);
217 validation.set_audience(aud);
218
219 validation.set_required_spec_claims(&["aud", "exp", "nbf", "iss"]);
224 Self {
225 decoding_keys: mapping,
226 validation,
227 }
228 }
229
230 #[inline]
234 pub fn iss_kid_dec(mut self, mapping: HashMap<(String, String), DecodingKey>) -> Self {
235 self.decoding_keys = mapping;
236 self
237 }
238
239 #[inline]
241 pub fn algorithms(mut self, algorithms: &[Algorithm]) -> Self {
242 self.validation.algorithms = algorithms.to_vec();
243 self
244 }
245
246 #[inline]
248 pub fn authorized_issuer<T: ToString>(mut self, iss: &[T]) -> Self {
249 self.validation.set_issuer(iss);
250 self
251 }
252
253 #[inline]
255 pub fn possible_audience<T: ToString>(mut self, aud: &[T]) -> Self {
256 self.validation.set_audience(aud);
257 self
258 }
259
260 #[inline]
262 pub const fn leeway(mut self, leeway: u64) -> Self {
263 self.validation.leeway = leeway;
264 self
265 }
266
267 #[inline]
269 pub const fn reject_tokens_expiring_in_less_than(mut self, tolerance: u64) -> Self {
270 self.validation.reject_tokens_expiring_in_less_than = tolerance;
271 self
272 }
273
274 #[cfg(feature = "server-side")]
330 pub fn decode<P>(&self, token: &str) -> Result<Jwt<P>, AuthError>
331 where
332 for<'de> P: Deserialize<'de>,
333 {
334 let kid = jsonwebtoken::decode_header(token)?
335 .kid
336 .ok_or(AuthError::MissingClaim("kid".to_string()))?;
337
338 let body_unchecked: Jwt<P> = serde_json::from_value(Self::decode_unchecked(token)?)?;
339
340 let key = self
341 .decoding_keys
342 .get(&(body_unchecked.iss, kid))
343 .ok_or(AuthError::InvalidIssuer)?;
344
345 Ok(jsonwebtoken::decode::<Jwt<P>>(token, key, &self.validation)?.claims)
346 }
347
348 #[cfg(feature = "server-side")]
358 pub fn decode_unchecked(token: &str) -> Result<serde_json::Value, AuthError> {
359 let mut parts = token.split('.');
360 let _header = parts.next();
361 let payload = parts.next().ok_or(AuthError::InvalidToken)?;
362
363 let decoded_payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload)?;
364 let json_value = serde_json::from_slice(&decoded_payload)?;
365
366 Ok(json_value)
367 }
368}
369
370impl<P: Serialize + for<'de> Deserialize<'de>> Jwt<P> {
371 #[inline]
381 pub fn new<T: ToString, U: ToString>(iss: T, aud: &[U], payload: P) -> Self {
382 let now = chrono::Utc::now().timestamp();
383 Self {
384 iss: iss.to_string(),
385 aud: aud.iter().map(|s| s.to_string()).collect(),
386 exp: now + 3600,
387 nbf: now,
388 iat: now,
389 jti: Uuid::new_v4(),
390 load: payload,
391 }
392 }
393
394 #[inline]
396 pub fn expires_in(mut self, duration: chrono::Duration) -> Self {
397 self.exp = (chrono::Utc::now() + duration).timestamp();
398 self
399 }
400
401 #[inline]
403 pub fn expires_at<T>(mut self, when: chrono::DateTime<T>) -> Self
404 where
405 T: chrono::TimeZone,
406 {
407 self.exp = when.timestamp();
408 self
409 }
410
411 #[inline]
413 pub const fn never_expires(mut self) -> Self {
414 self.exp = i32::MAX as i64;
415 self
416 }
417
418 #[inline]
420 pub fn not_valid_in(mut self, duration: chrono::Duration) -> Self {
421 self.nbf = (chrono::Utc::now() + duration).timestamp();
422 self
423 }
424
425 #[inline]
427 pub fn not_valid_till<T>(mut self, when: chrono::DateTime<T>) -> Self
428 where
429 T: chrono::TimeZone,
430 {
431 self.nbf = when.timestamp();
432 self
433 }
434
435 #[inline]
437 pub const fn uuid(mut self, id: Uuid) -> Self {
438 self.jti = id;
439 self
440 }
441}
442
443impl Default for Permission {
444 #[inline]
445 fn default() -> Self {
446 Self::new_minimum()
447 }
448}
449
450impl Permission {
451 fn validate_content_type_pattern(patterns: &[String]) -> Result<(), ValidationError> {
452 if patterns.len() <= 8 && patterns.iter().all(|s| s.len() <= 128) {
453 Ok(())
454 } else {
455 Err(ValidationError::new("pattern too long/much for parsing"))
456 }
457 }
458
459 #[inline]
460 pub const fn new() -> Self {
461 Self::new_minimum()
462 }
463
464 pub fn new_root() -> Self {
475 Self {
476 methods: vec![HttpMethod::All],
477 resource_pattern: Some("*".to_string()),
478 max_size: None,
479 allowed_content_types: vec!["*".to_string()],
480 }
481 }
482
483 pub const fn new_minimum() -> Self {
494 Self {
495 methods: vec![],
496 resource_pattern: None,
497 max_size: Some(0),
498 allowed_content_types: vec![],
499 }
500 }
501
502 #[inline]
506 pub fn permit_method(mut self, methods: Vec<HttpMethod>) -> Self {
507 self.methods = methods;
508 self
509 }
510
511 #[inline]
513 pub fn permit_resource_pattern<T>(mut self, pattern: T) -> Self
514 where
515 T: Into<String>,
516 {
517 self.resource_pattern = Some(pattern.into());
518 self
519 }
520
521 #[inline]
523 pub fn permit_resource_pattern_option<T>(mut self, pattern: Option<T>) -> Self
524 where
525 T: Into<String>,
526 {
527 self.resource_pattern = pattern.map(T::into);
528 self
529 }
530
531 #[inline]
533 pub const fn restrict_maximum_size(mut self, max: usize) -> Self {
534 self.max_size = Some(max);
535 self
536 }
537
538 #[inline]
539 pub const fn restrict_maximum_size_option(mut self, max: Option<usize>) -> Self {
540 self.max_size = max;
541 self
542 }
543
544 #[inline]
546 pub fn permit_content_type(mut self, content_type: Vec<String>) -> Self {
547 self.allowed_content_types = content_type;
548 self
549 }
550
551 #[cfg(feature = "server-side")]
552 pub fn compile(self) -> CompiledPermission {
553 let Permission {
554 methods,
555 resource_pattern,
556 max_size,
557 allowed_content_types,
558 } = self;
559
560 let resource_pattern_cache = match &resource_pattern {
561 Some(pat) => Pattern::new(pat).ok(),
562 None => None,
563 };
564
565 let mut allowed_content_types_cache = vec![];
566
567 for pat in &allowed_content_types {
568 if let Ok(pat) = Pattern::new(pat) {
569 allowed_content_types_cache.push(pat)
570 }
571 }
572
573 CompiledPermission {
574 methods,
575 resource_pattern,
576 max_size,
577 allowed_content_types,
578 resource_pattern_cache,
579 allowed_content_types_cache,
580 }
581 }
582}
583
584#[cfg(feature = "server-side")]
585impl CompiledPermission {
586 pub fn can_perform_method(&self, method: HttpMethod) -> bool {
596 self.methods.contains(&HttpMethod::All)
597 || self.methods.contains(&method)
598 || (self.methods.contains(&HttpMethod::Safe) && method.safe())
599 || (self.methods.contains(&HttpMethod::Unsafe) && !method.safe())
600 }
601
602 pub fn can_access(&self, path: &str) -> bool {
609 match &self.resource_pattern_cache {
610 Some(pat) => pat.matches(path),
611 None => false,
612 }
613 }
614
615 pub fn check_size(&self, size: usize) -> bool {
620 self.max_size.is_none_or(|limit| size <= limit)
621 }
622
623 pub fn check_content_type(&self, content_type: &str) -> bool {
627 self.allowed_content_types_cache
628 .iter()
629 .any(|allow_pat| allow_pat.matches(content_type))
630 }
631}
632
633impl From<&axum::http::Method> for HttpMethod {
634 fn from(value: &axum::http::Method) -> Self {
635 use axum::http::Method;
636
637 match *value {
638 Method::GET => Self::Get,
639 Method::POST => Self::Post,
640 Method::PUT => Self::Put,
641 Method::PATCH => Self::Patch,
642 Method::DELETE => Self::Delete,
643 Method::HEAD => Self::Head,
644 Method::OPTIONS => Self::Options,
645 Method::TRACE => Self::Trace,
646 Method::CONNECT => Self::Connect,
647 _ => Self::Other,
648 }
649 }
650}
651
652impl From<axum::http::Method> for HttpMethod {
653 fn from(value: axum::http::Method) -> Self {
654 Self::from(&value)
655 }
656}
657
658impl HttpMethod {
659 pub fn safe(self) -> bool {
680 match self {
681 HttpMethod::Safe
683 | HttpMethod::Get
684 | HttpMethod::Head
685 | HttpMethod::Options
686 | HttpMethod::Trace => true,
687 HttpMethod::Unsafe
689 | HttpMethod::Connect
690 | HttpMethod::Post
691 | HttpMethod::Put
692 | HttpMethod::Patch
693 | HttpMethod::Delete
694 | HttpMethod::Other
695 | HttpMethod::All => false,
696 }
697 }
698
699 pub fn as_str(self) -> &'static str {
700 match self {
701 HttpMethod::Get => "GET",
702 HttpMethod::Post => "POST",
703 HttpMethod::Put => "PUT",
704 HttpMethod::Patch => "PATCH",
705 HttpMethod::Delete => "DELETE",
706 HttpMethod::Head => "HEAD",
707 HttpMethod::Options => "OPTIONS",
708 HttpMethod::Trace => "TRACE",
709 HttpMethod::Connect => "CONNECT",
710 HttpMethod::Other => "OTHER",
711 HttpMethod::All => "ALL",
712 HttpMethod::Safe => "SAFE",
713 HttpMethod::Unsafe => "UNSAFE",
714 }
715 }
716}