diana/auth/jwt.rs
1use chrono::{prelude::Utc, Duration};
2use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::errors::*;
7
8/// The claims made by a JWT, including metadata.
9#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
10pub struct Claims {
11 /// The expiry of the JWT as a datetime in seconds from Unix epoch.
12 pub exp: u64,
13 /// The claims made by the user (non-metadata).
14 pub claims: HashMap<String, String>,
15}
16
17/// A parsed JWT secret. This should be created once with `get_jwt_secret` and then reused as much as possible. You may want to
18/// place it in your context under [`Options`](crate::Options).
19#[derive(Debug, Clone)]
20pub struct JWTSecret<'a> {
21 encoding_key: EncodingKey,
22 decoding_key: DecodingKey<'a>,
23}
24
25/// Transforms a string JWT secret into a form in which it can be used for operations.
26pub fn get_jwt_secret<'a>(secret_str: String) -> Result<JWTSecret<'a>> {
27 let encoding_key = EncodingKey::from_base64_secret(&secret_str)?;
28 let decoding_key = DecodingKey::from_base64_secret(&secret_str)?;
29
30 Ok(JWTSecret {
31 encoding_key,
32 decoding_key,
33 })
34}
35
36/// Decodes time strings like '1w' into actual datetimes from the present moment. If you've ever used NodeJS's [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken) module, this is
37/// very similar (based on Vercel's [`ms`](https://github.com/vercel/ms) module for JavaScript).
38/// Accepts strings of the form 'xXyYzZ...', where the lower-case letters are numbers meaning a number of the intervals X/Y/Z (e.g. 1m4d -- one month four days).
39/// The available intervals are:
40///
41/// - s: second,
42/// - m: minute,
43/// - h: hour,
44/// - d: day,
45/// - w: week,
46/// - M: month (30 days used here, 12M ≠ 1y!),
47/// - y: year (365 days always, leap years ignored, if you want them add them as days)
48pub fn decode_time_str(time_str: &str) -> Result<u64> {
49 let mut duration_after_current = Duration::zero();
50 // Get the current datetime since Unix epoch, we'll add to that
51 let current = Utc::now();
52 // A working variable to store the '123' part of an interval until we reach the idnicator and can do the full conversion
53 let mut curr_duration_length = String::new();
54 // Iterate through the time string's characters to get each interval
55 for c in time_str.chars() {
56 // If we have a number, append it to the working cache
57 // If we have an indicator character, we'll match it to a duration
58 if c.is_numeric() {
59 curr_duration_length.push(c);
60 } else {
61 // Parse the working variable into an actual number
62 let interval_length = curr_duration_length.parse::<i64>().unwrap(); // It's just a string of numbers, we know more than the compiler
63 let duration = match c {
64 's' => Duration::seconds(interval_length),
65 'm' => Duration::minutes(interval_length),
66 'h' => Duration::hours(interval_length),
67 'd' => Duration::days(interval_length),
68 'w' => Duration::weeks(interval_length),
69 'M' => Duration::days(interval_length * 30), // Multiplying the number of months by 30 days (assumed length of a month)
70 'y' => Duration::days(interval_length * 365), // Multiplying the number of years by 365 days (assumed length of a year)
71 c => bail!(ErrorKind::InvalidDatetimeIntervalIndicator(c.to_string())),
72 };
73 duration_after_current = duration_after_current + duration;
74 // Reset that working variable
75 curr_duration_length = String::new();
76 }
77 }
78 // Form the final duration by reducing the durations vector into one
79 let datetime = current + duration_after_current;
80
81 Ok(datetime.timestamp() as u64) // As Unix timestamp in u64 because that's what the JWT demands (we can't have expiries before January 1st 1970, let me know if that's a problem!)
82}
83
84/// Creates a new JWT. You should use this to issue all client JWTs and create the initial JWT for communication with the subscriptions
85/// server (more information in the book).
86pub fn create_jwt(
87 user_claims: HashMap<String, String>,
88 secret: &JWTSecret,
89 exp: u64,
90) -> Result<String> {
91 // Create the claims
92 let claims = Claims {
93 exp,
94 claims: user_claims,
95 };
96 let token = encode(
97 &Header::new(Algorithm::HS512),
98 &claims,
99 &secret.encoding_key,
100 )?;
101
102 Ok(token)
103}
104
105/// Validates a JWT and returns the payload. All client JWTs are automatically validated and their payloads are sent (parsed) to your resolvers,
106/// but if you have a system on top of that you'll want to use this function (not required for normal Diana usage though).
107pub fn validate_and_decode_jwt(jwt: &str, secret: &JWTSecret) -> Option<Claims> {
108 let decoded = decode::<Claims>(
109 jwt,
110 &secret.decoding_key,
111 &Validation::new(Algorithm::HS512),
112 );
113
114 match decoded {
115 Ok(decoded) => Some(decoded.claims),
116 Err(_) => None,
117 }
118}