1use base64::Engine as _;
39use base64::engine::general_purpose::URL_SAFE_NO_PAD;
40use rand_chacha10::ChaCha20Rng;
41use rand_core10::{Rng, SeedableRng};
42
43use serde_json::{Map, Value, json};
44use uselesskey_core::Seed;
45
46pub use super::base62::random_base62;
47
48pub const API_KEY_PREFIX: &str = "uk_test_";
50
51pub const API_KEY_RANDOM_LEN: usize = 32;
53
54pub const BEARER_RANDOM_BYTES: usize = 32;
56
57pub const OAUTH_JTI_BYTES: usize = 16;
59
60pub const OAUTH_SIGNATURE_BYTES: usize = 32;
62
63const SCANNER_SAFE_INVALID_TOKEN_SEGMENT: &str = "not_base64url!*";
64
65const NEAR_MISS_API_KEY_PREFIX: &str = "uk_tset_";
66
67pub use super::spec::TokenSpec as TokenKind;
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum NegativeToken {
73 MalformedJwtSegmentCount,
75 BadBase64UrlSegment,
77 InvalidJwtHeaderShape,
79 MissingAlg,
81 AlgNone,
83 MissingKid,
85 MismatchedKid,
87 ExpiredClaims,
89 NotYetValidClaims,
91 BadIssuer,
93 BadAudience,
95 MalformedBearer,
97 NearMissApiKey,
99}
100
101impl NegativeToken {
102 pub const fn variant_name(&self) -> &'static str {
104 match self {
105 Self::MalformedJwtSegmentCount => "malformed_jwt_segment_count",
106 Self::BadBase64UrlSegment => "bad_base64url_segment",
107 Self::InvalidJwtHeaderShape => "invalid_jwt_header_shape",
108 Self::MissingAlg => "missing_alg",
109 Self::AlgNone => "alg_none",
110 Self::MissingKid => "missing_kid",
111 Self::MismatchedKid => "mismatched_kid",
112 Self::ExpiredClaims => "expired_claims",
113 Self::NotYetValidClaims => "not_yet_valid_claims",
114 Self::BadIssuer => "bad_issuer",
115 Self::BadAudience => "bad_audience",
116 Self::MalformedBearer => "malformed_bearer",
117 Self::NearMissApiKey => "near_miss_api_key",
118 }
119 }
120
121 pub const fn stable_id(&self) -> &'static str {
123 match self {
124 Self::MalformedJwtSegmentCount => "jwt_bad_segment_count",
125 Self::BadBase64UrlSegment => "jwt_malformed_base64url",
126 Self::InvalidJwtHeaderShape => "jwt_invalid_header_shape",
127 Self::MissingAlg => "jwt_missing_alg",
128 Self::AlgNone => "jwt_alg_none",
129 Self::MissingKid => "jwt_missing_kid",
130 Self::MismatchedKid => "jwt_mismatched_kid",
131 Self::ExpiredClaims => "jwt_expired",
132 Self::NotYetValidClaims => "jwt_not_yet_valid",
133 Self::BadIssuer => "jwt_bad_issuer",
134 Self::BadAudience => "jwt_bad_audience",
135 Self::MalformedBearer => "token_malformed_bearer",
136 Self::NearMissApiKey => "token_near_miss",
137 }
138 }
139}
140
141pub fn generate_token(label: &str, kind: TokenKind, seed: Seed) -> String {
143 match kind {
144 TokenKind::ApiKey => generate_api_key(seed),
145 TokenKind::Bearer => generate_bearer_token(seed),
146 TokenKind::OAuthAccessToken => generate_oauth_access_token(label, seed),
147 }
148}
149
150pub fn generate_negative_token(
152 label: &str,
153 kind: TokenKind,
154 seed: Seed,
155 variant: NegativeToken,
156) -> String {
157 match variant {
158 NegativeToken::MalformedJwtSegmentCount => malformed_jwt_segment_count(label, seed),
159 NegativeToken::BadBase64UrlSegment => bad_base64url_segment(label, seed),
160 NegativeToken::InvalidJwtHeaderShape => invalid_jwt_header_shape(label, seed),
161 NegativeToken::MissingAlg => missing_alg(label, seed),
162 NegativeToken::AlgNone => alg_none(label, seed),
163 NegativeToken::MissingKid => missing_kid(label, seed),
164 NegativeToken::MismatchedKid => mismatched_kid(label, seed),
165 NegativeToken::ExpiredClaims => token_with_payload_claim(label, seed, "exp", json!(1u64)),
166 NegativeToken::NotYetValidClaims => not_yet_valid_claims(label, seed),
167 NegativeToken::BadIssuer => {
168 token_with_payload_claim(label, seed, "iss", json!("wrong-issuer"))
169 }
170 NegativeToken::BadAudience => {
171 token_with_payload_claim(label, seed, "aud", json!("wrong-audience"))
172 }
173 NegativeToken::MalformedBearer => malformed_bearer(seed),
174 NegativeToken::NearMissApiKey => near_miss_api_key(kind, seed),
175 }
176}
177
178pub fn authorization_scheme(kind: TokenKind) -> &'static str {
180 kind.authorization_scheme()
181}
182
183pub fn generate_api_key(seed: Seed) -> String {
185 let mut out = String::from(API_KEY_PREFIX);
186 out.push_str(&random_base62(seed, API_KEY_RANDOM_LEN));
187 out
188}
189
190pub fn generate_bearer_token(seed: Seed) -> String {
192 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
193 let mut bytes = [0u8; BEARER_RANDOM_BYTES];
194 rng.fill_bytes(&mut bytes);
195 URL_SAFE_NO_PAD.encode(bytes)
196}
197
198pub fn generate_oauth_access_token(label: &str, seed: Seed) -> String {
200 let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256","typ":"JWT"}"#);
201 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
202
203 let mut jti = [0u8; OAUTH_JTI_BYTES];
204 rng.fill_bytes(&mut jti);
205
206 let payload = json!({
207 "iss": "uselesskey",
208 "sub": label,
209 "aud": "tests",
210 "scope": "fixture.read",
211 "jti": URL_SAFE_NO_PAD.encode(jti),
212 "exp": 2_000_000_000u64,
213 });
214 let payload_json = serde_json::to_vec(&payload).expect("payload JSON");
215 let payload_segment = URL_SAFE_NO_PAD.encode(payload_json);
216
217 let mut signature = [0u8; OAUTH_SIGNATURE_BYTES];
218 rng.fill_bytes(&mut signature);
219 let signature_segment = URL_SAFE_NO_PAD.encode(signature);
220
221 format!("{header}.{payload_segment}.{signature_segment}")
222}
223
224fn malformed_jwt_segment_count(label: &str, seed: Seed) -> String {
225 let parts = JwtParts::generated(label, seed);
226 format!("{}.{}", parts.header, parts.payload)
227}
228
229fn bad_base64url_segment(label: &str, seed: Seed) -> String {
230 JwtParts::generated(label, seed)
231 .with_payload_segment(SCANNER_SAFE_INVALID_TOKEN_SEGMENT.to_string())
232 .into_token()
233}
234
235fn invalid_jwt_header_shape(label: &str, seed: Seed) -> String {
236 JwtParts::generated(label, seed)
237 .with_header_segment(encode_json(&json!(["not-a-header"])))
238 .into_token()
239}
240
241fn missing_alg(label: &str, seed: Seed) -> String {
242 JwtParts::generated(label, seed)
243 .with_header_segment(encode_json(&json!({ "typ": "JWT" })))
244 .into_token()
245}
246
247fn alg_none(label: &str, seed: Seed) -> String {
248 token_with_header_claim(label, seed, "alg", json!("none"))
249}
250
251fn missing_kid(label: &str, seed: Seed) -> String {
252 let parts = JwtParts::generated(label, seed);
253 let mut payload = parts.payload_object();
254 payload.insert("kid".to_string(), json!("expected-kid"));
255
256 parts.with_payload_object(payload).into_token()
257}
258
259fn mismatched_kid(label: &str, seed: Seed) -> String {
260 let mut header = jwt_header();
261 header.insert("kid".to_string(), json!("unknown-kid"));
262
263 let parts = JwtParts::generated(label, seed);
264 let mut payload = parts.payload_object();
265 payload.insert("kid".to_string(), json!("expected-kid"));
266
267 parts
268 .with_header_object(header)
269 .with_payload_object(payload)
270 .into_token()
271}
272
273fn not_yet_valid_claims(label: &str, seed: Seed) -> String {
274 let parts = JwtParts::generated(label, seed);
275 let mut claims = parts.payload_object();
276 claims.insert("nbf".to_string(), json!(4_000_000_000u64));
277 claims.insert("exp".to_string(), json!(4_100_000_000u64));
278
279 parts.with_payload_object(claims).into_token()
280}
281
282fn token_with_header_claim(label: &str, seed: Seed, claim: &str, value: Value) -> String {
283 let mut header = jwt_header();
284 header.insert(claim.to_string(), value);
285
286 JwtParts::generated(label, seed)
287 .with_header_object(header)
288 .into_token()
289}
290
291fn token_with_payload_claim(label: &str, seed: Seed, claim: &str, value: Value) -> String {
292 let parts = JwtParts::generated(label, seed);
293 let mut claims = parts.payload_object();
294 claims.insert(claim.to_string(), value);
295
296 parts.with_payload_object(claims).into_token()
297}
298
299fn malformed_bearer(seed: Seed) -> String {
300 let mut value = generate_bearer_token(seed);
301 value.replace_range(0..1, "!");
302 value
303}
304
305fn near_miss_api_key(_kind: TokenKind, seed: Seed) -> String {
306 let valid = generate_api_key(seed);
307 let suffix = valid.strip_prefix(API_KEY_PREFIX).unwrap_or(&valid);
308
309 format!("{NEAR_MISS_API_KEY_PREFIX}{suffix}")
310}
311
312#[derive(Debug)]
313struct JwtParts {
314 header: String,
315 payload: String,
316 signature: String,
317}
318
319impl JwtParts {
320 fn generated(label: &str, seed: Seed) -> Self {
321 let token = generate_oauth_access_token(label, seed);
322 let mut parts = token.split('.');
323 let header = parts.next().expect("JWT header segment").to_string();
324 let payload = parts.next().expect("JWT payload segment").to_string();
325 let signature = parts.next().expect("JWT signature segment").to_string();
326 assert!(
327 parts.next().is_none(),
328 "JWT should have exactly three segments"
329 );
330
331 Self {
332 header,
333 payload,
334 signature,
335 }
336 }
337
338 fn with_header_segment(mut self, header: String) -> Self {
339 self.header = header;
340 self
341 }
342
343 fn with_payload_segment(mut self, payload: String) -> Self {
344 self.payload = payload;
345 self
346 }
347
348 fn with_header_object(self, header: Map<String, Value>) -> Self {
349 self.with_header_segment(encode_object(&header))
350 }
351
352 fn with_payload_object(self, payload: Map<String, Value>) -> Self {
353 self.with_payload_segment(encode_object(&payload))
354 }
355
356 fn payload_object(&self) -> Map<String, Value> {
357 decode_object(&self.payload)
358 }
359
360 fn into_token(self) -> String {
361 format!("{}.{}.{}", self.header, self.payload, self.signature)
362 }
363}
364
365fn jwt_header() -> Map<String, Value> {
366 Map::from_iter([
367 ("alg".to_string(), json!("RS256")),
368 ("typ".to_string(), json!("JWT")),
369 ])
370}
371
372fn decode_object(segment: &str) -> Map<String, Value> {
373 let bytes = URL_SAFE_NO_PAD
374 .decode(segment)
375 .expect("decode generated JWT JSON segment");
376 let value: Value = serde_json::from_slice(&bytes).expect("parse generated JWT JSON segment");
377 value
378 .as_object()
379 .expect("generated JWT JSON segment should be an object")
380 .clone()
381}
382
383fn encode_object(value: &Map<String, Value>) -> String {
384 encode_json(&Value::Object(value.clone()))
385}
386
387fn encode_json(value: &Value) -> String {
388 let json = serde_json::to_vec(value).expect("serialize token JSON");
389 URL_SAFE_NO_PAD.encode(json)
390}
391
392#[cfg(test)]
393mod tests {
394 use base64::Engine as _;
395 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
396 use proptest::prelude::*;
397 use uselesskey_core::Seed;
398
399 use super::{
400 API_KEY_PREFIX, API_KEY_RANDOM_LEN, BEARER_RANDOM_BYTES, NEAR_MISS_API_KEY_PREFIX,
401 NegativeToken, SCANNER_SAFE_INVALID_TOKEN_SEGMENT, TokenKind, authorization_scheme,
402 generate_api_key, generate_bearer_token, generate_negative_token,
403 generate_oauth_access_token, generate_token, random_base62,
404 };
405
406 #[test]
407 fn api_key_shape_is_stable() {
408 let value = generate_api_key(Seed::new([7u8; 32]));
409
410 assert!(value.starts_with(API_KEY_PREFIX));
411 let suffix = value
412 .strip_prefix(API_KEY_PREFIX)
413 .expect("API key prefix should be present");
414 assert_eq!(suffix.len(), API_KEY_RANDOM_LEN);
415 assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
416 }
417
418 #[test]
419 fn bearer_shape_decodes_to_32_bytes() {
420 let value = generate_bearer_token(Seed::new([9u8; 32]));
421 let decoded = URL_SAFE_NO_PAD.decode(value).expect("base64url decode");
422 assert_eq!(decoded.len(), BEARER_RANDOM_BYTES);
423 }
424
425 #[test]
426 fn oauth_shape_has_three_segments_and_subject() {
427 let value = generate_oauth_access_token("issuer", Seed::new([11u8; 32]));
428 let parts: Vec<&str> = value.split('.').collect();
429 assert_eq!(parts.len(), 3);
430
431 let payload = URL_SAFE_NO_PAD
432 .decode(parts[1])
433 .expect("decode payload segment");
434 let json: serde_json::Value = serde_json::from_slice(&payload).expect("parse payload");
435 assert_eq!(json["sub"], "issuer");
436 assert_eq!(json["iss"], "uselesskey");
437 }
438
439 #[test]
440 fn authorization_scheme_matches_kind() {
441 assert_eq!(authorization_scheme(TokenKind::ApiKey), "ApiKey");
442 assert_eq!(authorization_scheme(TokenKind::Bearer), "Bearer");
443 assert_eq!(authorization_scheme(TokenKind::OAuthAccessToken), "Bearer");
444 }
445
446 #[test]
447 fn generate_token_varies_by_kind() {
448 let seed = [13u8; 32];
449
450 let api = generate_token("label", TokenKind::ApiKey, Seed::new(seed));
451 let bearer = generate_token("label", TokenKind::Bearer, Seed::new(seed));
452 let oauth = generate_token("label", TokenKind::OAuthAccessToken, Seed::new(seed));
453
454 assert_ne!(api, bearer);
455 assert_ne!(api, oauth);
456 assert_ne!(bearer, oauth);
457 }
458
459 #[test]
460 fn negative_token_variant_names_are_stable() {
461 assert_eq!(
462 NegativeToken::MalformedJwtSegmentCount.variant_name(),
463 "malformed_jwt_segment_count"
464 );
465 assert_eq!(
466 NegativeToken::BadBase64UrlSegment.variant_name(),
467 "bad_base64url_segment"
468 );
469 assert_eq!(
470 NegativeToken::InvalidJwtHeaderShape.variant_name(),
471 "invalid_jwt_header_shape"
472 );
473 assert_eq!(NegativeToken::MissingAlg.variant_name(), "missing_alg");
474 assert_eq!(NegativeToken::AlgNone.variant_name(), "alg_none");
475 assert_eq!(NegativeToken::MissingKid.variant_name(), "missing_kid");
476 assert_eq!(
477 NegativeToken::MismatchedKid.variant_name(),
478 "mismatched_kid"
479 );
480 assert_eq!(
481 NegativeToken::ExpiredClaims.variant_name(),
482 "expired_claims"
483 );
484 assert_eq!(
485 NegativeToken::NotYetValidClaims.variant_name(),
486 "not_yet_valid_claims"
487 );
488 assert_eq!(NegativeToken::BadIssuer.variant_name(), "bad_issuer");
489 assert_eq!(NegativeToken::BadAudience.variant_name(), "bad_audience");
490 assert_eq!(
491 NegativeToken::MalformedBearer.variant_name(),
492 "malformed_bearer"
493 );
494 assert_eq!(
495 NegativeToken::NearMissApiKey.variant_name(),
496 "near_miss_api_key"
497 );
498 }
499
500 #[test]
501 fn negative_token_stable_ids_match_taxonomy() {
502 assert_eq!(
503 NegativeToken::MalformedJwtSegmentCount.stable_id(),
504 "jwt_bad_segment_count"
505 );
506 assert_eq!(
507 NegativeToken::BadBase64UrlSegment.stable_id(),
508 "jwt_malformed_base64url"
509 );
510 assert_eq!(
511 NegativeToken::InvalidJwtHeaderShape.stable_id(),
512 "jwt_invalid_header_shape"
513 );
514 assert_eq!(NegativeToken::MissingAlg.stable_id(), "jwt_missing_alg");
515 assert_eq!(NegativeToken::AlgNone.stable_id(), "jwt_alg_none");
516 assert_eq!(NegativeToken::MissingKid.stable_id(), "jwt_missing_kid");
517 assert_eq!(
518 NegativeToken::MismatchedKid.stable_id(),
519 "jwt_mismatched_kid"
520 );
521 assert_eq!(NegativeToken::ExpiredClaims.stable_id(), "jwt_expired");
522 assert_eq!(
523 NegativeToken::NotYetValidClaims.stable_id(),
524 "jwt_not_yet_valid"
525 );
526 assert_eq!(NegativeToken::BadIssuer.stable_id(), "jwt_bad_issuer");
527 assert_eq!(NegativeToken::BadAudience.stable_id(), "jwt_bad_audience");
528 assert_eq!(
529 NegativeToken::MalformedBearer.stable_id(),
530 "token_malformed_bearer"
531 );
532 assert_eq!(NegativeToken::NearMissApiKey.stable_id(), "token_near_miss");
533 }
534
535 #[test]
536 fn negative_api_key_near_miss_is_scanner_safe() {
537 let value = generate_negative_token(
538 "svc",
539 TokenKind::ApiKey,
540 Seed::new([19u8; 32]),
541 NegativeToken::NearMissApiKey,
542 );
543
544 assert!(value.starts_with(NEAR_MISS_API_KEY_PREFIX));
545 assert!(!value.starts_with(API_KEY_PREFIX));
546 assert_eq!(
547 value.len(),
548 NEAR_MISS_API_KEY_PREFIX.len() + API_KEY_RANDOM_LEN
549 );
550 }
551
552 #[test]
553 fn negative_malformed_bearer_is_not_base64url() {
554 let value = generate_negative_token(
555 "svc",
556 TokenKind::Bearer,
557 Seed::new([23u8; 32]),
558 NegativeToken::MalformedBearer,
559 );
560
561 assert_ne!(value, SCANNER_SAFE_INVALID_TOKEN_SEGMENT);
562 assert!(value.contains('!'));
563 assert_eq!(value.len(), 43);
564 assert!(URL_SAFE_NO_PAD.decode(value).is_err());
565 }
566
567 #[test]
568 fn negative_jwt_segment_count_keeps_two_decodable_segments() {
569 let value = generate_negative_token(
570 "svc",
571 TokenKind::OAuthAccessToken,
572 Seed::new([31u8; 32]),
573 NegativeToken::MalformedJwtSegmentCount,
574 );
575 let parts = jwt_parts(&value);
576
577 assert_eq!(parts.len(), 2);
578 assert_eq!(decode_object_segment(parts[0])["alg"], "RS256");
579 assert_eq!(decode_object_segment(parts[0])["typ"], "JWT");
580 assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
581 }
582
583 #[test]
584 fn negative_bad_base64url_replaces_payload_only() {
585 let value = generate_negative_token(
586 "svc",
587 TokenKind::OAuthAccessToken,
588 Seed::new([32u8; 32]),
589 NegativeToken::BadBase64UrlSegment,
590 );
591 let parts = jwt_parts(&value);
592
593 assert_eq!(parts.len(), 3);
594 assert_eq!(decode_object_segment(parts[0])["alg"], "RS256");
595 assert_eq!(parts[1], SCANNER_SAFE_INVALID_TOKEN_SEGMENT);
596 assert!(URL_SAFE_NO_PAD.decode(parts[1]).is_err());
597 assert!(!parts[2].is_empty());
598 }
599
600 #[test]
601 fn negative_invalid_header_shape_keeps_payload_and_signature() {
602 let value = generate_negative_token(
603 "svc",
604 TokenKind::OAuthAccessToken,
605 Seed::new([33u8; 32]),
606 NegativeToken::InvalidJwtHeaderShape,
607 );
608 let parts = jwt_parts(&value);
609
610 assert_eq!(parts.len(), 3);
611 assert_eq!(
612 decode_json_segment(parts[0]),
613 serde_json::json!(["not-a-header"])
614 );
615 assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
616 assert!(!parts[2].is_empty());
617 }
618
619 #[test]
620 fn negative_missing_alg_keeps_typ_and_claims() {
621 let value = generate_negative_token(
622 "svc",
623 TokenKind::OAuthAccessToken,
624 Seed::new([34u8; 32]),
625 NegativeToken::MissingAlg,
626 );
627 let parts = jwt_parts(&value);
628 let header = decode_object_segment(parts[0]);
629
630 assert_eq!(parts.len(), 3);
631 assert!(!header.contains_key("alg"));
632 assert_eq!(header["typ"], "JWT");
633 assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
634 }
635
636 #[test]
637 fn negative_alg_none_changes_alg_only() {
638 let value = generate_negative_token(
639 "svc",
640 TokenKind::OAuthAccessToken,
641 Seed::new([35u8; 32]),
642 NegativeToken::AlgNone,
643 );
644 let parts = jwt_parts(&value);
645 let header = decode_object_segment(parts[0]);
646
647 assert_eq!(parts.len(), 3);
648 assert_eq!(header["alg"], "none");
649 assert_eq!(header["typ"], "JWT");
650 assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
651 }
652
653 #[test]
654 fn negative_mismatched_kid_keeps_header_and_payload_context() {
655 let value = generate_negative_token(
656 "svc",
657 TokenKind::OAuthAccessToken,
658 Seed::new([36u8; 32]),
659 NegativeToken::MismatchedKid,
660 );
661 let parts = jwt_parts(&value);
662 let header = decode_object_segment(parts[0]);
663 let payload = decode_object_segment(parts[1]);
664
665 assert_eq!(parts.len(), 3);
666 assert_eq!(header["alg"], "RS256");
667 assert_eq!(header["typ"], "JWT");
668 assert_eq!(header["kid"], "unknown-kid");
669 assert_eq!(payload["sub"], "svc");
670 assert_eq!(payload["kid"], "expected-kid");
671 assert_ne!(header["kid"], payload["kid"]);
672 }
673
674 #[test]
675 fn negative_missing_kid_keeps_payload_key_context() {
676 let value = generate_negative_token(
677 "svc",
678 TokenKind::OAuthAccessToken,
679 Seed::new([41u8; 32]),
680 NegativeToken::MissingKid,
681 );
682 let parts = jwt_parts(&value);
683 let header = decode_object_segment(parts[0]);
684 let payload = decode_object_segment(parts[1]);
685
686 assert_eq!(parts.len(), 3);
687 assert_eq!(header["alg"], "RS256");
688 assert_eq!(header["typ"], "JWT");
689 assert!(header.get("kid").is_none());
690 assert_eq!(payload["sub"], "svc");
691 assert_eq!(payload["kid"], "expected-kid");
692 }
693
694 #[test]
695 fn negative_not_yet_valid_keeps_future_window_and_subject() {
696 let value = generate_negative_token(
697 "svc",
698 TokenKind::OAuthAccessToken,
699 Seed::new([37u8; 32]),
700 NegativeToken::NotYetValidClaims,
701 );
702 let parts = jwt_parts(&value);
703 let header = decode_object_segment(parts[0]);
704 let payload = decode_object_segment(parts[1]);
705
706 assert_eq!(parts.len(), 3);
707 assert_eq!(header["alg"], "RS256");
708 assert_eq!(payload["sub"], "svc");
709 assert_eq!(payload["nbf"], 4_000_000_000u64);
710 assert_eq!(payload["exp"], 4_100_000_000u64);
711 }
712
713 #[test]
714 fn negative_expired_claims_only_replaces_expiration() {
715 let seed = Seed::new([38u8; 32]);
716 let original = generate_oauth_access_token("svc", seed);
717 let value = generate_negative_token(
718 "svc",
719 TokenKind::OAuthAccessToken,
720 seed,
721 NegativeToken::ExpiredClaims,
722 );
723 let original_parts = jwt_parts(&original);
724 let parts = jwt_parts(&value);
725 let payload = decode_object_segment(parts[1]);
726
727 assert_eq!(parts.len(), 3);
728 assert_eq!(parts[0], original_parts[0]);
729 assert_eq!(parts[2], original_parts[2]);
730 assert_eq!(payload["iss"], "uselesskey");
731 assert_eq!(payload["sub"], "svc");
732 assert_eq!(payload["aud"], "tests");
733 assert_eq!(payload["exp"], 1u64);
734 }
735
736 #[test]
737 fn negative_bad_issuer_and_audience_preserve_other_claims() {
738 let seed = Seed::new([39u8; 32]);
739 let issuer = generate_negative_token(
740 "svc",
741 TokenKind::OAuthAccessToken,
742 seed,
743 NegativeToken::BadIssuer,
744 );
745 let audience = generate_negative_token(
746 "svc",
747 TokenKind::OAuthAccessToken,
748 seed,
749 NegativeToken::BadAudience,
750 );
751 let issuer_payload = decode_object_segment(jwt_parts(&issuer)[1]);
752 let audience_payload = decode_object_segment(jwt_parts(&audience)[1]);
753
754 assert_eq!(issuer_payload["iss"], "wrong-issuer");
755 assert_eq!(issuer_payload["aud"], "tests");
756 assert_eq!(issuer_payload["sub"], "svc");
757 assert_eq!(audience_payload["iss"], "uselesskey");
758 assert_eq!(audience_payload["aud"], "wrong-audience");
759 assert_eq!(audience_payload["sub"], "svc");
760 }
761
762 #[test]
763 fn near_miss_api_key_uses_same_suffix_for_all_kinds() {
764 let seed = Seed::new([40u8; 32]);
765 let api = generate_negative_token(
766 "svc",
767 TokenKind::ApiKey,
768 seed,
769 NegativeToken::NearMissApiKey,
770 );
771 let bearer = generate_negative_token(
772 "svc",
773 TokenKind::Bearer,
774 seed,
775 NegativeToken::NearMissApiKey,
776 );
777 let oauth = generate_negative_token(
778 "svc",
779 TokenKind::OAuthAccessToken,
780 seed,
781 NegativeToken::NearMissApiKey,
782 );
783
784 assert_eq!(api, bearer);
785 assert_eq!(api, oauth);
786 assert_eq!(
787 api.strip_prefix(NEAR_MISS_API_KEY_PREFIX),
788 generate_api_key(seed).strip_prefix(API_KEY_PREFIX)
789 );
790 }
791
792 fn jwt_parts(value: &str) -> Vec<&str> {
793 value.split('.').collect()
794 }
795
796 fn decode_object_segment(segment: &str) -> serde_json::Map<String, serde_json::Value> {
797 decode_json_segment(segment)
798 .as_object()
799 .expect("JWT segment should decode to an object")
800 .clone()
801 }
802
803 fn decode_json_segment(segment: &str) -> serde_json::Value {
804 let bytes = URL_SAFE_NO_PAD.decode(segment).expect("decode JWT segment");
805 serde_json::from_slice(&bytes).expect("parse JWT segment JSON")
806 }
807
808 #[test]
809 fn random_base62_length_and_charset() {
810 let value = random_base62(Seed::new([17u8; 32]), 64);
811 assert_eq!(value.len(), 64);
812 assert!(value.chars().all(|c| c.is_ascii_alphanumeric()));
813 }
814
815 proptest! {
816 #[test]
817 fn api_key_same_seed_stable(seed in any::<[u8; 32]>()) {
818 let a = generate_api_key(Seed::new(seed));
819 let b = generate_api_key(Seed::new(seed));
820 prop_assert_eq!(a, b);
821 }
822
823 #[test]
824 fn bearer_token_always_43_chars(seed in any::<[u8; 32]>()) {
825 let token = generate_bearer_token(Seed::new(seed));
826 prop_assert_eq!(token.len(), 43);
827 }
828
829 #[test]
830 fn oauth_has_three_segments(seed in any::<[u8; 32]>(), label in "[a-z0-9_-]{1,16}") {
831 let token = generate_oauth_access_token(&label, Seed::new(seed));
832 prop_assert_eq!(token.matches('.').count(), 2);
833 }
834 }
835}