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 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>>,
pub scopes_supported: Option<Vec<String>>,
pub allowed_resources: Option<Box<[Box<str>]>>,
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 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;
#[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,
}
impl<'a> UserApproval<'a> {
pub fn granted(request: &'a ValidatedAuthorizationRequest, subject: impl Into<String>) -> Self {
UserApproval {
request,
subject: subject.into(),
}
}
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,
scopes_supported: None,
allowed_resources: 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,
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(Debug, Clone, Copy, Default, PartialEq, Eq)]
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<'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)]
pub struct TokenRequestContext<'a> {
pub credential: ClientCredential<'a>,
pub resources: &'a [String],
#[cfg(feature = "rar")]
pub authorization_details: Option<&'a str>,
#[cfg(feature = "dpop")]
pub dpop_proof: Option<&'a str>,
}
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 random_hex(n_bytes: usize) -> String {
let mut buf = vec![0u8; n_bytes];
getrandom::fill(&mut buf).expect("OS randomness for OAuth artifacts");
hex_encode(&buf)
}
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) -> String {
let mut out = String::with_capacity(len);
let mut buf = [0u8; 64];
while out.len() < len {
getrandom::fill(&mut buf).expect("OS randomness for OAuth artifacts");
for &byte in buf.iter() {
if out.len() == len {
break;
}
if let Some(symbol) = user_code_symbol(byte) {
out.push(symbol as char);
}
}
}
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()
}
#[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,
jti: String,
bound: &Bound<'_>,
) -> 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,
};
let claims = AccessTokenClaims {
iss: self.issuer_identifier().to_string(),
exp: crate::jwt::unix_seconds(now + self.config.access_token_ttl)
.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(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)
},
};
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)",
),
);
}
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"));
}
}
if !out.iter().any(|kept: &String| kept == value) {
out.push(value.to_string());
}
}
Ok(out)
}
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 fn store(&self) -> &S {
&self.store
}
pub async fn register_client(&self, client: Client) -> Result<(), StorageError> {
self.store.put_client(client).await
}
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(),
};
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));
}
let found = self
.store
.get_client(client_id)
.await
.map_err(storage_error)?;
let client = match found {
Some(client) => client,
None => {
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::UnknownClient,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
};
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 {
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::SecretExpired,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
}
}
#[cfg(feature = "client_assertion")]
if cred.client_assertion.is_some() {
return match self.authenticate_by_assertion(&client, cred).await {
Ok(()) => {
self.hooks.record(attempt, AttemptOutcome::Succeeded);
Ok(client)
}
Err(error) => {
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::AssertionInvalid,
});
Err(error)
}
};
}
#[cfg(feature = "mtls")]
if matches!(client.auth, crate::client::ClientAuth::Mtls { .. }) {
return match crate::mtls::verify_certificate(&client, cred) {
Ok(()) => {
self.hooks.record(attempt, AttemptOutcome::Succeeded);
Ok(client)
}
Err(failure) => {
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure,
});
Err(ErrorResponse::new(ErrorCode::InvalidClient))
}
};
}
if !client
.auth
.verify_with(cred.client_secret, self.hooks.secret_verifier())
{
self.hooks.record(attempt, AttemptOutcome::Failed);
self.hooks.emit(|| Event::ClientAuthenticationFailed {
client_id: client_id.as_str(),
failure: ClientAuthFailure::SecretMismatch,
});
return Err(ErrorResponse::new(ErrorCode::InvalidClient));
}
self.hooks.record(attempt, AttemptOutcome::Succeeded);
Ok(client)
}
#[cfg(feature = "client_assertion")]
async fn authenticate_by_assertion(
&self,
client: &Client,
cred: &ClientCredential<'_>,
) -> Result<(), ErrorResponse> {
let refused = || ErrorResponse::new(ErrorCode::InvalidClient);
let assertion = cred.client_assertion.ok_or_else(refused)?;
if cred.client_assertion_type != Some(CLIENT_ASSERTION_TYPE) {
return Err(refused());
}
if cred.client_secret.is_some() {
return Err(refused());
}
let keys = match &client.auth {
crate::client::ClientAuth::ConfidentialAssertion { keys } => keys,
_ => return Err(refused()),
};
let verified = verify_assertion(
self.es256_verifier(),
keys,
assertion,
client.client_id.as_str(),
&[self.token_endpoint(), self.issuer_identifier()],
self.clock.now(),
)
.map_err(|_| refused())?;
let claimed = self
.store
.claim_replay_id(
&replay_key("ca", client.client_id.as_str(), &verified.jti),
verified.expires_at,
)
.await
.map_err(|_| refused())?;
if !claimed {
return Err(refused());
}
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(|| {
ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("no ES256 verifier is installed")
})?;
let verified = verify_proof(
verifier,
proof,
"POST",
self.token_endpoint(),
self.clock.now(),
)
.map_err(|_| 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 {
return Err(ErrorResponse::new(ErrorCode::InvalidDpopProof)
.with_description("this DPoP proof has already been used"));
}
Ok(Some(verified.jkt.into_boxed_str()))
}
fn resolve_scope(
client: &Client,
requested: Option<&ScopeSet>,
) -> Result<ScopeSet, ErrorResponse> {
match requested {
None => Ok(client.default_scopes.clone()),
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 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 = random_hex(32);
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: now,
expires_at: 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, 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);
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"))]
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 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 = 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,
"client has no registered redirect_uri",
))
}
_ => {
return Err(direct(
ErrorCode::InvalidRequest,
"redirect_uri is required when several are registered",
))
}
},
};
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 => client.default_scopes.clone(),
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(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,
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> {
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 } = approval;
let now = self.clock.now();
let code = random_hex(32);
let record = AuthorizationCodeRecord {
code: code.clone(),
client_id: request.client_id.clone(),
redirect_uri: request.redirect_uri.clone(),
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: now + self.config.authorization_code_ttl,
state: AuthorizationCodeState::Issued,
#[cfg(feature = "consent")]
authentication: authentication.authentication,
};
self.store
.put_authorization_code(record)
.await
.map_err(|_| {
AuthorizationError::Redirect(AuthorizationErrorRedirect {
redirect_uri: request.redirect_uri.clone(),
error: ErrorResponse::new(ErrorCode::ServerError),
state: request.state.clone(),
iss: request.issuer.clone(),
})
})?;
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 AuthorizationCodeState::Consumed {
access_token,
refresh_token,
} = &record.state
{
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).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;
}
}
}
if self.store.put_authorization_code(record).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"));
}
match redirect_uri {
Some(u) if u == record.redirect_uri => {}
_ => {
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::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_resources(&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,
Some(subject),
scope,
resource,
details,
None,
true,
authentication,
)
.await?;
consumed.state = AuthorizationCodeState::Consumed {
access_token: Some(issued.access_token.clone()),
refresh_token: issued.refresh_token.clone(),
};
if self.store.put_authorization_code(consumed).await.is_err() {
let _ = self.store.delete_token(&issued.access_token).await;
if let Some(rt) = &issued.refresh_token {
let _ = self.store.take_refresh_token(rt).await;
}
return 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 client = self.authenticate_client(client_id, &bound.cred).await?;
if matches!(client.auth, crate::client::ClientAuth::Public) {
return Err(ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("client_credentials requires a confidential client"));
}
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,
None,
scope,
resource,
details,
None,
false,
GrantedAuthentication::default(),
)
.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 {
if now < last + grant.interval {
let expected = grant.state.clone();
grant.interval += 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,
Some(subject),
taken.scope,
Vec::new(),
GrantedDetails::default(),
None,
true,
GrantedAuthentication::default(),
)
.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.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
return Err(ErrorResponse::new(ErrorCode::InvalidGrant));
}
if record.state == RefreshTokenState::Spent {
let records_revoked = self
.store
.revoke_token_family(&record.family_id)
.await
.map_err(storage_error)?;
self.hooks.emit(|| Event::RefreshTokenReuseDetected {
client_id: client.client_id.as_str(),
family_id: &record.family_id,
records_revoked,
});
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.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
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.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
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"));
}
}
let scope = match requested_scope {
None => record.scope.clone(),
Some(s) if s.is_subset(&record.scope) => s.clone(),
Some(_) => {
self.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
return Err(ErrorResponse::new(ErrorCode::InvalidScope)
.with_description("refresh may narrow scope, never widen it"));
}
};
let narrowed =
Self::narrow_resources(&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.store
.put_refresh_token(record)
.await
.map_err(storage_error)?;
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 spent = RefreshTokenRecord {
state: RefreshTokenState::Spent,
expires_at: chain_expires_at
.or_else(|| Some(self.clock.now() + self.config.refresh_reuse_window)),
..record
};
self.store
.put_refresh_token(spent)
.await
.map_err(storage_error)?;
self.issue_boxed(
&client,
bound,
GrantType::RefreshToken,
subject,
scope,
resource,
details,
Some(RefreshChain {
family_id,
expires_at: chain_expires_at,
}),
true,
authentication,
)
.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,
subject: Option<String>,
scope: ScopeSet,
resource: Vec<String>,
details: GrantedDetails,
chain: Option<RefreshChain>,
allow_refresh: bool,
authentication: GrantedAuthentication,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<TokenResponse, ErrorResponse>> + Send + 'a>,
> {
Box::pin(self.issue(
client,
bound,
grant_type,
subject,
scope,
resource,
details,
chain,
allow_refresh,
authentication,
))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn issue(
&self,
client: &Client,
bound: &Bound<'_>,
grant_type: GrantType,
subject: Option<String>,
scope: ScopeSet,
resource: Vec<String>,
details: GrantedDetails,
chain: Option<RefreshChain>,
allow_refresh: bool,
authentication: GrantedAuthentication,
) -> Result<TokenResponse, ErrorResponse> {
#[cfg(not(feature = "dpop"))]
let _ = bound;
#[cfg(not(feature = "consent"))]
let _ = authentication;
#[cfg(not(feature = "rar"))]
let _ = details;
let now = self.clock.now();
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).expect("OS randomness for OAuth artifacts");
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,
access_token,
bound,
)?;
#[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)
})?,
};
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,
expires_at: now + self.config.access_token_ttl,
family_id: family_id.clone(),
#[cfg(feature = "consent")]
authentication: authentication.authentication.clone(),
})
.await
.map_err(storage_error)?;
let refresh_token = if issues_refresh {
let expires_at = match &chain {
Some(c) => c.expires_at,
None => self.config.refresh_token_ttl.map(|ttl| now + ttl),
};
let rt = pending_refresh.expect("issues_refresh decided both");
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,
family_id: family_id.unwrap_or_default(),
state: RefreshTokenState::Active,
#[cfg(feature = "consent")]
authentication: authentication.authentication,
})
.await
.map_err(storage_error)?;
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: self.config.access_token_ttl.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) {
return Err(ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("introspection requires a confidential client"));
}
let record = self.introspect(token).await.map_err(storage_error)?;
Ok(match record {
Some(t) if t.client_id == client.client_id => 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: (!t.resource.is_empty()).then(|| t.resource.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: t.authorization_details.clone(),
#[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)
},
},
_ => IntrospectionResponse::inactive(),
})
}
#[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> {
let now = self.clock.now();
let mut record = match self.store.find_consent(client_id, subject).await? {
Some(existing) => (*existing).clone(),
None => crate::consent::ConsentRecord {
consent_id: random_hex(16).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));
}
self.store.put_consent(record.clone()).await?;
Ok(record)
}
#[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).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 => {
self.store
.take_refresh_token(token)
.await
.map_err(storage_error)?;
let cascade_failed = self
.store
.revoke_token_family(&record.family_id)
.await
.is_err();
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;