Skip to main content

sofie_auth/
bearer.rs

1use std::fmt;
2
3use sofie_core::Result;
4
5use sofie_core::encoding::base64::{self, URL_SAFE};
6
7const SECRET_BYTES: usize = 32;
8const TEXT_LENGTH: usize = (SECRET_BYTES * 4).div_ceil(3);
9
10/// Shown once when issued; only its digest is stored.
11pub struct BearerToken(String);
12
13impl BearerToken {
14    pub fn generate() -> Result<Self> {
15        let secret: [u8; SECRET_BYTES] = crate::random_bytes("a token")?;
16        Ok(Self(base64::encode(&URL_SAFE, &secret)))
17    }
18
19    /// `None` for text no issued token has, which is then never looked up.
20    #[must_use]
21    pub fn presented(text: &str) -> Option<Self> {
22        let well_formed = text.len() == TEXT_LENGTH
23            && text
24                .bytes()
25                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_');
26        well_formed.then(|| Self(text.to_owned()))
27    }
28
29    #[must_use]
30    pub fn expose(&self) -> &str {
31        &self.0
32    }
33
34    /// Looked up by this, not compared: timing could only tell digests apart, never a token.
35    #[must_use]
36    pub fn digest(&self) -> String {
37        crate::digest(&[self.0.as_bytes()])
38    }
39}
40
41impl fmt::Debug for BearerToken {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str("BearerToken(…)")
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::BearerToken;
50
51    #[test]
52    fn a_token_is_random_presentable_and_never_printed() {
53        let token = BearerToken::generate().unwrap();
54        let again = BearerToken::presented(token.expose()).unwrap();
55
56        assert_eq!(token.digest(), again.digest());
57        assert_ne!(token.expose(), BearerToken::generate().unwrap().expose());
58        assert_eq!(format!("{token:?}"), "BearerToken(…)");
59        assert!(BearerToken::presented("short").is_none());
60        assert!(BearerToken::presented(&format!("{}=", &token.expose()[1..])).is_none());
61    }
62}