use std::fmt;
use std::time::{Duration, SystemTime};
use crate::authorization::{
AuthorizationCodeRecord, AuthorizationCodeState, AuthorizationError,
AuthorizationErrorRedirect, AuthorizationRequest, AuthorizationResponse, CodeChallengeMethod,
ValidatedAuthorizationRequest,
};
use crate::client::{Client, ClientId};
#[cfg(feature = "client-assertion")]
use crate::client_assertion::{verify_assertion, CLIENT_ASSERTION_TYPE};
use crate::device::{
normalize_user_code, DeviceAuthorizationResponse, DeviceGrant, DeviceGrantState,
};
#[cfg(feature = "dpop")]
use crate::dpop::verify_proof;
use crate::error::{ErrorCode, ErrorResponse};
use crate::events::{
Attempt, AttemptOutcome, ClientAuthFailure, Event, EventSink, Hooks, RateLimitDecision,
RateLimiter,
};
use crate::grant::GrantType;
use crate::hex::encode as hex_encode;
#[cfg(feature = "jwt")]
use crate::jwt::{AccessTokenClaims, AccessTokenFormat, Jwks};
use crate::scope::ScopeSet;
use crate::store::{Storage, StorageError};
use crate::token::{
IntrospectionResponse, IssuedToken, RefreshTokenRecord, RefreshTokenState, TokenResponse,
TokenType, TokenTypeHint,
};
pub(crate) fn saturating_deadline(base: SystemTime, span: std::time::Duration) -> SystemTime {
if let Some(exact) = base.checked_add(span) {
return exact;
}
let mut out = base;
let mut span = span;
while span > std::time::Duration::from_secs(1) {
span /= 2;
if let Some(next) = out.checked_add(span) {
out = next;
}
}
out
}
pub(crate) fn unix_seconds(t: SystemTime) -> Option<u64> {
t.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}
pub trait Clock: Send + Sync {
fn now(&self) -> SystemTime;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServerConfig {
pub issuer: String,
pub verification_uri: String,
pub authorization_endpoint: Option<String>,
pub token_endpoint: Option<String>,
pub device_authorization_endpoint: Option<String>,
pub introspection_endpoint: Option<String>,
pub revocation_endpoint: Option<String>,
pub jwks_uri: Option<String>,
pub registration: Option<Box<crate::registration::RegistrationConfig>>,
#[cfg(feature = "par")]
pub par: Option<Box<crate::par::ParConfig>>,
#[cfg(feature = "jar")]
pub jar: Option<Box<crate::par::JarConfig>>,
#[cfg(feature = "cimd")]
pub cimd: Option<Box<crate::cimd::CimdPolicy>>,
pub scopes_supported: Option<Vec<String>>,
pub allowed_resources: Option<Box<[Box<str>]>>,
pub resource_servers: Option<Box<[ResourceServerRegistration]>>,
pub service_documentation: Option<String>,
#[cfg(feature = "rar")]
pub authorization_details_types_supported: Option<Vec<String>>,
#[cfg(feature = "resource-metadata")]
pub protected_resources: Option<Vec<String>>,
#[cfg(feature = "jwt")]
pub access_token_format: AccessTokenFormat,
pub authorization_code_ttl: Duration,
pub include_verification_uri_complete: bool,
pub device_code_ttl: Duration,
pub poll_interval: Duration,
pub slow_down_increment: Duration,
pub access_token_ttl: Duration,
pub issue_refresh_tokens: bool,
pub allow_sender_constrained_exchange: bool,
pub allow_authorization_details_exchange: bool,
pub refresh_token_ttl: Option<Duration>,
pub refresh_reuse_window: Duration,
#[cfg(feature = "dpop")]
pub require_dpop: bool,
pub user_code_length: usize,
}
pub const MIN_USER_CODE_LENGTH: usize = 8;
pub const MAX_RESOURCE_INDICATORS: usize = 16;
enum IntrospectionView {
OwningClient,
ResourceServer(Vec<String>),
}
#[cfg(feature = "rar")]
fn details_for_resource_server(
details: &crate::rar::AuthorizationDetails,
mine: &[String],
) -> crate::rar::AuthorizationDetails {
crate::rar::AuthorizationDetails::from_elements(
details
.iter()
.filter_map(|detail| {
if detail.locations.is_empty() {
return Some(detail.clone());
}
let locations: Vec<Box<str>> = detail
.locations
.iter()
.filter(|at| mine.iter().any(|id| id.as_str() == &***at))
.cloned()
.collect();
(!locations.is_empty()).then(|| crate::rar::AuthorizationDetail {
locations: locations.into_boxed_slice(),
..detail.clone()
})
})
.collect(),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResourceServerRegistration {
pub client_id: ClientId,
pub resources: Vec<String>,
}
impl ResourceServerRegistration {
pub fn new(
client_id: ClientId,
resources: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
client_id,
resources: resources.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Default, Clone, PartialEq, Eq)]
pub(crate) struct GrantedActor {
#[cfg(feature = "token-exchange")]
pub(crate) act: Option<Box<crate::token_exchange::ActClaim>>,
}
#[derive(Default, Clone, PartialEq, Eq)]
pub(crate) struct GrantedAuthentication {
#[cfg(feature = "consent")]
pub(crate) authentication: Option<Box<crate::consent::Authentication>>,
}
impl GrantedAuthentication {
#[cfg(feature = "consent")]
pub(crate) fn from_code(record: &AuthorizationCodeRecord) -> Self {
GrantedAuthentication {
authentication: record.authentication.clone(),
}
}
#[cfg(not(feature = "consent"))]
pub(crate) fn from_code(_record: &AuthorizationCodeRecord) -> Self {
GrantedAuthentication {}
}
#[cfg(feature = "consent")]
pub(crate) fn from_refresh(record: &RefreshTokenRecord) -> Self {
GrantedAuthentication {
authentication: record.authentication.clone(),
}
}
#[cfg(not(feature = "consent"))]
pub(crate) fn from_refresh(_record: &RefreshTokenRecord) -> Self {
GrantedAuthentication {}
}
}
pub struct UserApproval<'a> {
request: &'a ValidatedAuthorizationRequest,
subject: String,
decided_at: Option<std::time::SystemTime>,
}
impl<'a> UserApproval<'a> {
pub fn granted(request: &'a ValidatedAuthorizationRequest, subject: impl Into<String>) -> Self {
UserApproval {
request,
subject: subject.into(),
decided_at: None,
}
}
pub fn granted_at(
request: &'a ValidatedAuthorizationRequest,
subject: impl Into<String>,
decided_at: std::time::SystemTime,
) -> Self {
UserApproval {
request,
subject: subject.into(),
decided_at: Some(decided_at),
}
}
pub fn decided_at(&self) -> Option<std::time::SystemTime> {
self.decided_at
}
pub fn request(&self) -> &'a ValidatedAuthorizationRequest {
self.request
}
pub fn subject(&self) -> &str {
&self.subject
}
}
impl fmt::Debug for UserApproval<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UserApproval")
.field("client_id", &self.request.client_id)
.field("scope", &self.request.scope)
.field("subject", &"[redacted]")
.finish()
}
}
const USER_CODE_GENERATION_ATTEMPTS: usize = 8;
impl ServerConfig {
pub fn new(issuer: impl Into<String>, verification_uri: impl Into<String>) -> Self {
ServerConfig {
issuer: issuer.into(),
verification_uri: verification_uri.into(),
authorization_endpoint: None,
token_endpoint: None,
device_authorization_endpoint: None,
introspection_endpoint: None,
revocation_endpoint: None,
jwks_uri: None,
registration: None,
#[cfg(feature = "par")]
par: None,
#[cfg(feature = "jar")]
jar: None,
#[cfg(feature = "cimd")]
cimd: None,
scopes_supported: None,
allowed_resources: None,
resource_servers: None,
service_documentation: None,
#[cfg(feature = "rar")]
authorization_details_types_supported: None,
#[cfg(feature = "resource-metadata")]
protected_resources: None,
#[cfg(feature = "jwt")]
access_token_format: AccessTokenFormat::Opaque,
authorization_code_ttl: Duration::from_secs(60),
include_verification_uri_complete: false,
device_code_ttl: Duration::from_secs(600),
poll_interval: Duration::from_secs(5),
slow_down_increment: Duration::from_secs(5),
access_token_ttl: Duration::from_secs(3600),
issue_refresh_tokens: true,
allow_sender_constrained_exchange: false,
allow_authorization_details_exchange: false,
refresh_token_ttl: None,
refresh_reuse_window: Duration::from_secs(30 * 24 * 60 * 60),
#[cfg(feature = "dpop")]
require_dpop: false,
user_code_length: MIN_USER_CODE_LENGTH,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum TokenRequest {
AuthorizationCode {
client_id: ClientId,
client_secret: Option<String>,
code: String,
redirect_uri: Option<String>,
code_verifier: Option<String>,
},
ClientCredentials {
client_id: ClientId,
client_secret: Option<String>,
scope: Option<ScopeSet>,
},
DeviceCode {
client_id: ClientId,
client_secret: Option<String>,
device_code: String,
},
RefreshToken {
client_id: ClientId,
client_secret: Option<String>,
refresh_token: String,
scope: Option<ScopeSet>,
},
}
impl fmt::Debug for TokenRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
value.as_ref().map(|_| "[redacted]")
}
match self {
TokenRequest::AuthorizationCode {
client_id,
client_secret,
code: _,
redirect_uri,
code_verifier,
} => f
.debug_struct("AuthorizationCode")
.field("client_id", client_id)
.field("client_secret", &redact_opt(client_secret))
.field("code", &"[redacted]")
.field("redirect_uri", redirect_uri)
.field("code_verifier", &redact_opt(code_verifier))
.finish(),
TokenRequest::ClientCredentials {
client_id,
client_secret,
scope,
} => f
.debug_struct("ClientCredentials")
.field("client_id", client_id)
.field("client_secret", &redact_opt(client_secret))
.field("scope", scope)
.finish(),
TokenRequest::DeviceCode {
client_id,
client_secret,
device_code: _,
} => f
.debug_struct("DeviceCode")
.field("client_id", client_id)
.field("client_secret", &redact_opt(client_secret))
.field("device_code", &"[redacted]")
.finish(),
TokenRequest::RefreshToken {
client_id,
client_secret,
refresh_token: _,
scope,
} => f
.debug_struct("RefreshToken")
.field("client_id", client_id)
.field("client_secret", &redact_opt(client_secret))
.field("refresh_token", &"[redacted]")
.field("scope", scope)
.finish(),
}
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ClientCredential<'a> {
pub client_secret: Option<&'a str>,
#[cfg(feature = "client-assertion")]
pub client_assertion_type: Option<&'a str>,
#[cfg(feature = "client-assertion")]
pub client_assertion: Option<&'a str>,
#[cfg(feature = "mtls")]
pub certificate: Option<&'a crate::mtls::ClientCertificate<'a>>,
}
impl fmt::Debug for ClientCredential<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
value.as_ref().map(|_| "[redacted]")
}
let mut out = f.debug_struct("ClientCredential");
out.field("client_secret", &redact_opt(&self.client_secret));
#[cfg(feature = "client-assertion")]
out.field("client_assertion_type", &self.client_assertion_type);
#[cfg(feature = "client-assertion")]
out.field("client_assertion", &redact_opt(&self.client_assertion));
#[cfg(feature = "mtls")]
out.field("certificate", &self.certificate);
out.finish()
}
}
impl<'a> ClientCredential<'a> {
pub fn secret(client_secret: Option<&'a str>) -> Self {
ClientCredential {
client_secret,
#[cfg(feature = "client-assertion")]
client_assertion_type: None,
#[cfg(feature = "client-assertion")]
client_assertion: None,
#[cfg(feature = "mtls")]
certificate: None,
}
}
#[cfg(feature = "client-assertion")]
pub fn assertion(client_assertion_type: Option<&'a str>, client_assertion: &'a str) -> Self {
ClientCredential {
client_secret: None,
client_assertion_type,
client_assertion: Some(client_assertion),
#[cfg(feature = "mtls")]
certificate: None,
}
}
#[cfg(feature = "mtls")]
pub fn certificate(certificate: &'a crate::mtls::ClientCertificate<'a>) -> Self {
ClientCredential {
certificate: Some(certificate),
..ClientCredential::secret(None)
}
}
fn or_secret(mut self, secret: Option<&'a str>) -> Self {
if self.client_secret.is_none() {
self.client_secret = secret;
}
self
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct TokenRequestContext<'a> {
pub credential: ClientCredential<'a>,
pub resources: &'a [String],
pub authorization_details: Option<&'a str>,
#[cfg(feature = "dpop")]
pub dpop_proof: Option<&'a str>,
}
impl<'a> TokenRequestContext<'a> {
pub fn new(credential: ClientCredential<'a>) -> Self {
TokenRequestContext {
credential,
resources: &[],
authorization_details: None,
#[cfg(feature = "dpop")]
dpop_proof: None,
}
}
pub fn with_resources(mut self, resources: &'a [String]) -> Self {
self.resources = resources;
self
}
pub fn with_authorization_details(mut self, authorization_details: &'a str) -> Self {
self.authorization_details = Some(authorization_details);
self
}
#[cfg(feature = "dpop")]
pub fn with_dpop_proof(mut self, dpop_proof: &'a str) -> Self {
self.dpop_proof = Some(dpop_proof);
self
}
}
pub(crate) struct Bound<'a> {
pub(crate) cred: ClientCredential<'a>,
#[cfg(feature = "dpop")]
pub(crate) jkt: Option<&'a str>,
}
impl<'a> Bound<'a> {
#[allow(dead_code)]
pub(crate) fn secret(client_secret: Option<&'a str>) -> Self {
Bound {
cred: ClientCredential::secret(client_secret),
#[cfg(feature = "dpop")]
jkt: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeviceApprovalError {
UnknownUserCode,
Expired,
NotPending,
RateLimited,
Storage(StorageError),
}
impl std::fmt::Display for DeviceApprovalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeviceApprovalError::UnknownUserCode => f.write_str("unknown user code"),
DeviceApprovalError::Expired => f.write_str("the code has expired"),
DeviceApprovalError::NotPending => f.write_str("the code was already used"),
DeviceApprovalError::RateLimited => f.write_str("too many attempts"),
DeviceApprovalError::Storage(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for DeviceApprovalError {}
const USER_CODE_ALPHABET: &[u8; 20] = b"BCDFGHJKLMNPQRSTVWXZ";
pub(crate) fn try_random_hex(n_bytes: usize) -> Option<String> {
let mut buf = vec![0u8; n_bytes];
getrandom::fill(&mut buf).ok()?;
Some(hex_encode(&buf))
}
#[cfg(feature = "client-assertion")]
const DUMMY_ASSERTION_SIGNING_INPUT: &str = "oauth-as dummy verification input";
#[cfg(feature = "client-assertion")]
const DUMMY_ASSERTION_SIGNATURE: [u8; 64] = [
91, 15, 217, 171, 65, 158, 255, 105, 97, 207, 103, 199, 34, 188, 42, 123, 113, 63, 9, 92, 242,
81, 20, 20, 147, 223, 209, 148, 122, 59, 212, 156, 132, 79, 44, 44, 108, 53, 228, 247, 251,
153, 155, 251, 71, 102, 34, 231, 227, 160, 80, 16, 215, 84, 84, 74, 117, 3, 91, 5, 148, 20, 28,
47,
];
#[cfg(feature = "client-assertion")]
fn dummy_assertion_key() -> crate::jwt::PublicJwk {
crate::jwt::Jwk {
kty: "EC",
crv: "P-256",
x: "LIZkYOSRaSLc5uMxzlzV9pgt1ARaDl_3tZfRkt9mzFY".to_string(),
y: "fBSzqWfCploda0TpKf3N56v6fk-fORAiVsXUmkWYWkw".to_string(),
kid: "oauth-as-dummy-verification-key".to_string(),
use_: "sig",
alg: "ES256",
}
.to_public_jwk()
}
fn randomness_error() -> ErrorResponse {
ErrorResponse::new(ErrorCode::ServerError)
}
#[derive(Default)]
struct CredentialCost {
secret: bool,
#[cfg(feature = "client-assertion")]
assertion: bool,
}
enum ClientAuthVerdict {
Authenticated(std::sync::Arc<Client>),
Refused(ClientAuthFailure),
}
const USER_CODE_REJECT_AT: u8 = 240;
fn user_code_symbol(byte: u8) -> Option<u8> {
if byte < USER_CODE_REJECT_AT {
Some(USER_CODE_ALPHABET[(byte % 20) as usize])
} else {
None
}
}
fn random_user_code(len: usize) -> Option<String> {
let mut out = String::with_capacity(len);
let mut buf = [0u8; 64];
while out.len() < len {
getrandom::fill(&mut buf).ok()?;
for &byte in buf.iter() {
if out.len() == len {
break;
}
if let Some(symbol) = user_code_symbol(byte) {
out.push(symbol as char);
}
}
}
Some(out)
}
fn display_user_code(raw: &str) -> String {
if raw.len() >= 4 && raw.len() % 2 == 0 {
let mid = raw.len() / 2;
format!("{}-{}", &raw[..mid], &raw[mid..])
} else {
raw.to_string()
}
}
fn challenge_is_well_formed(challenge: &str) -> bool {
challenge.len() == 43
&& challenge
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_'))
}
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
fn decimal_width(n: usize) -> usize {
let mut width = 1;
let mut rest = n / 10;
while rest > 0 {
rest /= 10;
width += 1;
}
width
}
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
fn replay_key(kind: &str, owner: &str, jti: &str) -> String {
use std::fmt::Write as _;
let mut key = String::with_capacity(
kind.len() + 1 + decimal_width(owner.len()) + 1 + owner.len() + jti.len(),
);
key.push_str(kind);
key.push(':');
let _ = write!(key, "{}", owner.len());
key.push(':');
key.push_str(owner);
key.push_str(jti);
key
}
fn storage_error(e: StorageError) -> ErrorResponse {
let _ = e;
ErrorResponse::new(ErrorCode::ServerError)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct GrantedDetails {
#[cfg(feature = "rar")]
inner: Option<Box<crate::rar::AuthorizationDetails>>,
}
impl GrantedDetails {
#[cfg(feature = "rar")]
fn of(details: &crate::rar::AuthorizationDetails) -> Self {
GrantedDetails {
inner: (!details.is_empty()).then(|| Box::new(details.clone())),
}
}
fn of_code(record: &AuthorizationCodeRecord) -> Self {
#[cfg(feature = "rar")]
{
GrantedDetails::of(&record.authorization_details)
}
#[cfg(not(feature = "rar"))]
{
let _ = record;
GrantedDetails {}
}
}
fn of_refresh(record: &RefreshTokenRecord) -> Self {
#[cfg(feature = "rar")]
{
GrantedDetails::of(&record.authorization_details)
}
#[cfg(not(feature = "rar"))]
{
let _ = record;
GrantedDetails {}
}
}
#[allow(dead_code)]
pub(crate) fn of_token(token: &IssuedToken) -> Self {
#[cfg(feature = "rar")]
{
GrantedDetails::of(&token.authorization_details)
}
#[cfg(not(feature = "rar"))]
{
let _ = token;
GrantedDetails {}
}
}
#[cfg(feature = "rar")]
fn into_details(self) -> crate::rar::AuthorizationDetails {
self.inner.map(|d| *d).unwrap_or_default()
}
#[allow(dead_code)]
fn is_empty(&self) -> bool {
#[cfg(feature = "rar")]
{
self.inner.is_none()
}
#[cfg(not(feature = "rar"))]
{
true
}
}
fn narrow(&self, requested: &GrantedDetails) -> Result<GrantedDetails, ErrorResponse> {
#[cfg(feature = "rar")]
{
let requested = match &requested.inner {
None => return Ok(self.clone()),
Some(requested) => requested.as_ref(),
};
let empty = crate::rar::AuthorizationDetails::none();
let granted = self.inner.as_deref().unwrap_or(&empty);
Ok(GrantedDetails::of(&granted.narrow(requested)?))
}
#[cfg(not(feature = "rar"))]
{
let _ = requested;
Ok(GrantedDetails {})
}
}
}
pub(crate) struct RefreshChain {
family_id: String,
expires_at: Option<SystemTime>,
}
pub struct AuthorizationServer<S: Storage, C: Clock = SystemClock> {
config: ServerConfig,
store: S,
clock: C,
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
token_endpoint: Box<str>,
hooks: Hooks,
}
impl<S: Storage> AuthorizationServer<S, SystemClock> {
pub fn new(config: ServerConfig, store: S) -> Self {
Self::with_clock(config, store, SystemClock)
}
}
impl<S: Storage, C: Clock> AuthorizationServer<S, C> {
pub fn with_clock(config: ServerConfig, store: S, clock: C) -> Self {
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
let token_endpoint: Box<str> = match &config.token_endpoint {
Some(endpoint) => endpoint.as_str().into(),
None => format!("{}/token", config.issuer.trim_end_matches('/')).into_boxed_str(),
};
AuthorizationServer {
config,
store,
clock,
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
token_endpoint,
hooks: Hooks::new(),
}
}
pub fn with_event_sink(mut self, sink: Box<dyn EventSink>) -> Self {
self.hooks.install_event_sink(sink);
self
}
pub fn with_rate_limiter(mut self, limiter: Box<dyn RateLimiter>) -> Self {
self.hooks.install_rate_limiter(limiter);
self
}
pub fn with_secret_verifier(
mut self,
verifier: Box<dyn crate::client::SecretVerifier>,
) -> Self {
self.hooks.install_secret_verifier(verifier);
self
}
pub fn with_registration_policy(
mut self,
policy: Box<dyn crate::registration::RegistrationPolicy>,
) -> Self {
self.hooks.install_registration_policy(policy);
self
}
#[cfg(feature = "jar")]
pub fn with_request_object_keys(
mut self,
keys: Box<dyn crate::par::RequestObjectKeys>,
) -> Self {
self.hooks.install_request_object_keys(keys);
self
}
#[cfg(feature = "jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "jwt")))]
pub fn with_es256_verifier(
mut self,
verifier: std::sync::Arc<dyn crate::jwt::Es256Verifier>,
) -> Self {
self.hooks.install_es256_verifier(verifier);
self
}
#[cfg(any(feature = "dpop", feature = "jar", feature = "client-assertion"))]
pub(crate) fn es256_verifier(&self) -> Option<&dyn crate::jwt::Es256Verifier> {
match self.hooks.es256_verifier() {
Some(installed) => Some(&**installed),
#[cfg(feature = "jwt-p256")]
None => Some(&crate::jwt::P256Verifier),
#[cfg(not(feature = "jwt-p256"))]
None => None,
}
}
pub fn hooks(&self) -> &Hooks {
&self.hooks
}
pub(crate) fn now(&self) -> SystemTime {
self.clock.now()
}
async fn restore_refresh_token(
&self,
record: crate::token::RefreshTokenRecord,
) -> Result<(), ErrorResponse> {
let _outcome = self
.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
Ok(())
}
async fn undo_issuance(&self, access_token: &str) {
let _ = self.store.delete_token(access_token).await;
}
pub(crate) fn revocation_window(&self) -> crate::store::RevocationWindow {
let longest = self
.config
.access_token_ttl
.max(self.config.refresh_token_ttl.unwrap_or_default())
.max(self.config.refresh_reuse_window)
.max(self.config.authorization_code_ttl)
.max(self.config.device_code_ttl);
let recorded_at = self.clock.now();
crate::store::RevocationWindow {
recorded_at,
until: saturating_deadline(recorded_at, longest),
}
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "jwt")]
fn access_token_signing_input(
&self,
client: &Client,
subject: Option<&str>,
scope: &ScopeSet,
resource: &[String],
details: &GrantedDetails,
now: SystemTime,
expires_at: SystemTime,
jti: String,
bound: &Bound<'_>,
actor: &GrantedActor,
authentication: &GrantedAuthentication,
) -> Result<Result<(&crate::jwt::JwtConfig, String), String>, ErrorResponse> {
#[cfg(not(feature = "mtls"))]
let _ = bound;
#[cfg(not(feature = "rar"))]
let _ = details;
let jwt = match &self.config.access_token_format {
AccessTokenFormat::Opaque => return Ok(Err(jti)),
AccessTokenFormat::Jwt(jwt) => jwt,
};
#[cfg(not(feature = "token-exchange"))]
let _ = actor;
#[cfg(not(feature = "consent"))]
let _ = authentication;
let claims = AccessTokenClaims {
iss: self.issuer_identifier().to_string(),
exp: crate::jwt::unix_seconds(expires_at)
.map_err(|_| ErrorResponse::new(ErrorCode::ServerError))?,
aud: match resource {
[] => jwt.audience().clone(),
[one] => crate::jwt::Audience::One(one.clone()),
many => crate::jwt::Audience::Many(many.to_vec()),
},
sub: subject
.unwrap_or_else(|| client.client_id.as_str())
.to_string(),
client_id: client.client_id.as_str().to_string(),
iat: crate::jwt::unix_seconds(now)
.map_err(|_| ErrorResponse::new(ErrorCode::ServerError))?,
jti,
scope: (!scope.is_empty()).then(|| scope.to_string()),
#[cfg(feature = "rar")]
authorization_details: details.clone().into_details(),
#[cfg(feature = "consent")]
auth_time: authentication
.authentication
.as_ref()
.and_then(|a| unix_seconds(a.auth_time)),
#[cfg(feature = "consent")]
acr: authentication
.authentication
.as_ref()
.and_then(|a| a.acr.as_deref().map(str::to_string)),
#[cfg(any(feature = "dpop", feature = "mtls"))]
cnf: {
let cnf = crate::token::Confirmation {
#[cfg(feature = "dpop")]
jkt: bound.jkt.map(str::to_string),
#[cfg(feature = "mtls")]
x5t_s256: bound.cred.certificate.map(|c| *c.thumbprint()),
};
(!cnf.is_empty()).then_some(cnf)
},
#[cfg(feature = "token-exchange")]
act: actor.act.as_deref().cloned(),
};
jwt.signing_input(&claims)
.map(|input| Ok((&**jwt, input)))
.map_err(|e| {
let _ = e;
ErrorResponse::new(ErrorCode::ServerError)
})
}
#[cfg(feature = "jwt")]
pub fn jwks(&self) -> Option<Jwks> {
match &self.config.access_token_format {
AccessTokenFormat::Opaque => None,
AccessTokenFormat::Jwt(jwt) => Some(jwt.jwks()),
}
}
#[cfg(feature = "jwt")]
pub fn jwks_uri(&self) -> Option<&str> {
match &self.config.access_token_format {
AccessTokenFormat::Opaque => None,
AccessTokenFormat::Jwt(jwt) => jwt.jwks_uri(),
}
}
pub fn config(&self) -> &ServerConfig {
&self.config
}
pub fn metadata(&self) -> crate::metadata::AuthorizationServerMetadata {
#[allow(unused_mut)]
let mut meta = crate::metadata::AuthorizationServerMetadata::from_config(&self.config);
#[cfg(any(feature = "client-assertion", feature = "jar", feature = "dpop"))]
if self.es256_verifier().is_some() {
meta.es256_verification_is_available();
}
meta
}
pub(crate) fn issuer_identifier(&self) -> &str {
self.config.issuer.trim_end_matches('/')
}
pub(crate) fn validate_resources<'a>(
&self,
requested: impl IntoIterator<Item = &'a str>,
) -> Result<Vec<String>, ErrorResponse> {
let mut out = Vec::new();
let mut seen = 0usize;
for value in requested {
seen += 1;
if seen > MAX_RESOURCE_INDICATORS {
return Err(ErrorResponse::new(ErrorCode::InvalidTarget)
.with_description("too many resource indicators (RFC 8707 s2)"));
}
if !crate::authorization::is_valid_resource_indicator(value) {
return Err(
ErrorResponse::new(ErrorCode::InvalidTarget).with_description(
"resource must be an absolute URI with no fragment (RFC 8707 s2)",
),
);
}
self.target_is_permitted(value)?;
if !out.iter().any(|kept: &String| kept == value) {
out.push(value.to_string());
}
}
Ok(out)
}
pub(crate) fn target_is_permitted(&self, value: &str) -> Result<(), ErrorResponse> {
if let Some(allowed) = &self.config.allowed_resources {
if !allowed.iter().any(|a| &**a == value) {
return Err(ErrorResponse::new(ErrorCode::InvalidTarget)
.with_description("this server does not issue tokens for that resource"));
}
}
Ok(())
}
pub(crate) fn narrow_resources(
granted: &[String],
requested: &[String],
) -> Result<Vec<String>, ErrorResponse> {
if requested.is_empty() {
return Ok(granted.to_vec());
}
for want in requested {
if !granted.iter().any(|g| g == want) {
return Err(ErrorResponse::new(ErrorCode::InvalidTarget)
.with_description("resource was not granted by the authorization request"));
}
}
Ok(requested.to_vec())
}
pub(crate) fn narrow_and_permit(
&self,
granted: &[String],
requested: &[String],
) -> Result<Vec<String>, ErrorResponse> {
let issued = Self::narrow_resources(granted, requested)?;
if !requested.is_empty() {
return Ok(issued);
}
let permitted: Vec<String> = issued
.into_iter()
.filter(|value| self.target_is_permitted(value).is_ok())
.collect();
if permitted.is_empty() && !granted.is_empty() {
return Err(
ErrorResponse::new(ErrorCode::InvalidTarget).with_description(
"this server no longer issues tokens for any resource this grant names",
),
);
}
Ok(permitted)
}
pub fn store(&self) -> &S {
&self.store
}
pub async fn register_client(&self, client: Client) -> Result<(), StorageError> {
for uri in &client.redirect_uris {
if !crate::authorization::is_valid_resource_indicator(uri) {
return Err(StorageError::new(format!(
"redirect_uri {uri:?} is not registerable: RFC 6749 s3.1.2 requires an \
absolute URI with no fragment, and the authorization endpoint matches it by \
exact string, so a registration this server cannot reproduce is a client that \
can never complete a flow"
)));
}
}
self.store.put_client(client).await
}
fn dummy_verify(&self, presented: Option<&str>) {
let presented = match presented {
Some(p) => p,
None => return,
};
let verifier = self.hooks.secret_verifier();
let hash = match verifier.and_then(|v| v.dummy_hash()) {
Some(hash) => hash,
None => crate::client::SecretHash::sha256(
"oauth-as dummy verification input; no registration is stored under this",
),
};
let dummy = crate::client::ClientAuth::ConfidentialSecretHash { hash };
let _ = std::hint::black_box(dummy.verify_with(Some(presented), verifier));
}
#[cfg(feature = "client-assertion")]
fn dummy_assertion_verify(&self) {
match self.es256_verifier() {
Some(verifier) => {
let _ = std::hint::black_box(verifier.verify(
&dummy_assertion_key(),
DUMMY_ASSERTION_SIGNING_INPUT.as_bytes(),
&DUMMY_ASSERTION_SIGNATURE,
));
}
None => {
let _ = std::hint::black_box(crate::jwt::verify_hs256(
DUMMY_ASSERTION_SIGNING_INPUT.as_bytes(),
DUMMY_ASSERTION_SIGNING_INPUT.as_bytes(),
&DUMMY_ASSERTION_SIGNATURE[..32],
));
}
}
}
#[cfg(feature = "client-assertion")]
fn assertion_could_be_verified(cred: &ClientCredential<'_>) -> bool {
cred.client_assertion.is_some()
&& cred.client_assertion_type == Some(CLIENT_ASSERTION_TYPE)
&& cred.client_secret.is_none()
}
pub(crate) async fn authenticate_client(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
) -> Result<std::sync::Arc<Client>, ErrorResponse> {
let attempt = Attempt::ClientAuthentication {
client_id: client_id.as_str(),
};
self.admit_client_authentication(attempt, client_id)?;
let mut paid = CredentialCost::default();
let verdict = self
.classify_client_credential(client_id, cred, &mut paid)
.await?;
match verdict {
ClientAuthVerdict::Authenticated(client) => {
self.hooks.record(attempt, AttemptOutcome::Succeeded);
Ok(client)
}
ClientAuthVerdict::Refused(failure) => {
self.settle_credential_cost(cred, &paid);
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure,
});
Err(ErrorResponse::new(ErrorCode::InvalidClient))
}
}
}
fn admit_client_authentication(
&self,
attempt: Attempt<'_>,
client_id: &ClientId,
) -> Result<(), ErrorResponse> {
if self.hooks.check(attempt) == RateLimitDecision::Deny {
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::RateLimited,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
Ok(())
}
fn settle_credential_cost(&self, cred: &ClientCredential<'_>, paid: &CredentialCost) {
if !paid.secret {
self.dummy_verify(cred.client_secret);
}
#[cfg(feature = "client-assertion")]
if !paid.assertion && Self::assertion_could_be_verified(cred) {
self.dummy_assertion_verify();
}
}
async fn classify_client_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
paid: &mut CredentialCost,
) -> Result<ClientAuthVerdict, ErrorResponse> {
let found = self
.store
.get_client(client_id)
.await
.map_err(storage_error)?;
let client = match found {
Some(client) => client,
None => return Ok(ClientAuthVerdict::Refused(ClientAuthFailure::UnknownClient)),
};
if let Some(registration) = &client.registration {
if let Some(expires_at) = registration.client_secret_expires_at {
let expired = expires_at != 0
&& self
.clock
.now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_secs() >= expires_at)
.unwrap_or(false);
if expired {
return Ok(ClientAuthVerdict::Refused(ClientAuthFailure::SecretExpired));
}
}
}
#[cfg(feature = "client-assertion")]
if cred.client_assertion.is_some() {
let outcome = self.authenticate_by_assertion(&client, cred, paid).await;
return Ok(match outcome {
Ok(()) => ClientAuthVerdict::Authenticated(client),
Err(reason) => {
ClientAuthVerdict::Refused(ClientAuthFailure::AssertionInvalid { reason })
}
});
}
#[cfg(feature = "mtls")]
if matches!(client.auth, crate::client::ClientAuth::Mtls { .. }) {
return Ok(match crate::mtls::verify_certificate(&client, cred) {
Ok(()) => ClientAuthVerdict::Authenticated(client),
Err(failure) => ClientAuthVerdict::Refused(failure),
});
}
paid.secret = matches!(
client.auth,
crate::client::ClientAuth::ConfidentialSecret { .. }
| crate::client::ClientAuth::ConfidentialSecretHash { .. }
);
if !client
.auth
.verify_with(cred.client_secret, self.hooks.secret_verifier())
{
return Ok(ClientAuthVerdict::Refused(
ClientAuthFailure::SecretMismatch,
));
}
Ok(ClientAuthVerdict::Authenticated(client))
}
#[cfg(feature = "client-assertion")]
async fn authenticate_by_assertion(
&self,
client: &Client,
cred: &ClientCredential<'_>,
paid: &mut CredentialCost,
) -> Result<(), crate::client_assertion::AssertionFailure> {
use crate::client_assertion::AssertionFailure;
if !Self::assertion_could_be_verified(cred) {
return Err(AssertionFailure::Malformed);
}
let assertion = cred.client_assertion.ok_or(AssertionFailure::Malformed)?;
let keys = match &client.auth {
crate::client::ClientAuth::ConfidentialAssertion { keys } => keys,
_ => return Err(AssertionFailure::WrongPrincipal),
};
paid.assertion = true;
let verified = verify_assertion(
self.es256_verifier(),
keys,
assertion,
client.client_id.as_str(),
&[self.token_endpoint(), self.issuer_identifier()],
self.clock.now(),
)?;
let claimed = self
.store
.claim_replay_id(
&replay_key("ca", client.client_id.as_str(), &verified.jti),
verified.expires_at,
)
.await
.map_err(|_| AssertionFailure::ReplayCheckUnavailable)?;
if !claimed {
return Err(AssertionFailure::Replayed);
}
Ok(())
}
#[cfg(any(feature = "client-assertion", feature = "dpop"))]
fn token_endpoint(&self) -> &str {
&self.token_endpoint
}
#[cfg(feature = "dpop")]
async fn verify_dpop(&self, proof: Option<&str>) -> Result<Option<Box<str>>, ErrorResponse> {
let proof = match proof {
Some(proof) => proof,
None if self.config.require_dpop => {
return Err(ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("this server requires a DPoP proof on every token request"))
}
None => return Ok(None),
};
let verifier = self.es256_verifier().ok_or_else(|| {
self.hooks.emit(|| Event::DpopProofRefused {
failure: crate::dpop::DpopFailure::UnsupportedAlgorithm,
});
ErrorResponse::new(ErrorCode::InvalidDpopProof)
})?;
let verified = verify_proof(
verifier,
proof,
"POST",
self.token_endpoint(),
self.clock.now(),
)
.map_err(|failure| {
self.hooks.emit(|| Event::DpopProofRefused { failure });
ErrorResponse::new(ErrorCode::InvalidDpopProof)
})?;
let claimed = self
.store
.claim_replay_id(
&replay_key("dpop", &verified.jkt, &verified.jti),
verified.replay_until,
)
.await
.map_err(storage_error)?;
if !claimed {
self.hooks.emit(|| Event::DpopProofRefused {
failure: crate::dpop::DpopFailure::Replayed,
});
return Err(ErrorResponse::new(ErrorCode::InvalidDpopProof));
}
Ok(Some(verified.jkt.into_boxed_str()))
}
fn granted_default_scope(client: &Client) -> ScopeSet {
if client.default_scopes.is_subset(&client.allowed_scopes) {
return client.default_scopes.clone();
}
ScopeSet::from_tokens(
client
.default_scopes
.iter()
.filter(|s| client.allowed_scopes.contains(s.as_str()))
.map(|s| s.as_str()),
)
.unwrap_or_else(|_| ScopeSet::empty())
}
fn resolve_scope(
client: &Client,
requested: Option<&ScopeSet>,
) -> Result<ScopeSet, ErrorResponse> {
match requested {
None => Ok(Self::granted_default_scope(client)),
Some(s) if s.is_subset(&client.allowed_scopes) => Ok(s.clone()),
Some(_) => Err(ErrorResponse::new(ErrorCode::InvalidScope)
.with_description("requested scope exceeds the client registration")),
}
}
pub async fn device_authorization(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
requested_scope: Option<&ScopeSet>,
) -> Result<DeviceAuthorizationResponse, ErrorResponse> {
self.device_authorization_with_credential(
client_id,
&ClientCredential::secret(client_secret),
requested_scope,
)
.await
}
pub async fn device_authorization_with_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
requested_scope: Option<&ScopeSet>,
) -> Result<DeviceAuthorizationResponse, ErrorResponse> {
let created_at = self.clock.now();
let client = self.authenticate_client(client_id, cred).await?;
if !client.allows_grant(GrantType::DeviceCode) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient)
.with_description("client registration does not include the device_code grant"));
}
let scope = Self::resolve_scope(&client, requested_scope)?;
let now = self.clock.now();
let device_code = try_random_hex(32).ok_or_else(randomness_error)?;
let user_code = self.unique_user_code().await?;
let grant = DeviceGrant {
device_code: device_code.clone(),
user_code: user_code.clone(),
client_id: client.client_id.clone(),
scope,
state: DeviceGrantState::Pending,
created_at,
expires_at: saturating_deadline(now, self.config.device_code_ttl),
interval: self.config.poll_interval,
last_poll_at: None,
};
self.store
.put_device_grant(grant)
.await
.map_err(storage_error)?;
let verification_uri_complete = self.config.include_verification_uri_complete.then(|| {
format!(
"{}{}user_code={}",
self.config.verification_uri,
crate::authorization::query_separator(&self.config.verification_uri),
user_code
)
});
Ok(DeviceAuthorizationResponse {
device_code,
user_code,
verification_uri: self.config.verification_uri.clone(),
verification_uri_complete,
expires_in: self.config.device_code_ttl.as_secs(),
interval: self.config.poll_interval.as_secs(),
})
}
async fn unique_user_code(&self) -> Result<String, ErrorResponse> {
let len = self.config.user_code_length.max(MIN_USER_CODE_LENGTH);
for _ in 0..USER_CODE_GENERATION_ATTEMPTS {
let raw = random_user_code(len).ok_or_else(randomness_error)?;
if self
.store
.find_device_grant_by_user_code(&raw)
.await
.map_err(storage_error)?
.is_none()
{
return Ok(display_user_code(&raw));
}
}
Err(ErrorResponse::new(ErrorCode::ServerError)
.with_description("could not allocate an unused user code"))
}
async fn pending_grant_by_user_code(
&self,
entered_user_code: &str,
) -> Result<DeviceGrant, DeviceApprovalError> {
let attempt = Attempt::DeviceUserCodeEntry;
if self.hooks.check(attempt) == RateLimitDecision::Deny {
return Err(DeviceApprovalError::RateLimited);
}
let outcome = self
.lookup_pending_grant_by_user_code(entered_user_code)
.await;
self.hooks.record(
attempt,
if outcome.is_ok() {
AttemptOutcome::Succeeded
} else {
AttemptOutcome::Failed
},
);
outcome
}
async fn lookup_pending_grant_by_user_code(
&self,
entered_user_code: &str,
) -> Result<DeviceGrant, DeviceApprovalError> {
let normalized = normalize_user_code(entered_user_code);
let grant = self
.store
.find_device_grant_by_user_code(&normalized)
.await
.map_err(DeviceApprovalError::Storage)?
.ok_or(DeviceApprovalError::UnknownUserCode)?;
if self.clock.now() >= grant.expires_at {
let _ = self.store.take_device_grant(&grant.device_code).await;
return Err(DeviceApprovalError::Expired);
}
if grant.state != DeviceGrantState::Pending {
return Err(DeviceApprovalError::NotPending);
}
Ok(grant)
}
pub async fn approve_device(
&self,
entered_user_code: &str,
subject: impl Into<String>,
) -> Result<(), DeviceApprovalError> {
let mut grant = self.pending_grant_by_user_code(entered_user_code).await?;
let subject = subject.into();
let audit = self
.hooks
.is_observed()
.then(|| (grant.client_id.clone(), subject.clone()));
grant.state = DeviceGrantState::Approved { subject };
if !self
.store
.compare_and_swap_device_grant(&DeviceGrantState::Pending, grant)
.await
.map_err(DeviceApprovalError::Storage)?
{
return Err(DeviceApprovalError::NotPending);
}
if let Some((client_id, subject)) = &audit {
self.hooks.emit(|| Event::DeviceGrantApproved {
client_id: client_id.as_str(),
subject,
});
}
Ok(())
}
pub async fn deny_device(&self, entered_user_code: &str) -> Result<(), DeviceApprovalError> {
let mut grant = self.pending_grant_by_user_code(entered_user_code).await?;
let audit = self.hooks.is_observed().then(|| grant.client_id.clone());
grant.state = DeviceGrantState::Denied;
if !self
.store
.compare_and_swap_device_grant(&DeviceGrantState::Pending, grant)
.await
.map_err(DeviceApprovalError::Storage)?
{
return Err(DeviceApprovalError::NotPending);
}
if let Some(client_id) = &audit {
self.hooks.emit(|| Event::DeviceGrantDenied {
client_id: client_id.as_str(),
});
}
Ok(())
}
pub fn token(
&self,
request: TokenRequest,
) -> impl std::future::Future<Output = Result<TokenResponse, ErrorResponse>> + '_ {
self.token_with_resources(request, &[])
}
pub fn token_with_resources<'a>(
&'a self,
request: TokenRequest,
resources: &'a [String],
) -> impl std::future::Future<Output = Result<TokenResponse, ErrorResponse>> + 'a {
self.token_with_context(
request,
TokenRequestContext {
resources,
..Default::default()
},
)
}
#[allow(clippy::manual_async_fn)]
pub fn token_with_context<'a>(
&'a self,
request: TokenRequest,
context: TokenRequestContext<'a>,
) -> impl std::future::Future<Output = Result<TokenResponse, ErrorResponse>> + 'a {
async move {
let requested_resources =
self.validate_resources(context.resources.iter().map(|r| r.as_str()))?;
#[cfg(feature = "rar")]
let requested_details = match context.authorization_details {
None => GrantedDetails::default(),
Some(raw) => {
let parsed = crate::rar::AuthorizationDetails::parse(raw)?;
parsed.require_supported_types(
self.config.authorization_details_types_supported.as_deref(),
)?;
GrantedDetails::of(&parsed)
}
};
#[cfg(not(feature = "rar"))]
if context.authorization_details.is_some() {
return Err(ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
.with_description("this server does not support authorization_details"));
}
#[cfg(not(feature = "rar"))]
let requested_details = GrantedDetails::default();
#[cfg(feature = "dpop")]
let jkt = self.verify_dpop(context.dpop_proof).await?;
match &request {
TokenRequest::AuthorizationCode {
client_id,
client_secret,
code,
redirect_uri,
code_verifier,
} => {
let bound = Bound {
cred: context.credential.or_secret(client_secret.as_deref()),
#[cfg(feature = "dpop")]
jkt: jkt.as_deref(),
};
let outcome = self
.authorization_code_token(
client_id,
&bound,
code,
redirect_uri.as_deref(),
code_verifier.as_deref(),
&requested_resources,
requested_details,
)
.await;
self.emit_refusal(client_id, GrantType::AuthorizationCode, &outcome);
outcome
}
TokenRequest::ClientCredentials {
client_id,
client_secret,
scope,
} => {
let bound = Bound {
cred: context.credential.or_secret(client_secret.as_deref()),
#[cfg(feature = "dpop")]
jkt: jkt.as_deref(),
};
let outcome = self
.client_credentials_token(
client_id,
&bound,
scope.as_ref(),
requested_resources,
requested_details,
)
.await;
self.emit_refusal(client_id, GrantType::ClientCredentials, &outcome);
outcome
}
TokenRequest::DeviceCode {
client_id,
client_secret,
device_code,
} => {
if !requested_resources.is_empty() {
return Err(
ErrorResponse::new(ErrorCode::InvalidTarget).with_description(
"the device authorization request granted no resource to narrow to",
),
);
}
#[cfg(feature = "rar")]
if !requested_details.is_empty() {
return Err(ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
.with_description(
"the device authorization request granted no authorization_details",
));
}
let bound = Bound {
cred: context.credential.or_secret(client_secret.as_deref()),
#[cfg(feature = "dpop")]
jkt: jkt.as_deref(),
};
let outcome = self.device_token(client_id, &bound, device_code).await;
self.emit_refusal(client_id, GrantType::DeviceCode, &outcome);
outcome
}
TokenRequest::RefreshToken {
client_id,
client_secret,
refresh_token,
scope,
} => {
let bound = Bound {
cred: context.credential.or_secret(client_secret.as_deref()),
#[cfg(feature = "dpop")]
jkt: jkt.as_deref(),
};
let outcome = self
.refresh_token(
client_id,
&bound,
refresh_token,
scope.as_ref(),
&requested_resources,
requested_details,
)
.await;
self.emit_refusal(client_id, GrantType::RefreshToken, &outcome);
outcome
}
}
}
}
fn emit_refusal(
&self,
client_id: &ClientId,
grant_type: GrantType,
outcome: &Result<TokenResponse, ErrorResponse>,
) {
if let Err(error) = outcome {
self.hooks.emit(|| Event::GrantRefused {
client_id: client_id.as_str(),
grant_type,
error: error.error,
});
}
}
pub async fn validate_authorization_request(
&self,
request: &AuthorizationRequest<'_>,
) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
#[cfg(feature = "par")]
if matches!(&self.config.par, Some(par) if par.require_pushed_authorization_requests) {
return Err(AuthorizationError::Direct(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"this server accepts authorization request data only via PAR (RFC 9126 s4)",
),
));
}
#[cfg(feature = "jar")]
if matches!(&self.config.jar, Some(jar) if jar.require_signed_request_object) {
return Err(AuthorizationError::Direct(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"this server requires a signed request object (RFC 9101 s10.5)",
),
));
}
self.validate_direct_authorization_request(request).await
}
pub(crate) async fn validate_direct_authorization_request(
&self,
request: &AuthorizationRequest<'_>,
) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
let attempt = Attempt::AuthorizationRequest {
client_id: request.client_id.as_deref().unwrap_or(""),
};
if self.hooks.check(attempt) == RateLimitDecision::Deny {
return Err(AuthorizationError::Direct(
ErrorResponse::new(ErrorCode::TemporarilyUnavailable)
.with_description("too many authorization requests; retry later"),
));
}
let outcome = self
.validate_direct_authorization_request_inner(request)
.await;
self.hooks.record(
attempt,
match &outcome {
Ok(_) => AttemptOutcome::Succeeded,
Err(_) => AttemptOutcome::Failed,
},
);
outcome
}
async fn validate_direct_authorization_request_inner(
&self,
request: &AuthorizationRequest<'_>,
) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
let direct = |code: ErrorCode, why: &'static str| {
AuthorizationError::Direct(ErrorResponse::new(code).with_description(why))
};
let client_id = request
.client_id
.as_deref()
.filter(|s| !s.is_empty())
.ok_or_else(|| direct(ErrorCode::InvalidRequest, "missing client_id"))?;
let client = self
.store
.get_client(&ClientId::new(client_id))
.await
.map_err(|_| direct(ErrorCode::ServerError, "storage unavailable"))?
.ok_or_else(|| direct(ErrorCode::InvalidRequest, "unknown client_id"))?;
let redirect_uri_was_explicit = request.redirect_uri.is_some();
let redirect_uri = match request.redirect_uri.as_deref() {
Some(requested) => client
.redirect_uris
.iter()
.find(|registered| registered.as_str() == requested)
.cloned()
.ok_or_else(|| {
direct(
ErrorCode::InvalidRequest,
"redirect_uri does not exactly match a registered URI",
)
})?,
None => match client.redirect_uris.as_slice() {
[only] => only.clone(),
_ => {
return Err(direct(
ErrorCode::InvalidRequest,
"redirect_uri is required for this client",
))
}
},
};
let state = request.state.as_deref().map(str::to_string);
let redirect = |code: ErrorCode, why: &'static str| {
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: redirect_uri.clone(),
error: ErrorResponse::new(code).with_description(why),
state: state.clone(),
iss: self.issuer_identifier().to_string(),
})
};
match request.response_type.as_deref() {
Some("code") => {}
None => return Err(redirect(ErrorCode::InvalidRequest, "missing response_type")),
Some(_) => {
return Err(redirect(
ErrorCode::UnsupportedResponseType,
"this server issues authorization codes only",
))
}
}
if !client.allows_grant(GrantType::AuthorizationCode) {
return Err(redirect(
ErrorCode::UnauthorizedClient,
"client registration does not include the authorization_code grant",
));
}
match request.code_challenge_method.as_deref() {
Some("S256") => {}
None => {
return Err(redirect(
ErrorCode::InvalidRequest,
"code_challenge_method=S256 is required",
))
}
Some(_) => {
return Err(redirect(
ErrorCode::InvalidRequest,
"only code_challenge_method=S256 is supported",
))
}
}
let code_challenge = request.code_challenge.as_deref().unwrap_or_default();
if !challenge_is_well_formed(code_challenge) {
return Err(redirect(
ErrorCode::InvalidRequest,
"code_challenge must be the base64url SHA-256 form of RFC 7636 section 4.2",
));
}
let scope = match request.scope.as_deref() {
None => Self::granted_default_scope(&client),
Some(s) => {
let requested = ScopeSet::parse(s)
.map_err(|_| redirect(ErrorCode::InvalidScope, "malformed scope"))?;
if !requested.is_subset(&client.allowed_scopes) {
return Err(redirect(
ErrorCode::InvalidScope,
"requested scope exceeds the client registration",
));
}
requested
}
};
let resource = self
.validate_resources(request.resource.iter().map(|r| r.as_ref()))
.map_err(|e| {
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: redirect_uri.clone(),
error: e,
state: state.clone(),
iss: self.issuer_identifier().to_string(),
})
})?;
#[cfg(not(feature = "rar"))]
if request.authorization_details.is_some() {
return Err(AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: redirect_uri.clone(),
error: ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
.with_description("this server does not support authorization_details"),
state: state.clone(),
iss: self.issuer_identifier().to_string(),
}));
}
#[cfg(feature = "rar")]
let details = {
let to_redirect = |error: ErrorResponse| {
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: redirect_uri.clone(),
error,
state: state.clone(),
iss: self.issuer_identifier().to_string(),
})
};
match request.authorization_details.as_deref() {
None => crate::rar::AuthorizationDetails::none(),
Some(raw) => {
let parsed =
crate::rar::AuthorizationDetails::parse(raw).map_err(to_redirect)?;
parsed
.require_supported_types(
self.config.authorization_details_types_supported.as_deref(),
)
.map_err(to_redirect)?;
parsed
}
}
};
#[cfg(feature = "consent")]
let requirement = crate::consent::AuthenticationRequirement::from_request(request)
.map_err(|error| {
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: redirect_uri.clone(),
error,
state: state.clone(),
iss: self.issuer_identifier().to_string(),
})
})?;
#[allow(unused_mut)]
let mut validated = ValidatedAuthorizationRequest::new(
client.client_id.clone(),
redirect_uri,
redirect_uri_was_explicit,
scope,
state,
code_challenge.to_string(),
CodeChallengeMethod::S256,
self.issuer_identifier().to_string(),
resource,
);
#[cfg(feature = "rar")]
validated.set_authorization_details(details);
#[cfg(feature = "consent")]
validated.set_authentication_requirement(requirement);
Ok(validated)
}
pub async fn issue_authorization_code(
&self,
approval: UserApproval<'_>,
) -> Result<AuthorizationResponse, AuthorizationError> {
#[cfg(feature = "consent")]
{
let requirement = approval.request().authentication_requirement.clone();
return self
.issue_authorization_code_with_authentication(approval, &requirement, None)
.await;
}
#[cfg(not(feature = "consent"))]
self.issue_authorization_code_inner(approval, GrantedAuthentication::default())
.await
}
#[cfg(feature = "consent")]
pub async fn issue_authorization_code_with_authentication(
&self,
approval: UserApproval<'_>,
requirement: &crate::consent::AuthenticationRequirement,
authentication: Option<&crate::consent::Authentication>,
) -> Result<AuthorizationResponse, AuthorizationError> {
if let Err(failure) = requirement.satisfied_by(authentication, self.clock.now()) {
let request = approval.request();
return Err(AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: request.redirect_uri.clone(),
error: failure.error_response(),
state: request.state.clone(),
iss: request.issuer.clone(),
}));
}
self.issue_authorization_code_inner(
approval,
GrantedAuthentication {
authentication: authentication.cloned().map(Box::new),
},
)
.await
}
async fn issue_authorization_code_inner(
&self,
approval: UserApproval<'_>,
authentication: GrantedAuthentication,
) -> Result<AuthorizationResponse, AuthorizationError> {
#[cfg(not(feature = "consent"))]
let _ = authentication;
let UserApproval {
request,
subject,
decided_at,
} = approval;
let attempt = Attempt::AuthorizationRequest {
client_id: request.client_id.as_str(),
};
if self.hooks.check(attempt) == RateLimitDecision::Deny {
return Err(AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: request.redirect_uri.clone(),
error: ErrorResponse::new(ErrorCode::TemporarilyUnavailable)
.with_description("too many authorization requests; retry later"),
state: request.state.clone(),
iss: request.issuer.clone(),
}));
}
let now = self.clock.now();
let code = try_random_hex(32).ok_or_else(|| {
self.hooks.record(attempt, AttemptOutcome::Failed);
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: request.redirect_uri.clone(),
error: ErrorResponse::new(ErrorCode::ServerError),
state: request.state.clone(),
iss: request.issuer.clone(),
})
})?;
let record = AuthorizationCodeRecord {
issued_at: decided_at.unwrap_or(now),
code: code.clone(),
client_id: request.client_id.clone(),
redirect_uri: request.redirect_uri.clone(),
redirect_uri_was_explicit: request.redirect_uri_was_explicit,
scope: request.scope.clone(),
subject,
code_challenge: request.code_challenge.clone(),
code_challenge_method: request.code_challenge_method,
resource: request.resource.clone(),
#[cfg(feature = "rar")]
authorization_details: request.authorization_details.clone(),
expires_at: saturating_deadline(now, self.config.authorization_code_ttl),
state: AuthorizationCodeState::Issued,
#[cfg(feature = "consent")]
authentication: authentication.authentication,
};
self.store
.put_authorization_code(record)
.await
.map_err(|_| {
self.hooks.record(attempt, AttemptOutcome::Failed);
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: request.redirect_uri.clone(),
error: ErrorResponse::new(ErrorCode::ServerError),
state: request.state.clone(),
iss: request.issuer.clone(),
})
})?;
self.hooks.record(attempt, AttemptOutcome::Succeeded);
Ok(AuthorizationResponse {
code,
state: request.state.clone(),
iss: request.issuer.clone(),
})
}
#[allow(clippy::too_many_arguments)]
async fn authorization_code_token(
&self,
client_id: &ClientId,
bound: &Bound<'_>,
code: &str,
redirect_uri: Option<&str>,
code_verifier: Option<&str>,
requested_resources: &[String],
requested_details: GrantedDetails,
) -> Result<TokenResponse, ErrorResponse> {
let client = self.authenticate_client(client_id, &bound.cred).await?;
if !client.allows_grant(GrantType::AuthorizationCode) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient));
}
let record = self
.store
.take_authorization_code(code)
.await
.map_err(storage_error)?
.ok_or_else(|| ErrorResponse::new(ErrorCode::InvalidGrant))?;
if record.client_id != client.client_id {
self.store
.put_authorization_code(record)
.await
.map_err(storage_error)?;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant));
}
if let Some((access_token, refresh_token)) = record.state.minted() {
let (access_token, refresh_token) = (
access_token.map(str::to_string),
refresh_token.map(str::to_string),
);
let (access_token, refresh_token) = (&access_token, &refresh_token);
let mut revoked_family = false;
let mut containment_failed = false;
let mut revoked_family_id: Option<String> = None;
if let Some(rt) = refresh_token {
match self.store.get_refresh_token(rt).await {
Ok(Some(rec)) => {
revoked_family_id = Some(rec.family_id.clone());
match self
.store
.revoke_token_family(&rec.family_id, self.revocation_window())
.await
{
Ok(_) => revoked_family = true,
Err(_) => containment_failed = true,
}
}
Ok(None) => {}
Err(_) => containment_failed = true,
}
}
if !revoked_family {
if let Some(at) = access_token {
if self.store.delete_token(at).await.is_err() {
containment_failed = true;
}
}
}
let replayed = AuthorizationCodeRecord {
state: AuthorizationCodeState::Replayed {
access_token: access_token.clone(),
refresh_token: refresh_token.clone(),
},
..record
};
if self.store.put_authorization_code(replayed).await.is_err() {
containment_failed = true;
}
self.hooks.emit(|| Event::AuthorizationCodeReplayDetected {
client_id: client.client_id.as_str(),
family_id: revoked_family_id.as_deref(),
tokens_revoked: revoked_family,
containment_failed,
});
return Err(ErrorResponse::new(ErrorCode::InvalidGrant));
}
if self.clock.now() >= record.expires_at {
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("authorization code expired"));
}
let redirect_uri_matches = match redirect_uri {
Some(u) => u == record.redirect_uri,
None => !record.redirect_uri_was_explicit,
};
if !redirect_uri_matches {
let _ = self.store.put_authorization_code(record).await;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("redirect_uri does not match the authorization request"));
}
let verified = match (code_verifier, record.code_challenge_method) {
(Some(v), CodeChallengeMethod::S256) => {
crate::pkce::verifier_is_valid(v)
&& crate::pkce::verify_s256(v, &record.code_challenge)
}
(None, _) => false,
};
if !verified {
let _ = self.store.put_authorization_code(record).await;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("code_verifier does not match the recorded code_challenge"));
}
let narrowed = self
.narrow_and_permit(&record.resource, requested_resources)
.and_then(|r| {
GrantedDetails::of_code(&record)
.narrow(&requested_details)
.map(|d| (r, d))
});
let (resource, details) = match narrowed {
Ok(narrowed) => narrowed,
Err(e) => {
let _ = self.store.put_authorization_code(record).await;
return Err(e);
}
};
let subject = record.subject.clone();
let scope = record.scope.clone();
let authentication = GrantedAuthentication::from_code(&record);
let mut consumed = AuthorizationCodeRecord {
state: AuthorizationCodeState::Consumed {
access_token: None,
refresh_token: None,
},
..record
};
self.store
.put_authorization_code(consumed.clone())
.await
.map_err(storage_error)?;
let issued = self
.issue_boxed(
&client,
bound,
GrantType::AuthorizationCode,
record.issued_at,
Some(subject),
scope,
resource,
details,
None,
true,
authentication,
GrantedActor::default(),
None,
)
.await?;
let expected_before_issuance = AuthorizationCodeState::Consumed {
access_token: None,
refresh_token: None,
};
consumed.state = AuthorizationCodeState::Consumed {
access_token: Some(issued.access_token.clone()),
refresh_token: issued.refresh_token.clone(),
};
let recorded = self
.store
.compare_and_swap_authorization_code(&expected_before_issuance, consumed)
.await;
if !matches!(recorded, Ok(true)) {
self.undo_issuance(&issued.access_token).await;
if let Some(rt) = &issued.refresh_token {
let _ = self.store.take_refresh_token(rt).await;
}
return Err(match recorded {
Ok(_) => ErrorResponse::new(ErrorCode::InvalidGrant).with_description(
"this authorization code was replayed or revoked during redemption",
),
Err(_) => ErrorResponse::new(ErrorCode::ServerError)
.with_description("could not record the redemption"),
});
}
Ok(issued)
}
async fn client_credentials_token(
&self,
client_id: &ClientId,
bound: &Bound<'_>,
requested_scope: Option<&ScopeSet>,
resource: Vec<String>,
details: GrantedDetails,
) -> Result<TokenResponse, ErrorResponse> {
let grant_established_at = self.clock.now();
let client = self.authenticate_client(client_id, &bound.cred).await?;
if matches!(client.auth, crate::client::ClientAuth::Public) {
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::NotConfidential,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
if !client.allows_grant(GrantType::ClientCredentials) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient));
}
let scope = Self::resolve_scope(&client, requested_scope)?;
self.issue_boxed(
&client,
bound,
GrantType::ClientCredentials,
grant_established_at,
None,
scope,
resource,
details,
None,
false,
GrantedAuthentication::default(),
GrantedActor::default(),
None,
)
.await
}
async fn device_token(
&self,
client_id: &ClientId,
bound: &Bound<'_>,
device_code: &str,
) -> Result<TokenResponse, ErrorResponse> {
let client = self.authenticate_client(client_id, &bound.cred).await?;
if !client.allows_grant(GrantType::DeviceCode) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient));
}
let mut grant = self
.store
.get_device_grant(device_code)
.await
.map_err(storage_error)?
.ok_or_else(|| ErrorResponse::new(ErrorCode::InvalidGrant))?;
if grant.client_id != client.client_id {
return Err(ErrorResponse::new(ErrorCode::InvalidGrant));
}
let now = self.clock.now();
if now >= grant.expires_at {
let _ = self.store.take_device_grant(device_code).await;
return Err(ErrorResponse::new(ErrorCode::ExpiredToken));
}
if let Some(last) = grant.last_poll_at {
#[allow(clippy::unnecessary_map_or)]
if last
.checked_add(grant.interval)
.map_or(true, |next| now < next)
{
let expected = grant.state.clone();
grant.interval = grant
.interval
.saturating_add(self.config.slow_down_increment);
grant.last_poll_at = Some(now);
self.store
.compare_and_swap_device_grant(&expected, grant)
.await
.map_err(storage_error)?;
return Err(ErrorResponse::new(ErrorCode::SlowDown));
}
}
let state = grant.state.clone();
grant.last_poll_at = Some(now);
match state {
DeviceGrantState::Pending => {
self.store
.compare_and_swap_device_grant(&DeviceGrantState::Pending, grant)
.await
.map_err(storage_error)?;
Err(ErrorResponse::new(ErrorCode::AuthorizationPending))
}
DeviceGrantState::Denied => {
let _ = self.store.take_device_grant(device_code).await;
Err(ErrorResponse::new(ErrorCode::AccessDenied))
}
DeviceGrantState::Approved { subject } => {
let taken = self
.store
.take_device_grant(device_code)
.await
.map_err(storage_error)?
.ok_or_else(|| ErrorResponse::new(ErrorCode::InvalidGrant))?;
self.issue_boxed(
&client,
bound,
GrantType::DeviceCode,
taken.created_at,
Some(subject),
taken.scope,
Vec::new(),
GrantedDetails::default(),
None,
true,
GrantedAuthentication::default(),
GrantedActor::default(),
None,
)
.await
}
}
}
async fn refresh_token(
&self,
client_id: &ClientId,
bound: &Bound<'_>,
refresh_token: &str,
requested_scope: Option<&ScopeSet>,
requested_resources: &[String],
requested_details: GrantedDetails,
) -> Result<TokenResponse, ErrorResponse> {
let client = self.authenticate_client(client_id, &bound.cred).await?;
if !client.allows_grant(GrantType::RefreshToken) {
return Err(ErrorResponse::new(ErrorCode::UnauthorizedClient));
}
let record = self
.store
.take_refresh_token(refresh_token)
.await
.map_err(storage_error)?
.ok_or_else(|| ErrorResponse::new(ErrorCode::InvalidGrant))?;
if record.client_id != client.client_id {
self.restore_refresh_token(record).await?;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant));
}
if record.state == RefreshTokenState::Spent {
let mut containment_failed = false;
let family_id = record.family_id.clone();
let mut records_revoked = 0;
match self
.store
.revoke_token_family(&family_id, self.revocation_window())
.await
{
Ok(revoked) => records_revoked = revoked,
Err(_) => {
containment_failed = true;
let _ = self.store.put_refresh_token(record).await;
}
}
self.hooks.emit(|| Event::RefreshTokenReuseDetected {
client_id: client.client_id.as_str(),
family_id: &family_id,
records_revoked,
containment_failed,
});
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("refresh token reuse detected; the grant has been revoked"));
}
#[cfg(feature = "dpop")]
if record.jkt.as_deref() != bound.jkt {
self.restore_refresh_token(record).await?;
return Err(ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("this refresh token is bound to a different DPoP key"));
}
#[cfg(feature = "mtls")]
if record.x5t_s256.as_deref() != bound.cred.certificate.map(|c| c.thumbprint()) {
self.restore_refresh_token(record).await?;
return Err(
ErrorResponse::new(ErrorCode::InvalidGrant).with_description(
"this refresh token is bound to a different client certificate",
),
);
}
if let Some(expires_at) = record.expires_at {
if self.clock.now() >= expires_at {
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("refresh token chain expired"));
}
}
if !record.scope.is_subset(&client.allowed_scopes) {
return Err(
ErrorResponse::new(ErrorCode::InvalidScope).with_description(
"this grant carries scopes the client's registration no longer allows",
),
);
}
let scope = match requested_scope {
None => record.scope.clone(),
Some(s) if s.is_subset(&record.scope) => s.clone(),
Some(_) => {
self.restore_refresh_token(record).await?;
return Err(ErrorResponse::new(ErrorCode::InvalidScope)
.with_description("refresh may narrow scope, never widen it"));
}
};
let narrowed = self
.narrow_and_permit(&record.resource, requested_resources)
.and_then(|r| {
GrantedDetails::of_refresh(&record)
.narrow(&requested_details)
.map(|d| (r, d))
});
let (resource, details) = match narrowed {
Ok(narrowed) => narrowed,
Err(e) => {
self.restore_refresh_token(record).await?;
return Err(e);
}
};
let chain_expires_at = record.expires_at;
let subject = record.subject.clone();
let family_id = record.family_id.clone();
let authentication = GrantedAuthentication::from_refresh(&record);
let grant_established_at = record.grant_established_at;
let spent = RefreshTokenRecord {
state: RefreshTokenState::Spent,
expires_at: chain_expires_at.or_else(|| {
Some(saturating_deadline(
self.clock.now(),
self.config.refresh_reuse_window,
))
}),
..record
};
if self
.store
.put_refresh_token(spent)
.await
.map_err(storage_error)?
.is_refused()
{
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("the grant was revoked while this token was being refreshed"));
}
self.issue_boxed(
&client,
bound,
GrantType::RefreshToken,
grant_established_at,
subject,
scope,
resource,
details,
Some(RefreshChain {
family_id,
expires_at: chain_expires_at,
}),
true,
authentication,
GrantedActor::default(),
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
fn issue_boxed<'a>(
&'a self,
client: &'a Client,
bound: &'a Bound<'_>,
grant_type: GrantType,
grant_established_at: SystemTime,
subject: Option<String>,
scope: ScopeSet,
resource: Vec<String>,
details: GrantedDetails,
chain: Option<RefreshChain>,
allow_refresh: bool,
authentication: GrantedAuthentication,
actor: GrantedActor,
lifetime_ceiling: Option<SystemTime>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<TokenResponse, ErrorResponse>> + Send + 'a>,
> {
Box::pin(self.issue(
client,
bound,
grant_type,
grant_established_at,
subject,
scope,
resource,
details,
chain,
allow_refresh,
authentication,
actor,
lifetime_ceiling,
))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn issue(
&self,
client: &Client,
bound: &Bound<'_>,
grant_type: GrantType,
grant_established_at: SystemTime,
subject: Option<String>,
scope: ScopeSet,
resource: Vec<String>,
details: GrantedDetails,
chain: Option<RefreshChain>,
allow_refresh: bool,
authentication: GrantedAuthentication,
actor: GrantedActor,
lifetime_ceiling: Option<SystemTime>,
) -> Result<TokenResponse, ErrorResponse> {
#[cfg(not(feature = "dpop"))]
let _ = bound;
#[cfg(not(feature = "consent"))]
let _ = authentication;
#[cfg(not(feature = "token-exchange"))]
let _ = actor;
#[cfg(not(feature = "rar"))]
let _ = details;
let now = self.clock.now();
let expires_at = {
let ordinary = saturating_deadline(now, self.config.access_token_ttl);
match lifetime_ceiling {
Some(ceiling) => ordinary.min(ceiling),
None => ordinary,
}
};
let issues_refresh = allow_refresh
&& self.config.issue_refresh_tokens
&& client.allows_grant(GrantType::RefreshToken);
let (family_id, access_token, pending_refresh) = {
let mut entropy = [0u8; 80];
getrandom::fill(&mut entropy).map_err(|_| randomness_error())?;
let family_id = match (&chain, issues_refresh) {
(Some(c), _) => Some(c.family_id.clone()),
(None, true) => Some(hex_encode(&entropy[..16])),
(None, false) => None,
};
(
family_id,
hex_encode(&entropy[16..48]),
issues_refresh.then(|| hex_encode(&entropy[48..])),
)
};
let audit = self
.hooks
.is_observed()
.then(|| Box::new((subject.clone(), family_id.clone())));
#[cfg(feature = "jwt")]
let prepared = self.access_token_signing_input(
client,
subject.as_deref(),
&scope,
&resource,
&details,
now,
expires_at,
access_token,
bound,
&actor,
&authentication,
)?;
#[cfg(feature = "jwt")]
let access_token = match prepared {
Err(opaque) => opaque,
Ok((jwt, input)) => jwt.finish_signing(input).await.map_err(|e| {
let _ = e;
ErrorResponse::new(ErrorCode::ServerError)
})?,
};
let refused = self
.store
.put_token(IssuedToken {
#[cfg(feature = "dpop")]
jkt: bound.jkt.map(Box::from),
#[cfg(feature = "mtls")]
x5t_s256: bound.cred.certificate.map(|c| Box::new(*c.thumbprint())),
access_token: access_token.clone(),
client_id: client.client_id.clone(),
subject: subject.clone(),
scope: scope.clone(),
resource: resource.clone(),
#[cfg(feature = "rar")]
authorization_details: details.clone().into_details(),
issued_at: now,
grant_established_at,
expires_at,
family_id: family_id.clone(),
#[cfg(feature = "token-exchange")]
act: actor.act.clone(),
#[cfg(feature = "consent")]
authentication: authentication.authentication.clone(),
})
.await
.map_err(storage_error)?;
if refused.is_refused() {
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("the grant was revoked while this token was being issued"));
}
let refresh_token = if issues_refresh {
let expires_at = match &chain {
Some(c) => c.expires_at,
None => self
.config
.refresh_token_ttl
.map(|ttl| saturating_deadline(now, ttl)),
};
let rt = pending_refresh.expect("issues_refresh decided both");
let refused = self
.store
.put_refresh_token(RefreshTokenRecord {
#[cfg(feature = "dpop")]
jkt: bound.jkt.map(Box::from),
#[cfg(feature = "mtls")]
x5t_s256: bound.cred.certificate.map(|c| Box::new(*c.thumbprint())),
refresh_token: rt.clone(),
client_id: client.client_id.clone(),
subject,
scope: scope.clone(),
resource,
#[cfg(feature = "rar")]
authorization_details: details.clone().into_details(),
expires_at,
grant_established_at,
family_id: family_id.unwrap_or_default(),
state: RefreshTokenState::Active,
#[cfg(feature = "consent")]
authentication: authentication.authentication,
})
.await
.map_err(storage_error)?;
if refused.is_refused() {
self.undo_issuance(&access_token).await;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant)
.with_description("the grant was revoked while this token was being issued"));
}
Some(rt)
} else {
None
};
if let Some(audit) = &audit {
self.hooks.emit(|| Event::TokenIssued {
client_id: client.client_id.as_str(),
grant_type,
subject: audit.0.as_deref(),
scope: &scope,
family_id: audit.1.as_deref(),
refresh_issued: refresh_token.is_some(),
});
}
Ok(TokenResponse {
access_token,
#[cfg(feature = "dpop")]
token_type: match bound.jkt {
Some(_) => TokenType::Dpop,
None => TokenType::Bearer,
},
#[cfg(not(feature = "dpop"))]
token_type: TokenType::Bearer,
expires_in: expires_at.duration_since(now).unwrap_or_default().as_secs(),
refresh_token,
scope: (!scope.is_empty()).then(|| scope.to_string()),
#[cfg(feature = "rar")]
authorization_details: details.into_details(),
})
}
pub async fn introspect(
&self,
access_token: &str,
) -> Result<Option<std::sync::Arc<IssuedToken>>, StorageError> {
Ok(self
.store
.get_token(access_token)
.await?
.filter(|t| self.clock.now() < t.expires_at))
}
pub async fn introspection_response(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
token: &str,
) -> Result<IntrospectionResponse, ErrorResponse> {
self.introspection_response_with_credential(
client_id,
&ClientCredential::secret(client_secret),
token,
)
.await
}
pub async fn introspection_response_with_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
token: &str,
) -> Result<IntrospectionResponse, ErrorResponse> {
let client = self.authenticate_client(client_id, cred).await?;
if matches!(client.auth, crate::client::ClientAuth::Public) {
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::NotConfidential,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
let record = self.introspect(token).await.map_err(storage_error)?;
let view = record
.as_ref()
.and_then(|t| self.introspection_view(&client, t));
Ok(match (record, view) {
(Some(t), Some(view)) => IntrospectionResponse {
active: true,
scope: (!t.scope.is_empty()).then(|| t.scope.to_string()),
client_id: Some(t.client_id.as_str().to_string()),
sub: t.subject.clone(),
#[cfg(feature = "dpop")]
token_type: Some(match t.jkt {
Some(_) => TokenType::Dpop,
None => TokenType::Bearer,
}),
#[cfg(not(feature = "dpop"))]
token_type: Some(TokenType::Bearer),
exp: unix_seconds(t.expires_at),
iat: unix_seconds(t.issued_at),
iss: Some(self.issuer_identifier().to_string()),
aud: match &view {
IntrospectionView::OwningClient => {
(!t.resource.is_empty()).then(|| t.resource.clone())
}
IntrospectionView::ResourceServer(mine) => Some(mine.clone()),
},
#[cfg(feature = "consent")]
auth_time: t
.authentication
.as_ref()
.and_then(|a| unix_seconds(a.auth_time)),
#[cfg(feature = "consent")]
acr: t
.authentication
.as_ref()
.and_then(|a| a.acr.as_deref().map(str::to_string)),
#[cfg(feature = "rar")]
authorization_details: match &view {
IntrospectionView::OwningClient => t.authorization_details.clone(),
IntrospectionView::ResourceServer(mine) => {
details_for_resource_server(&t.authorization_details, mine)
}
},
#[cfg(any(feature = "dpop", feature = "mtls"))]
cnf: {
let cnf = crate::token::Confirmation {
#[cfg(feature = "dpop")]
jkt: t.jkt.as_deref().map(str::to_string),
#[cfg(feature = "mtls")]
x5t_s256: t.x5t_s256.as_deref().copied(),
};
(!cnf.is_empty()).then_some(cnf)
},
#[cfg(feature = "token-exchange")]
act: t.act.as_deref().cloned(),
},
_ => IntrospectionResponse::inactive(),
})
}
fn introspection_view(
&self,
client: &Client,
token: &IssuedToken,
) -> Option<IntrospectionView> {
if token.client_id == client.client_id {
return Some(IntrospectionView::OwningClient);
}
let mine: Vec<String> = self
.config
.resource_servers
.as_deref()
.unwrap_or(&[])
.iter()
.filter(|rs| rs.client_id == client.client_id)
.flat_map(|rs| rs.resources.iter())
.filter(|id| token.resource.iter().any(|r| r == *id))
.map(|id| id.to_string())
.fold(Vec::new(), |mut acc, id| {
if !acc.contains(&id) {
acc.push(id);
}
acc
});
(!mine.is_empty()).then_some(IntrospectionView::ResourceServer(mine))
}
#[cfg(feature = "consent")]
pub async fn record_consent(
&self,
client_id: &ClientId,
subject: &str,
scope: &ScopeSet,
resource: &[String],
authentication: Option<crate::consent::Authentication>,
) -> Result<crate::consent::ConsentRecord, StorageError> {
if subject.is_empty() {
return Err(StorageError::new(
"a consent must name a subject; an empty subject cannot be withdrawn",
));
}
let now = self.clock.now();
let mut started_as_widen = None;
for attempt in 0..2 {
let existing = self.store.find_consent(client_id, subject).await?;
let expected = existing.as_deref().cloned();
match started_as_widen {
None => started_as_widen = Some(expected.is_some()),
Some(true) if expected.is_none() => {
return Err(StorageError::new(
"the consent was withdrawn while it was being recorded",
))
}
Some(_) => {}
}
let _ = attempt;
let mut record = match &expected {
Some(existing) => existing.clone(),
None => crate::consent::ConsentRecord {
consent_id: try_random_hex(16)
.ok_or_else(|| {
StorageError::new(
"the OS would not provide randomness for a consent id",
)
})?
.into_boxed_str(),
client_id: client_id.clone(),
subject: subject.into(),
scope: ScopeSet::empty(),
resource: Vec::new(),
granted_at: now,
authentication: None,
},
};
record.extend(scope, resource);
if let Some(a) = &authentication {
record.authentication = Some(Box::new(a.clone()));
}
if self
.store
.compare_and_swap_consent(expected.as_ref(), record.clone())
.await?
{
return Ok(record);
}
}
Err(StorageError::new(
"consent record changed concurrently twice; not recorded",
))
}
#[cfg(feature = "consent")]
pub async fn remembered_consent(
&self,
client_id: &ClientId,
subject: &str,
) -> Result<Option<std::sync::Arc<crate::consent::ConsentRecord>>, StorageError> {
self.store.find_consent(client_id, subject).await
}
#[cfg(feature = "consent")]
pub async fn consents_for_subject(
&self,
subject: &str,
) -> Result<Vec<std::sync::Arc<crate::consent::ConsentRecord>>, StorageError> {
self.store.consents_for_subject(subject).await
}
#[cfg(feature = "consent")]
pub async fn withdraw_consent(&self, consent_id: &str) -> Result<u64, StorageError> {
let record = self.store.get_consent(consent_id).await?;
let records_revoked = self
.store
.revoke_consent(consent_id, self.revocation_window())
.await?;
if let Some(record) = &record {
self.hooks.emit(|| Event::ConsentWithdrawn {
client_id: record.client_id.as_str(),
subject: record.subject.as_ref(),
records_revoked,
});
}
Ok(records_revoked)
}
pub async fn revoke(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
token: &str,
token_type_hint: Option<TokenTypeHint>,
) -> Result<(), ErrorResponse> {
self.revoke_with_credential(
client_id,
&ClientCredential::secret(client_secret),
token,
token_type_hint,
)
.await
}
pub async fn revoke_with_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
token: &str,
token_type_hint: Option<TokenTypeHint>,
) -> Result<(), ErrorResponse> {
let client = self.authenticate_client(client_id, cred).await?;
let try_refresh = || async {
match self.store.get_refresh_token(token).await {
Ok(Some(record)) if record.client_id == client.client_id => {
let cascade_failed = self
.store
.revoke_token_family(record.family_id.as_str(), self.revocation_window())
.await
.is_err();
self.store
.take_refresh_token(token)
.await
.map_err(storage_error)?;
self.hooks.emit(|| Event::TokenRevoked {
client_id: client.client_id.as_str(),
token_type: TokenTypeHint::RefreshToken,
cascade_failed,
});
Ok(true)
}
Ok(_) => Ok(false),
Err(e) => Err(storage_error(e)),
}
};
let try_access = || async {
match self.store.get_token(token).await {
Ok(Some(t)) if t.client_id == client.client_id => {
self.store
.delete_token(token)
.await
.map_err(storage_error)?;
self.hooks.emit(|| Event::TokenRevoked {
client_id: client.client_id.as_str(),
token_type: TokenTypeHint::AccessToken,
cascade_failed: false,
});
Ok(true)
}
Ok(_) => Ok(false),
Err(e) => Err(storage_error(e)),
}
};
match token_type_hint {
Some(TokenTypeHint::AccessToken) => {
if !try_access().await? {
try_refresh().await?;
}
}
_ => {
if !try_refresh().await? {
try_access().await?;
}
}
}
Ok(())
}
}
#[cfg(test)]
#[path = "tests/server.rs"]
mod tests;