wowlab_centrifuge/
token.rs1use 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#[derive(Debug, thiserror::Error)]
13#[error("failed to encode Centrifugo token: {source}")]
14pub struct TokenError {
15 #[source]
16 source: jsonwebtoken::errors::Error,
17}
18
19pub 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}