uselesskey_core_token_shape/
lib.rs1#![forbid(unsafe_code)]
2
3use base64::Engine as _;
30use base64::engine::general_purpose::URL_SAFE_NO_PAD;
31use rand_chacha10::ChaCha20Rng;
32use rand_core10::{Rng, SeedableRng};
33
34use serde_json::json;
35pub use uselesskey_core_base62::random_base62;
36use uselesskey_core_seed::Seed;
37
38pub const API_KEY_PREFIX: &str = "uk_test_";
40
41pub const API_KEY_RANDOM_LEN: usize = 32;
43
44pub const BEARER_RANDOM_BYTES: usize = 32;
46
47pub const OAUTH_JTI_BYTES: usize = 16;
49
50pub const OAUTH_SIGNATURE_BYTES: usize = 32;
52
53pub use uselesskey_token_spec::TokenSpec as TokenKind;
55
56pub fn generate_token(label: &str, kind: TokenKind, seed: Seed) -> String {
58 match kind {
59 TokenKind::ApiKey => generate_api_key(seed),
60 TokenKind::Bearer => generate_bearer_token(seed),
61 TokenKind::OAuthAccessToken => generate_oauth_access_token(label, seed),
62 }
63}
64
65pub fn authorization_scheme(kind: TokenKind) -> &'static str {
67 kind.authorization_scheme()
68}
69
70pub fn generate_api_key(seed: Seed) -> String {
72 let mut out = String::from(API_KEY_PREFIX);
73 out.push_str(&random_base62(seed, API_KEY_RANDOM_LEN));
74 out
75}
76
77pub fn generate_bearer_token(seed: Seed) -> String {
79 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
80 let mut bytes = [0u8; BEARER_RANDOM_BYTES];
81 rng.fill_bytes(&mut bytes);
82 URL_SAFE_NO_PAD.encode(bytes)
83}
84
85pub fn generate_oauth_access_token(label: &str, seed: Seed) -> String {
87 let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256","typ":"JWT"}"#);
88 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
89
90 let mut jti = [0u8; OAUTH_JTI_BYTES];
91 rng.fill_bytes(&mut jti);
92
93 let payload = json!({
94 "iss": "uselesskey",
95 "sub": label,
96 "aud": "tests",
97 "scope": "fixture.read",
98 "jti": URL_SAFE_NO_PAD.encode(jti),
99 "exp": 2_000_000_000u64,
100 });
101 let payload_json = serde_json::to_vec(&payload).expect("payload JSON");
102 let payload_segment = URL_SAFE_NO_PAD.encode(payload_json);
103
104 let mut signature = [0u8; OAUTH_SIGNATURE_BYTES];
105 rng.fill_bytes(&mut signature);
106 let signature_segment = URL_SAFE_NO_PAD.encode(signature);
107
108 format!("{header}.{payload_segment}.{signature_segment}")
109}
110
111#[cfg(test)]
112mod tests {
113 use base64::Engine as _;
114 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
115 use proptest::prelude::*;
116 use uselesskey_core_seed::Seed;
117
118 use super::{
119 API_KEY_PREFIX, API_KEY_RANDOM_LEN, BEARER_RANDOM_BYTES, TokenKind, authorization_scheme,
120 generate_api_key, generate_bearer_token, generate_oauth_access_token, generate_token,
121 };
122 use uselesskey_core_base62::random_base62;
123
124 #[test]
125 fn api_key_shape_is_stable() {
126 let value = generate_api_key(Seed::new([7u8; 32]));
127
128 assert!(value.starts_with(API_KEY_PREFIX));
129 let suffix = value
130 .strip_prefix(API_KEY_PREFIX)
131 .expect("API key prefix should be present");
132 assert_eq!(suffix.len(), API_KEY_RANDOM_LEN);
133 assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
134 }
135
136 #[test]
137 fn bearer_shape_decodes_to_32_bytes() {
138 let value = generate_bearer_token(Seed::new([9u8; 32]));
139 let decoded = URL_SAFE_NO_PAD.decode(value).expect("base64url decode");
140 assert_eq!(decoded.len(), BEARER_RANDOM_BYTES);
141 }
142
143 #[test]
144 fn oauth_shape_has_three_segments_and_subject() {
145 let value = generate_oauth_access_token("issuer", Seed::new([11u8; 32]));
146 let parts: Vec<&str> = value.split('.').collect();
147 assert_eq!(parts.len(), 3);
148
149 let payload = URL_SAFE_NO_PAD
150 .decode(parts[1])
151 .expect("decode payload segment");
152 let json: serde_json::Value = serde_json::from_slice(&payload).expect("parse payload");
153 assert_eq!(json["sub"], "issuer");
154 assert_eq!(json["iss"], "uselesskey");
155 }
156
157 #[test]
158 fn authorization_scheme_matches_kind() {
159 assert_eq!(authorization_scheme(TokenKind::ApiKey), "ApiKey");
160 assert_eq!(authorization_scheme(TokenKind::Bearer), "Bearer");
161 assert_eq!(authorization_scheme(TokenKind::OAuthAccessToken), "Bearer");
162 }
163
164 #[test]
165 fn generate_token_varies_by_kind() {
166 let seed = [13u8; 32];
167
168 let api = generate_token("label", TokenKind::ApiKey, Seed::new(seed));
169 let bearer = generate_token("label", TokenKind::Bearer, Seed::new(seed));
170 let oauth = generate_token("label", TokenKind::OAuthAccessToken, Seed::new(seed));
171
172 assert_ne!(api, bearer);
173 assert_ne!(api, oauth);
174 assert_ne!(bearer, oauth);
175 }
176
177 #[test]
178 fn random_base62_length_and_charset() {
179 let value = random_base62(Seed::new([17u8; 32]), 64);
180 assert_eq!(value.len(), 64);
181 assert!(value.chars().all(|c| c.is_ascii_alphanumeric()));
182 }
183
184 proptest! {
185 #[test]
186 fn api_key_same_seed_stable(seed in any::<[u8; 32]>()) {
187 let a = generate_api_key(Seed::new(seed));
188 let b = generate_api_key(Seed::new(seed));
189 prop_assert_eq!(a, b);
190 }
191
192 #[test]
193 fn bearer_token_always_43_chars(seed in any::<[u8; 32]>()) {
194 let token = generate_bearer_token(Seed::new(seed));
195 prop_assert_eq!(token.len(), 43);
196 }
197
198 #[test]
199 fn oauth_has_three_segments(seed in any::<[u8; 32]>(), label in "[a-z0-9_-]{1,16}") {
200 let token = generate_oauth_access_token(&label, Seed::new(seed));
201 prop_assert_eq!(token.matches('.').count(), 2);
202 }
203 }
204}