Skip to main content

wowlab_centrifuge/
token.rs

1use jsonwebtoken::{EncodingKey, Header, encode};
2use wowlab_common::time;
3use wowlab_types::{constants::SECONDS_PER_DAY, sensitive::Sensitive};
4
5#[derive(serde::Serialize)]
6struct Claims {
7    sub: String,
8    exp: i64,
9}
10
11/// Failure to encode a Centrifugo JWT.
12#[derive(Debug, thiserror::Error)]
13#[error("failed to encode Centrifugo token: {source}")]
14pub struct TokenError {
15    #[source]
16    source: jsonwebtoken::errors::Error,
17}
18
19/// Generates a one-day Centrifugo JWT for `subject`.
20/// # Errors
21/// Returns an error if the claims cannot be encoded with `secret`.
22pub fn generate_token(subject: &str, secret: &str) -> Result<Sensitive<String>, TokenError> {
23    let now = i64::try_from(time::unix_timestamp_secs()).unwrap_or(i64::MAX);
24    let ttl = i64::try_from(SECONDS_PER_DAY).unwrap_or(i64::MAX);
25    let exp = now.saturating_add(ttl);
26
27    let claims = Claims {
28        sub: subject.to_string(),
29        exp,
30    };
31
32    encode(
33        &Header::default(),
34        &claims,
35        &EncodingKey::from_secret(secret.as_bytes()),
36    )
37    .map(Sensitive::new)
38    .map_err(|source| TokenError { source })
39}
40
41#[cfg(test)]
42mod tests {
43    use std::error::Error as _;
44
45    use googletest::prelude::*;
46    use jsonwebtoken::EncodingKey;
47
48    use super::{TokenError, generate_token};
49
50    #[gtest]
51    fn generated_token_is_redacted_and_empty_inputs_remain_supported() -> Result<()> {
52        let token = generate_token("", "").or_fail()?;
53
54        verify_true!(!token.expose().is_empty())?;
55        verify_that!(token.to_string(), eq("[REDACTED]"))?;
56
57        verify_that!(
58            format!("{token:?}"),
59            not(contains_substring(token.expose().as_str()))
60        )
61    }
62
63    #[gtest]
64    fn token_error_preserves_jsonwebtoken_source_without_secret_context() -> Result<()> {
65        let source = EncodingKey::from_rsa_pem(b"invalid key").err().or_fail()?;
66        let error = TokenError { source };
67
68        verify_true!(
69            error
70                .source()
71                .and_then(|source| source.downcast_ref::<jsonwebtoken::errors::Error>())
72                .is_some()
73        )?;
74
75        verify_that!(
76            error.to_string(),
77            not(contains_substring("connection-secret"))
78        )
79    }
80}