use std::fmt;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::client::ClientId;
use crate::scope::ScopeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TokenType {
#[serde(rename = "Bearer")]
Bearer,
#[cfg(feature = "dpop")]
#[serde(rename = "DPoP")]
Dpop,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TokenResponse {
pub access_token: String,
pub token_type: TokenType,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[cfg(feature = "rar")]
#[serde(
default,
skip_serializing_if = "crate::rar::AuthorizationDetails::is_empty"
)]
pub authorization_details: crate::rar::AuthorizationDetails,
}
impl fmt::Debug for TokenResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
value.as_ref().map(|_| "[redacted]")
}
f.debug_struct("TokenResponse")
.field("access_token", &"[redacted]")
.field("token_type", &self.token_type)
.field("expires_in", &self.expires_in)
.field("refresh_token", &redact_opt(&self.refresh_token))
.field("scope", &self.scope)
.finish()
}
}
#[cfg(any(feature = "dpop", feature = "mtls"))]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "ConfirmationWire")]
#[non_exhaustive]
pub struct Confirmation {
#[cfg(feature = "dpop")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jkt: Option<String>,
#[cfg(feature = "mtls")]
#[serde(rename = "x5t#S256", default, skip_serializing_if = "Option::is_none")]
pub x5t_s256: Option<crate::mtls::CertificateThumbprint>,
}
#[cfg(any(feature = "dpop", feature = "mtls"))]
impl Confirmation {
#[cfg(feature = "dpop")]
pub fn jkt(jkt: impl Into<String>) -> Self {
Confirmation {
jkt: Some(jkt.into()),
#[cfg(feature = "mtls")]
x5t_s256: None,
}
}
pub fn is_empty(&self) -> bool {
#[cfg(feature = "dpop")]
if self.jkt.is_some() {
return false;
}
#[cfg(feature = "mtls")]
if self.x5t_s256.is_some() {
return false;
}
true
}
}
#[cfg(any(feature = "dpop", feature = "mtls"))]
#[derive(Deserialize)]
struct ConfirmationWire {
#[cfg(feature = "dpop")]
jkt: Option<String>,
#[cfg(feature = "mtls")]
#[serde(rename = "x5t#S256")]
x5t_s256: Option<crate::mtls::CertificateThumbprint>,
#[cfg(not(feature = "dpop"))]
jkt: Option<serde::de::IgnoredAny>,
#[cfg(not(feature = "mtls"))]
#[serde(rename = "x5t#S256")]
x5t_s256: Option<serde::de::IgnoredAny>,
}
#[cfg(any(feature = "dpop", feature = "mtls"))]
impl TryFrom<ConfirmationWire> for Confirmation {
type Error = UnrepresentableMember;
fn try_from(wire: ConfirmationWire) -> Result<Self, Self::Error> {
#[cfg(not(feature = "dpop"))]
if wire.jkt.is_some() {
return Err(
"confirmation carries `jkt` (RFC 9449 s6.1), so the token is bound to a DPoP key \
and this build of oauth-as cannot represent that: rebuild with the `dpop` feature",
);
}
#[cfg(not(feature = "mtls"))]
if wire.x5t_s256.is_some() {
return Err(
"confirmation carries `x5t#S256` (RFC 8705 s3.1), so the token is bound to a \
client certificate and this build of oauth-as cannot represent that: rebuild \
with the `mtls` feature",
);
}
Ok(Confirmation {
#[cfg(feature = "dpop")]
jkt: wire.jkt,
#[cfg(feature = "mtls")]
x5t_s256: wire.x5t_s256,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenTypeHint {
AccessToken,
RefreshToken,
}
impl std::str::FromStr for TokenTypeHint {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"access_token" => Ok(TokenTypeHint::AccessToken),
"refresh_token" => Ok(TokenTypeHint::RefreshToken),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "IntrospectionWire")]
#[non_exhaustive]
pub struct IntrospectionResponse {
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<TokenType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<Vec<String>>,
#[cfg(feature = "consent")]
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_time: Option<u64>,
#[cfg(feature = "consent")]
#[serde(skip_serializing_if = "Option::is_none")]
pub acr: Option<String>,
#[cfg(feature = "rar")]
#[serde(
default,
skip_serializing_if = "crate::rar::AuthorizationDetails::is_empty"
)]
pub authorization_details: crate::rar::AuthorizationDetails,
#[cfg(any(feature = "dpop", feature = "mtls"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub cnf: Option<Confirmation>,
#[cfg(feature = "token-exchange")]
#[serde(skip_serializing_if = "Option::is_none")]
pub act: Option<crate::token_exchange::ActClaim>,
}
impl IntrospectionResponse {
pub fn inactive() -> Self {
IntrospectionResponse {
active: false,
scope: None,
client_id: None,
sub: None,
token_type: None,
exp: None,
iat: None,
iss: None,
aud: None,
#[cfg(feature = "consent")]
auth_time: None,
#[cfg(feature = "consent")]
acr: None,
#[cfg(feature = "rar")]
authorization_details: crate::rar::AuthorizationDetails::none(),
#[cfg(any(feature = "dpop", feature = "mtls"))]
cnf: None,
#[cfg(feature = "token-exchange")]
act: None,
}
}
}
#[derive(Deserialize)]
struct IntrospectionWire {
active: bool,
scope: Option<String>,
client_id: Option<String>,
sub: Option<String>,
token_type: Option<TokenType>,
exp: Option<u64>,
iat: Option<u64>,
iss: Option<String>,
aud: Option<Vec<String>>,
#[cfg(feature = "consent")]
auth_time: Option<u64>,
#[cfg(feature = "consent")]
acr: Option<String>,
#[cfg(feature = "rar")]
#[serde(default)]
authorization_details: crate::rar::AuthorizationDetails,
#[cfg(any(feature = "dpop", feature = "mtls"))]
cnf: Option<Confirmation>,
#[cfg(feature = "token-exchange")]
act: Option<crate::token_exchange::ActClaim>,
#[cfg(not(feature = "consent"))]
auth_time: Option<serde::de::IgnoredAny>,
#[cfg(not(feature = "consent"))]
acr: Option<serde::de::IgnoredAny>,
#[cfg(not(feature = "rar"))]
authorization_details: Option<serde::de::IgnoredAny>,
#[cfg(not(any(feature = "dpop", feature = "mtls")))]
cnf: Option<serde::de::IgnoredAny>,
#[cfg(not(feature = "token-exchange"))]
act: Option<serde::de::IgnoredAny>,
}
type UnrepresentableMember = &'static str;
impl TryFrom<IntrospectionWire> for IntrospectionResponse {
type Error = UnrepresentableMember;
fn try_from(wire: IntrospectionWire) -> Result<Self, Self::Error> {
#[cfg(not(feature = "consent"))]
if wire.auth_time.is_some() {
return Err(
"introspection response carries `auth_time` (RFC 9470 s6.2), which this build of \
oauth-as cannot represent: rebuild with the `consent` feature",
);
}
#[cfg(not(feature = "consent"))]
if wire.acr.is_some() {
return Err(
"introspection response carries `acr` (RFC 9470 s6.2), which this build of \
oauth-as cannot represent: rebuild with the `consent` feature",
);
}
#[cfg(not(feature = "rar"))]
if wire.authorization_details.is_some() {
return Err(
"introspection response carries `authorization_details` (RFC 9396 s9.2), which \
this build of oauth-as cannot represent: rebuild with the `rar` feature",
);
}
#[cfg(not(any(feature = "dpop", feature = "mtls")))]
if wire.cnf.is_some() {
return Err(
"introspection response carries `cnf` (RFC 9449 s6.1 / RFC 8705 s3.2), so the \
token is sender constrained and this build of oauth-as cannot represent that: \
rebuild with the `dpop` or `mtls` feature",
);
}
#[cfg(not(feature = "token-exchange"))]
if wire.act.is_some() {
return Err(
"introspection response carries `act` (RFC 8693 s4.1), so the token is a \
delegation and this build of oauth-as cannot represent that: rebuild with the \
`token-exchange` feature",
);
}
Ok(IntrospectionResponse {
active: wire.active,
scope: wire.scope,
client_id: wire.client_id,
sub: wire.sub,
token_type: wire.token_type,
exp: wire.exp,
iat: wire.iat,
iss: wire.iss,
aud: wire.aud,
#[cfg(feature = "consent")]
auth_time: wire.auth_time,
#[cfg(feature = "consent")]
acr: wire.acr,
#[cfg(feature = "rar")]
authorization_details: wire.authorization_details,
#[cfg(any(feature = "dpop", feature = "mtls"))]
cnf: wire.cnf,
#[cfg(feature = "token-exchange")]
act: wire.act,
})
}
}
fn grant_established_at_default() -> SystemTime {
SystemTime::UNIX_EPOCH
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IssuedToken {
pub access_token: String,
pub client_id: ClientId,
pub subject: Option<String>,
pub scope: ScopeSet,
pub resource: Vec<String>,
#[cfg(feature = "rar")]
#[serde(default)]
pub authorization_details: crate::rar::AuthorizationDetails,
pub issued_at: SystemTime,
#[serde(default = "grant_established_at_default")]
pub grant_established_at: SystemTime,
pub expires_at: SystemTime,
#[cfg(feature = "dpop")]
pub jkt: Option<Box<str>>,
#[cfg(feature = "mtls")]
pub x5t_s256: Option<Box<crate::mtls::CertificateThumbprint>>,
pub family_id: Option<String>,
#[cfg(feature = "token-exchange")]
#[cfg_attr(docsrs, doc(cfg(feature = "token-exchange")))]
pub act: Option<Box<crate::token_exchange::ActClaim>>,
#[cfg(feature = "consent")]
pub authentication: Option<Box<crate::consent::Authentication>>,
}
impl IssuedToken {
pub fn new(
access_token: impl Into<String>,
client_id: ClientId,
subject: Option<String>,
scope: ScopeSet,
issued_at: SystemTime,
expires_at: SystemTime,
) -> Self {
IssuedToken {
grant_established_at: SystemTime::UNIX_EPOCH,
access_token: access_token.into(),
client_id,
subject,
scope,
resource: Vec::new(),
#[cfg(feature = "rar")]
authorization_details: crate::rar::AuthorizationDetails::none(),
issued_at,
expires_at,
#[cfg(feature = "dpop")]
jkt: None,
#[cfg(feature = "mtls")]
x5t_s256: None,
family_id: None,
#[cfg(feature = "token-exchange")]
act: None,
#[cfg(feature = "consent")]
authentication: None,
}
}
}
impl fmt::Debug for IssuedToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = f.debug_struct("IssuedToken");
out.field("access_token", &"[redacted]")
.field("client_id", &self.client_id)
.field("subject", &self.subject)
.field("scope", &self.scope)
.field("resource", &self.resource);
#[cfg(feature = "rar")]
out.field("authorization_details", &self.authorization_details);
out.field("issued_at", &self.issued_at)
.field("grant_established_at", &self.grant_established_at)
.field("expires_at", &self.expires_at);
#[cfg(feature = "dpop")]
out.field("jkt", &self.jkt);
#[cfg(feature = "mtls")]
out.field("x5t_s256", &self.x5t_s256);
out.field("family_id", &self.family_id);
#[cfg(feature = "token-exchange")]
out.field("act", &self.act);
#[cfg(feature = "consent")]
out.field("authentication", &self.authentication);
out.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RefreshTokenState {
Active,
Spent,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RefreshTokenRecord {
pub refresh_token: String,
pub client_id: ClientId,
pub subject: Option<String>,
pub scope: ScopeSet,
pub resource: Vec<String>,
#[cfg(feature = "rar")]
#[serde(default)]
pub authorization_details: crate::rar::AuthorizationDetails,
#[serde(default = "grant_established_at_default")]
pub grant_established_at: SystemTime,
pub expires_at: Option<SystemTime>,
#[cfg(feature = "dpop")]
pub jkt: Option<Box<str>>,
#[cfg(feature = "mtls")]
pub x5t_s256: Option<Box<crate::mtls::CertificateThumbprint>>,
pub family_id: String,
pub state: RefreshTokenState,
#[cfg(feature = "consent")]
pub authentication: Option<Box<crate::consent::Authentication>>,
}
impl RefreshTokenRecord {
pub fn new(
refresh_token: impl Into<String>,
client_id: ClientId,
subject: Option<String>,
scope: ScopeSet,
family_id: impl Into<String>,
) -> Self {
RefreshTokenRecord {
refresh_token: refresh_token.into(),
client_id,
subject,
scope,
resource: Vec::new(),
#[cfg(feature = "rar")]
authorization_details: crate::rar::AuthorizationDetails::none(),
grant_established_at: SystemTime::UNIX_EPOCH,
expires_at: None,
#[cfg(feature = "dpop")]
jkt: None,
#[cfg(feature = "mtls")]
x5t_s256: None,
family_id: family_id.into(),
state: RefreshTokenState::Active,
#[cfg(feature = "consent")]
authentication: None,
}
}
}
impl fmt::Debug for RefreshTokenRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = f.debug_struct("RefreshTokenRecord");
out.field("refresh_token", &"[redacted]")
.field("client_id", &self.client_id)
.field("subject", &self.subject)
.field("scope", &self.scope)
.field("resource", &self.resource);
#[cfg(feature = "rar")]
out.field("authorization_details", &self.authorization_details);
out.field("grant_established_at", &self.grant_established_at)
.field("expires_at", &self.expires_at);
#[cfg(feature = "dpop")]
out.field("jkt", &self.jkt);
#[cfg(feature = "mtls")]
out.field("x5t_s256", &self.x5t_s256);
out.field("family_id", &self.family_id)
.field("state", &self.state);
#[cfg(feature = "consent")]
out.field("authentication", &self.authentication);
out.finish()
}
}
#[cfg(test)]
#[path = "tests/token.rs"]
mod tests;