use http::{HeaderValue, header::InvalidHeaderValue};
use crate::core::{
platform::{Duration, SystemTime},
secrets::SecretString,
};
fn effective_expiry(
received_at: SystemTime,
expires_in: Duration,
expires_margin: Duration,
) -> SystemTime {
const CENTURY: Duration = Duration::from_hours(100 * 365 * 24);
received_at
.checked_add(expires_in)
.or_else(|| received_at.checked_add(CENTURY))
.unwrap_or(received_at)
.checked_sub(expires_margin)
.unwrap_or(SystemTime::UNIX_EPOCH)
}
#[derive(Debug, Clone)]
pub enum AccessToken {
DPoP(DPoPAccessToken),
Bearer(BearerAccessToken),
NotAccessToken(NonAccessToken),
}
impl AccessToken {
#[must_use]
pub fn token(&self) -> &SecretString {
match self {
AccessToken::DPoP(token) => &token.token,
AccessToken::Bearer(token) => &token.token,
AccessToken::NotAccessToken(token) => &token.token,
}
}
pub fn expose_header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
match self {
AccessToken::DPoP(dpop_access_token) => dpop_access_token.expose_header_value(),
AccessToken::Bearer(bearer_access_token) => bearer_access_token.expose_header_value(),
AccessToken::NotAccessToken(_) => HeaderValue::from_str("\n")
.map(|_| unreachable!("a control character is never a valid header value")),
}
}
#[must_use]
pub fn dpop_jkt(&self) -> Option<&str> {
match self {
AccessToken::DPoP(token) => Some(token.jkt.as_str()),
AccessToken::Bearer(_) | AccessToken::NotAccessToken(_) => None,
}
}
#[must_use]
pub fn token_type(&self) -> &str {
match self {
AccessToken::DPoP(_) => "DPoP",
AccessToken::Bearer(_) => "Bearer",
AccessToken::NotAccessToken(_) => "N_A",
}
}
#[must_use]
pub fn effective_expiry(
&self,
default_expires_in: Duration,
expires_margin: Duration,
) -> SystemTime {
match self {
AccessToken::DPoP(dpop_access_token) => {
dpop_access_token.effective_expiry(default_expires_in, expires_margin)
}
AccessToken::Bearer(bearer_access_token) => {
bearer_access_token.effective_expiry(default_expires_in, expires_margin)
}
AccessToken::NotAccessToken(token) => {
token.effective_expiry(default_expires_in, expires_margin)
}
}
}
#[must_use]
pub fn is_expired(&self, default_expires_in: Duration, expires_margin: Duration) -> bool {
SystemTime::now() >= self.effective_expiry(default_expires_in, expires_margin)
}
#[must_use]
pub fn effective_lifetime(&self, default_expires_in: Duration) -> Duration {
match self {
AccessToken::DPoP(dpop_access_token) => {
dpop_access_token.effective_lifetime(default_expires_in)
}
AccessToken::Bearer(bearer_access_token) => {
bearer_access_token.effective_lifetime(default_expires_in)
}
AccessToken::NotAccessToken(token) => token.effective_lifetime(default_expires_in),
}
}
}
#[derive(Debug, Clone)]
pub struct NonAccessToken {
token: SecretString,
received_at: SystemTime,
expires_in: Option<Duration>,
}
impl NonAccessToken {
#[must_use]
pub fn new(token: SecretString, received_at: SystemTime, expires_in: Option<Duration>) -> Self {
Self {
token,
received_at,
expires_in,
}
}
#[must_use]
pub fn expose_token(&self) -> &str {
self.token.expose_secret()
}
#[must_use]
pub fn effective_expiry(
&self,
default_expires_in: Duration,
expires_margin: Duration,
) -> SystemTime {
let expires_in = self.expires_in.unwrap_or(default_expires_in);
effective_expiry(self.received_at, expires_in, expires_margin)
}
#[must_use]
pub fn is_expired(&self, default_expires_in: Duration, expires_margin: Duration) -> bool {
SystemTime::now() >= self.effective_expiry(default_expires_in, expires_margin)
}
#[must_use]
pub fn effective_lifetime(&self, default_expires_in: Duration) -> Duration {
self.expires_in.unwrap_or(default_expires_in)
}
}
#[derive(Debug, Clone)]
pub struct DPoPAccessToken {
token: SecretString,
jkt: String,
received_at: SystemTime,
expires_in: Option<Duration>,
}
impl DPoPAccessToken {
#[must_use]
pub fn new(
token: SecretString,
jkt: String,
received_at: SystemTime,
expires_in: Option<Duration>,
) -> Self {
Self {
token,
jkt,
received_at,
expires_in,
}
}
#[must_use]
pub fn jkt(&self) -> &str {
&self.jkt
}
#[must_use]
pub fn token(&self) -> &SecretString {
&self.token
}
pub fn expose_header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
HeaderValue::from_str(&format!("DPoP {}", self.token.expose_secret()))
}
#[must_use]
pub fn effective_expiry(
&self,
default_expires_in: Duration,
expires_margin: Duration,
) -> SystemTime {
let expires_in = self.expires_in.unwrap_or(default_expires_in);
effective_expiry(self.received_at, expires_in, expires_margin)
}
#[must_use]
pub fn is_expired(&self, default_expires_in: Duration, expires_margin: Duration) -> bool {
SystemTime::now() >= self.effective_expiry(default_expires_in, expires_margin)
}
#[must_use]
pub fn effective_lifetime(&self, default_expires_in: Duration) -> Duration {
self.expires_in.unwrap_or(default_expires_in)
}
}
#[derive(Debug, Clone)]
pub struct BearerAccessToken {
token: SecretString,
received_at: SystemTime,
expires_in: Option<Duration>,
}
impl BearerAccessToken {
#[must_use]
pub fn new(token: SecretString, received_at: SystemTime, expires_in: Option<Duration>) -> Self {
Self {
token,
received_at,
expires_in,
}
}
#[must_use]
pub fn expose_token(&self) -> &str {
self.token.expose_secret()
}
pub fn expose_header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
HeaderValue::from_str(&format!("Bearer {}", self.token.expose_secret()))
}
#[must_use]
pub fn effective_expiry(
&self,
default_expires_in: Duration,
expires_margin: Duration,
) -> SystemTime {
let expires_in = self.expires_in.unwrap_or(default_expires_in);
effective_expiry(self.received_at, expires_in, expires_margin)
}
#[must_use]
pub fn is_expired(&self, default_expires_in: Duration, expires_margin: Duration) -> bool {
SystemTime::now() >= self.effective_expiry(default_expires_in, expires_margin)
}
#[must_use]
pub fn effective_lifetime(&self, default_expires_in: Duration) -> Duration {
self.expires_in.unwrap_or(default_expires_in)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn huge_expires_in_does_not_panic() {
let token = BearerAccessToken::new(
SecretString::new("tok"),
SystemTime::now(),
Some(Duration::from_secs(u64::MAX)),
);
assert!(!token.is_expired(Duration::from_hours(1), Duration::from_secs(30)));
}
#[test]
fn huge_margin_does_not_panic() {
let token = BearerAccessToken::new(
SecretString::new("tok"),
SystemTime::now(),
Some(Duration::from_hours(1)),
);
assert!(token.is_expired(Duration::from_hours(1), Duration::from_secs(u64::MAX)));
}
#[test]
fn normal_expiry_unchanged() {
let received_at = SystemTime::now();
let token = BearerAccessToken::new(
SecretString::new("tok"),
received_at,
Some(Duration::from_hours(1)),
);
assert_eq!(
token.effective_expiry(Duration::from_hours(1), Duration::from_secs(30)),
received_at + Duration::from_secs(3600 - 30)
);
assert!(!token.is_expired(Duration::from_hours(1), Duration::from_secs(30)));
}
}