Skip to main content

headless_lms_models/library/oauth/
tokens.rs

1use hmac::{Hmac, KeyInit, Mac};
2use rand::RngExt;
3use rand::distr::SampleString;
4use rand::rng;
5use secrecy::{ExposeSecret, SecretString};
6use sha2::Sha256;
7
8use crate::library::oauth::Digest;
9
10const ACCESS_TOKEN_LENGTH: usize = 64;
11
12/// Crockford base32 alphabet (excludes I, L, O, U to avoid ambiguity with 1/0).
13const USER_CODE_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
14/// Number of characters in each dash-separated group of a user code.
15const USER_CODE_GROUP_LEN: usize = 4;
16
17/// Generate a cryptographically strong opaque token suitable for access/refresh/auth codes.
18pub fn generate_access_token() -> String {
19    rand::distr::Alphanumeric.sample_string(&mut rng(), ACCESS_TOKEN_LENGTH)
20}
21
22/// Generate a human-typable `user_code` for the OAuth 2.0 Device Authorization
23/// Grant (RFC 8628).
24///
25/// The code is 8 characters of Crockford base32 (no I, L, O, U), formatted as
26/// two dash-separated groups: `XXXX-XXXX`. Characters are drawn from a
27/// cryptographically secure RNG. The alphabet has 32 symbols, so index
28/// selection over `0..32` is bias-free.
29pub fn generate_user_code() -> String {
30    let mut rng = rng();
31    let mut code = String::with_capacity(USER_CODE_GROUP_LEN * 2 + 1);
32    for i in 0..(USER_CODE_GROUP_LEN * 2) {
33        if i == USER_CODE_GROUP_LEN {
34            code.push('-');
35        }
36        let idx = rng.random_range(0..USER_CODE_ALPHABET.len());
37        code.push(USER_CODE_ALPHABET[idx] as char);
38    }
39    code
40}
41
42/// Produce a `Digest` (HMAC-SHA-256) from an access/refresh token plaintext using a secret key.
43///
44/// This function uses HMAC-SHA-256 instead of plain SHA-256 to provide better security
45/// by requiring knowledge of the secret key to compute valid digests.
46///
47/// # Arguments
48/// * `token_plaintext` - The token string to hash
49/// * `key` - The secret key (pepper) to use for HMAC
50pub fn token_digest_sha256(token_plaintext: &str, key: &SecretString) -> Digest {
51    // The HMAC key is exposed only here, where it is fed into the MAC.
52    let mut mac = Hmac::<Sha256>::new_from_slice(key.expose_secret().as_bytes())
53        .expect("HMAC can take key of any size");
54    mac.update(token_plaintext.as_bytes());
55    let result = mac.finalize();
56    let code_bytes = result.into_bytes();
57    let mut arr = [0u8; Digest::LEN];
58    arr.copy_from_slice(&code_bytes);
59    Digest::new(arr)
60}