use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use crate::client::ClientId;
use crate::error::{ErrorCode, ErrorResponse};
use crate::scope::ScopeSet;
pub const MAX_CONSENT_RESOURCES: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Authentication {
pub auth_time: SystemTime,
pub acr: Option<Box<str>>,
}
impl Authentication {
pub fn at(auth_time: SystemTime) -> Self {
Authentication {
auth_time,
acr: None,
}
}
pub fn with_acr(mut self, acr: &str) -> Self {
self.acr = Some(acr.into());
self
}
pub fn age(&self, now: SystemTime) -> Option<Duration> {
now.duration_since(self.auth_time).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentRecord {
pub consent_id: Box<str>,
pub client_id: ClientId,
pub subject: Box<str>,
pub scope: ScopeSet,
pub resource: Vec<String>,
pub granted_at: SystemTime,
pub authentication: Option<Box<Authentication>>,
}
impl ConsentRecord {
pub fn covers(&self, scope: &ScopeSet, resource: &[String]) -> bool {
scope.is_subset(&self.scope) && resource.iter().all(|r| self.resource.contains(r))
}
pub fn extend(&mut self, scope: &ScopeSet, resource: &[String]) {
if !scope.is_subset(&self.scope) {
let merged = self
.scope
.iter()
.chain(scope.iter())
.map(|s| s.as_str())
.collect::<Vec<&str>>();
if let Ok(widened) = ScopeSet::from_tokens(merged) {
self.scope = widened;
}
}
let room = MAX_CONSENT_RESOURCES.saturating_sub(self.resource.len());
self.resource.reserve(resource.len().min(room));
for r in resource {
if self.resource.len() >= MAX_CONSENT_RESOURCES {
break;
}
if !self.resource.contains(r) {
self.resource.push(r.clone());
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AuthenticationRequirement {
pub acr_values: Vec<Box<str>>,
pub max_age: Option<Duration>,
}
impl AuthenticationRequirement {
pub fn none() -> Self {
AuthenticationRequirement::default()
}
pub fn from_pairs<I, K, V>(pairs: I) -> Result<Self, ErrorResponse>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let mut acr_values = None;
let mut max_age = None;
for (k, v) in pairs {
match k.as_ref() {
"acr_values" if acr_values.is_none() => acr_values = Some(v),
"max_age" if max_age.is_none() => max_age = Some(v),
_ => {}
}
}
AuthenticationRequirement::from_raw(
acr_values.as_ref().map(AsRef::as_ref),
max_age.as_ref().map(AsRef::as_ref),
)
}
pub fn from_request(
request: &crate::authorization::AuthorizationRequest<'_>,
) -> Result<Self, ErrorResponse> {
AuthenticationRequirement::from_raw(
request.acr_values.as_deref(),
request.max_age.as_deref(),
)
}
fn from_raw(acr_values: Option<&str>, max_age: Option<&str>) -> Result<Self, ErrorResponse> {
let mut out = AuthenticationRequirement::none();
if let Some(raw) = acr_values {
out.acr_values = Vec::with_capacity(raw.bytes().filter(|b| *b == b' ').count() + 1);
out.acr_values.extend(
raw.split(' ')
.filter(|s| !s.is_empty())
.map(Box::<str>::from),
);
}
if let Some(raw) = max_age {
let secs: u64 = raw.parse().map_err(|_| {
ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("max_age must be a non-negative number of seconds")
})?;
out.max_age = Some(Duration::from_secs(secs));
}
Ok(out)
}
pub fn is_empty(&self) -> bool {
self.acr_values.is_empty() && self.max_age.is_none()
}
pub fn satisfied_by(
&self,
authentication: Option<&Authentication>,
now: SystemTime,
) -> Result<(), StepUpFailure> {
if self.is_empty() {
return Ok(());
}
let authentication = match authentication {
Some(a) => a,
None => return Err(StepUpFailure::NotReported),
};
if let Some(max_age) = self.max_age {
if authentication.age(now).unwrap_or_default() > max_age {
return Err(StepUpFailure::Stale);
}
}
if !self.acr_values.is_empty() {
match &authentication.acr {
Some(acr) => {
if !self
.acr_values
.iter()
.any(|want| want.as_ref() == acr.as_ref())
{
return Err(StepUpFailure::AcrNotMet);
}
}
None => return Err(StepUpFailure::AcrNotMet),
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StepUpFailure {
NotReported,
Stale,
AcrNotMet,
}
impl StepUpFailure {
pub fn description(self) -> &'static str {
match self {
StepUpFailure::NotReported => "no user authentication was reported for this request",
StepUpFailure::Stale => "the user authentication is older than the requested max_age",
StepUpFailure::AcrNotMet => {
"the user authentication does not satisfy the requested acr_values"
}
}
}
pub fn error_response(self) -> ErrorResponse {
ErrorResponse::new(ErrorCode::InsufficientUserAuthentication)
.with_description(self.description())
}
}
impl std::fmt::Display for StepUpFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
StepUpFailure::NotReported => "no authentication reported",
StepUpFailure::Stale => "authentication older than max_age",
StepUpFailure::AcrNotMet => "acr_values not satisfied",
})
}
}
impl std::error::Error for StepUpFailure {}
impl From<StepUpFailure> for ErrorResponse {
fn from(failure: StepUpFailure) -> ErrorResponse {
failure.error_response()
}
}
pub fn step_up_challenge(
scheme: &str,
acr_values: &[Box<str>],
max_age: Option<Duration>,
) -> String {
use std::fmt::Write as _;
const ERROR: &str = " error=\"insufficient_user_authentication\"";
const DESCRIPTION: &str =
", error_description=\"the user authentication does not meet the requirements of this \
resource\"";
let acr_len: usize = acr_values.iter().map(|a| a.len() + 3).sum();
let mut out = String::with_capacity(
scheme.len() + ERROR.len() + DESCRIPTION.len() + acr_len + max_age.map_or(0, |_| 32),
);
out.push_str(scheme);
out.push_str(ERROR);
out.push_str(DESCRIPTION);
if !acr_values.is_empty() {
out.push_str(", acr_values=\"");
for (i, acr) in acr_values.iter().enumerate() {
if i > 0 {
out.push(' ');
}
push_quoted(&mut out, acr);
}
out.push('"');
}
if let Some(max_age) = max_age {
let _ = write!(out, ", max_age=\"{}\"", max_age.as_secs());
}
out
}
fn push_quoted(out: &mut String, value: &str) {
for c in value.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
}
#[cfg(test)]
#[path = "tests/consent.rs"]
mod tests;