pub struct StaticTokens { /* private fields */ }Expand description
A set of static API keys, each with an optional label: several keys accepted at once, for rotating a key with no downtime (the old and the new one both accepted until every client has moved) or for one key per client.
Opaque: nothing reads a secret back out of it. Build it with
StaticTokens::single or StaticTokens::new and
StaticTokens::with; hand it to authenticate_with_static_tokens, or
to a layer’s static_tokens builder method (features axum/tower). The
env feature’s static_tokens_from_env loads a current and a next key.
§Rules
Checked by StaticTokens::with, which refuses (never silently fixes) an
entry that breaks one:
- A secret must not be empty or whitespace-only
(
StaticTokensError::BlankSecret). It is otherwise kept verbatim, untrimmed, like the singlestatic_tokeneverywhere else. - A secret must not repeat one already in the set
(
StaticTokensError::DuplicateSecret): two entries holding one secret would make the reported label depend on insertion order, and for per-client keys means two clients share a key, so it is refused rather than deduplicated. - A label is 1 to
StaticTokens::MAX_LABEL_LEN(64) visible ASCII characters,!through~— no space, no control character, nothing else (StaticTokensError::InvalidLabel) — so it is always safe to put in a log line or a metric. Labels are not secrets. - A label must not repeat one already in the set
(
StaticTokensError::DuplicateLabel); any number of entries may be unlabeled.
§Security
Comparison is constant-time across entries: every candidate is compared
with every entry, with no early exit once one matches, and the matching
index is selected without a data-dependent branch (subtle). What is not
hidden is length: each single comparison returns faster when the
candidate’s length differs from that entry’s, exactly as the single-token
comparison always has, so the total time depends on how many entries share
the candidate’s length (never on which entry matched, apart from cloning
the matched label after the comparison, observable only by a holder of a
valid key). Use high-entropy keys of one fixed length, and the length
reveals nothing useful.
Debug prints the number of entries and their labels, never a secret. No
PartialEq: comparing two sets would compare secrets in variable time.
Each secret is held in a zeroize::Zeroizing buffer, wiped when the
set (or a clone of it) is dropped; so are the layer builders’ single
static_token, and the env loaders’ intermediate copies (untrimmed
values, a next key equal to the current one). What is not wiped: the
strings a caller passes in or keeps (with’s argument before it is
moved in, secret_from_env’s returned String), a
StaticTokenDecision’s String payload,
the process environment, and a secrets file. A String’s earlier
buffers, left behind if it was grown before being handed over, are not
wiped either.
§Examples
use oauth_resource_server::{StaticTokens, StaticTokensError};
// Rotation: the current key and the one replacing it.
let tokens = StaticTokens::new()
.with(Some("current"), "example-key-2024")?
.with(Some("next"), "example-key-2025")?;
assert_eq!(tokens.len(), 2);
assert!(!format!("{tokens:?}").contains("example-key"));
// Blank secrets and repeated secrets are refused.
assert!(matches!(
StaticTokens::single(" "),
Err(StaticTokensError::BlankSecret { .. })
));
assert!(matches!(
tokens.with(None, "example-key-2025"),
Err(StaticTokensError::DuplicateSecret { .. })
));Implementations§
Source§impl StaticTokens
impl StaticTokens
Sourcepub const MAX_LABEL_LEN: usize = 64
pub const MAX_LABEL_LEN: usize = 64
The longest label StaticTokens::with accepts, in bytes (every
accepted character is one byte).
Sourcepub fn new() -> Self
pub fn new() -> Self
An empty set. Add entries with StaticTokens::with. An empty set
accepts nothing and, handed to a layer, does not count as a credential
mechanism.
Sourcepub fn single(secret: impl Into<String>) -> Result<Self, StaticTokensError>
pub fn single(secret: impl Into<String>) -> Result<Self, StaticTokensError>
A set holding one unlabeled secret: the same credential a single
static_token configures.
§Errors
StaticTokensError::BlankSecret for an empty or whitespace-only
secret.
Sourcepub fn with(
self,
label: Option<&str>,
secret: impl Into<String>,
) -> Result<Self, StaticTokensError>
pub fn with( self, label: Option<&str>, secret: impl Into<String>, ) -> Result<Self, StaticTokensError>
This set plus one more secret, optionally labeled (see the rules).
§Errors
StaticTokensError::BlankSecret, StaticTokensError::InvalidLabel,
StaticTokensError::DuplicateLabel or
StaticTokensError::DuplicateSecret, checked in that order. The set
is consumed either way; on an error its secrets are wiped with it.
Trait Implementations§
Source§impl Clone for StaticTokens
impl Clone for StaticTokens
Source§impl Debug for StaticTokens
Hand-written so no secret ever reaches a log line through {:?}: the
count and the labels (log-safe by construction), never a secret.
impl Debug for StaticTokens
Hand-written so no secret ever reaches a log line through {:?}: the
count and the labels (log-safe by construction), never a secret.