#[cfg(feature = "jar")]
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
#[cfg(feature = "jar")]
use base64::Engine as _;
#[cfg(feature = "par")]
use serde::{Deserialize, Serialize};
use crate::authorization::{
AuthorizationError, AuthorizationRequest, ValidatedAuthorizationRequest,
};
use crate::client::ClientId;
use crate::error::{ErrorCode, ErrorResponse};
use crate::server::{AuthorizationServer, Clock};
use crate::store::Storage;
#[cfg(feature = "par")]
fn pushed_at_default() -> std::time::SystemTime {
std::time::SystemTime::UNIX_EPOCH
}
#[cfg(feature = "par")]
pub const MIN_REQUEST_URI_TTL: std::time::Duration = std::time::Duration::from_secs(1);
#[cfg(feature = "par")]
pub const REQUEST_URI_PREFIX: &str = "urn:ietf:params:oauth:request_uri:";
#[cfg(feature = "par")]
const REQUEST_URI_ENTROPY_BYTES: usize = 32;
#[cfg(feature = "par")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParConfig {
pub pushed_authorization_request_endpoint: Option<String>,
pub request_uri_ttl: std::time::Duration,
pub require_pushed_authorization_requests: bool,
}
#[cfg(feature = "par")]
impl Default for ParConfig {
fn default() -> Self {
ParConfig::new()
}
}
#[cfg(feature = "par")]
impl ParConfig {
pub fn new() -> Self {
ParConfig {
pushed_authorization_request_endpoint: None,
request_uri_ttl: std::time::Duration::from_secs(60),
require_pushed_authorization_requests: false,
}
}
pub fn endpoint(&self, issuer: &str) -> String {
match &self.pushed_authorization_request_endpoint {
Some(url) => url.clone(),
None => format!("{}/par", issuer.trim_end_matches('/')),
}
}
}
#[cfg(feature = "par")]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PushedAuthorizationResponse {
pub request_uri: String,
pub expires_in: u64,
}
#[cfg(feature = "par")]
impl std::fmt::Debug for PushedAuthorizationResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PushedAuthorizationResponse")
.field("request_uri", &"[redacted]")
.field("expires_in", &self.expires_in)
.finish()
}
}
#[cfg(feature = "par")]
impl PushedAuthorizationResponse {
pub fn http_status(&self) -> u16 {
201
}
}
#[cfg(feature = "par")]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PushedAuthorizationRequest {
pub request_uri: String,
pub client_id: ClientId,
pub response_type: Option<String>,
pub redirect_uri: Option<String>,
pub scope: Option<String>,
pub state: Option<String>,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
pub resource: Vec<String>,
#[cfg(feature = "rar")]
pub authorization_details: Option<String>,
#[cfg(feature = "consent")]
pub acr_values: Option<String>,
#[cfg(feature = "consent")]
pub max_age: Option<String>,
#[serde(default = "pushed_at_default")]
pub pushed_at: std::time::SystemTime,
pub expires_at: std::time::SystemTime,
}
#[cfg(feature = "par")]
impl std::fmt::Debug for PushedAuthorizationRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = f.debug_struct("PushedAuthorizationRequest");
out.field("request_uri", &"[redacted]")
.field("pushed_at", &self.pushed_at)
.field("client_id", &self.client_id)
.field("response_type", &self.response_type)
.field("redirect_uri", &self.redirect_uri)
.field("scope", &self.scope)
.field("state", &self.state)
.field("code_challenge", &self.code_challenge)
.field("code_challenge_method", &self.code_challenge_method)
.field("resource", &self.resource);
#[cfg(feature = "rar")]
out.field("authorization_details", &self.authorization_details);
#[cfg(feature = "consent")]
out.field("acr_values", &self.acr_values)
.field("max_age", &self.max_age);
out.field("expires_at", &self.expires_at).finish()
}
}
#[cfg(feature = "par")]
impl PushedAuthorizationRequest {
pub fn new(
request_uri: impl Into<String>,
client_id: ClientId,
expires_at: std::time::SystemTime,
) -> Self {
PushedAuthorizationRequest {
pushed_at: std::time::SystemTime::UNIX_EPOCH,
request_uri: request_uri.into(),
client_id,
response_type: None,
redirect_uri: None,
scope: None,
state: None,
code_challenge: None,
code_challenge_method: None,
resource: Vec::new(),
#[cfg(feature = "rar")]
authorization_details: None,
#[cfg(feature = "consent")]
acr_values: None,
#[cfg(feature = "consent")]
max_age: None,
expires_at,
}
}
pub fn as_request(&self) -> AuthorizationRequest<'_> {
AuthorizationRequest {
response_type: self.response_type.as_deref().map(Into::into),
client_id: Some(self.client_id.as_str().into()),
redirect_uri: self.redirect_uri.as_deref().map(Into::into),
scope: self.scope.as_deref().map(Into::into),
state: self.state.as_deref().map(Into::into),
code_challenge: self.code_challenge.as_deref().map(Into::into),
code_challenge_method: self.code_challenge_method.as_deref().map(Into::into),
resource: self.resource.iter().map(|r| r.as_str().into()).collect(),
#[cfg(feature = "rar")]
authorization_details: self.authorization_details.as_deref().map(Into::into),
#[cfg(not(feature = "rar"))]
authorization_details: None,
#[cfg(feature = "consent")]
acr_values: self.acr_values.as_deref().map(Into::into),
#[cfg(feature = "consent")]
max_age: self.max_age.as_deref().map(Into::into),
}
}
}
#[cfg(feature = "jar")]
pub const REQUEST_OBJECT_SIGNING_ALGS: &[&str] = &["ES256"];
#[cfg(feature = "jar")]
pub const REQUEST_OBJECT_TYP: &str = "oauth-authz-req+jwt";
#[cfg(feature = "jar")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct JarConfig {
pub require_signed_request_object: bool,
pub max_request_object_lifetime: std::time::Duration,
}
#[cfg(feature = "jar")]
impl JarConfig {
pub fn new() -> Self {
JarConfig::default()
}
}
#[cfg(feature = "jar")]
impl Default for JarConfig {
fn default() -> Self {
JarConfig {
require_signed_request_object: false,
max_request_object_lifetime: std::time::Duration::from_secs(300),
}
}
}
#[cfg(feature = "jar")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequestObjectAlg {
Es256,
}
#[cfg(feature = "jar")]
impl RequestObjectAlg {
pub fn as_str(self) -> &'static str {
match self {
RequestObjectAlg::Es256 => "ES256",
}
}
}
#[cfg(feature = "jar")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestObjectKeyError(String);
#[cfg(feature = "jar")]
impl RequestObjectKeyError {
pub fn detail(&self) -> &str {
&self.0
}
}
#[cfg(feature = "jar")]
impl std::fmt::Display for RequestObjectKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "request object key error: {}", self.0)
}
}
#[cfg(feature = "jar")]
impl std::error::Error for RequestObjectKeyError {}
#[cfg(feature = "jar")]
#[derive(Clone, PartialEq, Eq)]
pub struct RegisteredRequestObjectKey {
alg: RequestObjectAlg,
kid: Option<String>,
key: crate::jwt::PublicJwk,
}
#[cfg(feature = "jar")]
impl std::fmt::Debug for RegisteredRequestObjectKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisteredRequestObjectKey")
.field("alg", &self.alg)
.field("kid", &self.kid)
.finish()
}
}
#[cfg(feature = "jar")]
impl RegisteredRequestObjectKey {
pub fn es256_from_jwk_coordinates(
kid: Option<String>,
x: &str,
y: &str,
) -> Result<Self, RequestObjectKeyError> {
let x = URL_SAFE_NO_PAD
.decode(x)
.map_err(|_| RequestObjectKeyError("x is not base64url".into()))?;
let y = URL_SAFE_NO_PAD
.decode(y)
.map_err(|_| RequestObjectKeyError("y is not base64url".into()))?;
Ok(RegisteredRequestObjectKey {
alg: RequestObjectAlg::Es256,
kid,
key: public_jwk(&x, &y)?,
})
}
pub fn es256_from_sec1(
kid: Option<String>,
sec1: &[u8],
) -> Result<Self, RequestObjectKeyError> {
if sec1.len() != 65 {
return Err(RequestObjectKeyError(
"an uncompressed P-256 point is exactly 65 bytes".into(),
));
}
if sec1[0] != 0x04 {
return Err(RequestObjectKeyError(
"an uncompressed P-256 point begins with 0x04".into(),
));
}
Ok(RegisteredRequestObjectKey {
alg: RequestObjectAlg::Es256,
kid,
key: public_jwk(&sec1[1..33], &sec1[33..])?,
})
}
pub fn alg(&self) -> RequestObjectAlg {
self.alg
}
pub fn kid(&self) -> Option<&str> {
self.kid.as_deref()
}
}
#[cfg(feature = "jar")]
pub trait RequestObjectKeys: Send + Sync {
fn registered_key(&self, client_id: &ClientId) -> Option<RegisteredRequestObjectKey>;
}
#[cfg(feature = "jar")]
#[derive(Debug)]
struct RequestObjectClaims {
client_id: String,
response_type: Option<String>,
redirect_uri: Option<String>,
scope: Option<String>,
state: Option<String>,
code_challenge: Option<String>,
code_challenge_method: Option<String>,
resource: Vec<String>,
#[cfg(feature = "rar")]
authorization_details: Option<String>,
#[cfg(feature = "consent")]
acr_values: Option<String>,
#[cfg(feature = "consent")]
max_age: Option<String>,
}
#[cfg(feature = "jar")]
impl RequestObjectClaims {
fn as_request(&self) -> AuthorizationRequest<'_> {
AuthorizationRequest {
response_type: self.response_type.as_deref().map(Into::into),
client_id: Some(self.client_id.as_str().into()),
redirect_uri: self.redirect_uri.as_deref().map(Into::into),
scope: self.scope.as_deref().map(Into::into),
state: self.state.as_deref().map(Into::into),
code_challenge: self.code_challenge.as_deref().map(Into::into),
code_challenge_method: self.code_challenge_method.as_deref().map(Into::into),
resource: self.resource.iter().map(|r| r.as_str().into()).collect(),
#[cfg(feature = "rar")]
authorization_details: self.authorization_details.as_deref().map(Into::into),
#[cfg(not(feature = "rar"))]
authorization_details: None,
#[cfg(feature = "consent")]
acr_values: self.acr_values.as_deref().map(Into::into),
#[cfg(feature = "consent")]
max_age: self.max_age.as_deref().map(Into::into),
}
}
}
#[cfg(feature = "jar")]
fn decode_segment(segment: &str, refusal: &'static str) -> Result<Vec<u8>, ErrorResponse> {
URL_SAFE_NO_PAD
.decode(segment)
.map_err(|_| ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(refusal))
}
#[cfg(feature = "jar")]
const HEADER_NOT_BASE64URL: &str = "the header is not base64url";
#[cfg(feature = "jar")]
const PAYLOAD_NOT_BASE64URL: &str = "the payload is not base64url";
#[cfg(feature = "jar")]
const SIGNATURE_NOT_BASE64URL: &str = "the signature is not base64url";
#[cfg(feature = "jar")]
fn public_jwk(x: &[u8], y: &[u8]) -> Result<crate::jwt::PublicJwk, RequestObjectKeyError> {
if x.len() != 32 || y.len() != 32 {
return Err(RequestObjectKeyError(
"a P-256 coordinate is exactly 32 bytes".into(),
));
}
crate::jwt::PublicJwk::from_coordinates(&URL_SAFE_NO_PAD.encode(x), &URL_SAFE_NO_PAD.encode(y))
.map_err(|_| RequestObjectKeyError("a P-256 coordinate is exactly 32 bytes".into()))
}
#[cfg(feature = "jar")]
fn string_claim(
claims: &serde_json::Map<String, serde_json::Value>,
name: &str,
) -> Result<Option<String>, ErrorResponse> {
match claims.get(name) {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
Some(_) => Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description(format!("the {name} claim must be a JSON string"))),
}
}
impl<S: Storage, C: Clock> AuthorizationServer<S, C> {
#[cfg(feature = "par")]
pub async fn pushed_authorization_request(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
parameters: &[(&str, &str)],
) -> Result<PushedAuthorizationResponse, ErrorResponse> {
self.pushed_authorization_request_with_credential(
client_id,
&crate::server::ClientCredential::secret(client_secret),
parameters,
)
.await
}
#[cfg(feature = "par")]
pub async fn pushed_authorization_request_with_credential(
&self,
client_id: &ClientId,
credential: &crate::server::ClientCredential<'_>,
parameters: &[(&str, &str)],
) -> Result<PushedAuthorizationResponse, ErrorResponse> {
if self.config().par.is_none() {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("this server does not offer pushed authorization requests"));
}
let pushed_at = self.now();
let client = self.authenticate_client(client_id, credential).await?;
if parameters.iter().any(|(name, _)| *name == "request_uri") {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("request_uri must not be pushed (RFC 9126 s2.1)"));
}
#[cfg(feature = "jar")]
if let Some((_, object)) = parameters.iter().find(|(name, _)| *name == "request") {
let claims = self.verified_request_object(&client.client_id, object)?;
return self
.store_pushed_request(&client.client_id, &claims.as_request(), pushed_at)
.await;
}
#[cfg(feature = "jar")]
if matches!(&self.config().jar, Some(jar) if jar.require_signed_request_object) {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"this server acts only on signed request objects (RFC 9101 s10.5), and this push \
carried none",
));
}
let request = AuthorizationRequest::from_pairs(parameters.iter().copied());
self.store_pushed_request(&client.client_id, &request, pushed_at)
.await
}
#[cfg(feature = "par")]
async fn store_pushed_request(
&self,
authenticated: &ClientId,
request: &AuthorizationRequest<'_>,
pushed_at: std::time::SystemTime,
) -> Result<PushedAuthorizationResponse, ErrorResponse> {
match request.client_id.as_deref() {
Some(pushed) if pushed == authenticated.as_str() => {}
Some(_) => {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
"client_id does not match the authenticated client (RFC 9126 s2.1)",
),
)
}
None => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("client_id is required (RFC 9126 s2.1)"))
}
}
self.validate_direct_authorization_request(request)
.await
.map_err(|e| match e {
AuthorizationError::Direct(error) => error,
AuthorizationError::Redirect(redirect) => redirect.error,
})?;
let ttl = match &self.config().par {
Some(par) => par.request_uri_ttl.max(MIN_REQUEST_URI_TTL),
None => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
.with_description("this server does not offer pushed authorization requests"))
}
};
let now = self.now();
let request_uri = match crate::server::try_random_hex(REQUEST_URI_ENTROPY_BYTES) {
Some(hex) => format!("{REQUEST_URI_PREFIX}{hex}"),
None => return Err(ErrorResponse::new(ErrorCode::ServerError)),
};
let expires_at = crate::server::saturating_deadline(now, ttl);
let record = PushedAuthorizationRequest {
pushed_at,
request_uri: request_uri.clone(),
client_id: authenticated.clone(),
response_type: request.response_type.as_deref().map(str::to_string),
redirect_uri: request.redirect_uri.as_deref().map(str::to_string),
scope: request.scope.as_deref().map(str::to_string),
state: request.state.as_deref().map(str::to_string),
code_challenge: request.code_challenge.as_deref().map(str::to_string),
code_challenge_method: request.code_challenge_method.as_deref().map(str::to_string),
resource: request.resource.iter().map(|r| r.to_string()).collect(),
#[cfg(feature = "rar")]
authorization_details: request.authorization_details.as_deref().map(str::to_string),
#[cfg(feature = "consent")]
acr_values: request.acr_values.as_deref().map(str::to_string),
#[cfg(feature = "consent")]
max_age: request.max_age.as_deref().map(str::to_string),
expires_at,
};
let stored = self
.store()
.put_pushed_authorization_request(record)
.await
.map_err(|e| {
let _ = e;
ErrorResponse::new(ErrorCode::ServerError)
})?;
if stored.is_refused() {
return Err(ErrorResponse::new(ErrorCode::InvalidClient)
.with_description("this client was deleted while its request was being pushed"));
}
Ok(PushedAuthorizationResponse {
request_uri,
expires_in: expires_at
.duration_since(now)
.unwrap_or(MIN_REQUEST_URI_TTL)
.as_secs(),
})
}
#[cfg(feature = "par")]
pub async fn validate_pushed_authorization_request(
&self,
client_id: &str,
request_uri: &str,
) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
let direct = |code: ErrorCode, why: &'static str| {
AuthorizationError::Direct(ErrorResponse::new(code).with_description(why))
};
let record = self
.store()
.take_pushed_authorization_request(request_uri)
.await
.map_err(|e| {
let _ = e;
AuthorizationError::Direct(ErrorResponse::new(ErrorCode::ServerError))
})?
.ok_or_else(|| {
direct(
ErrorCode::InvalidRequestUri,
"unknown, expired or already used request_uri",
)
})?;
if record.client_id.as_str() != client_id {
let _restored = self
.store()
.put_pushed_authorization_request(record)
.await
.map_err(|e| {
let _ = e;
AuthorizationError::Direct(ErrorResponse::new(ErrorCode::ServerError))
})?;
return Err(direct(
ErrorCode::InvalidRequestUri,
"request_uri was not issued to this client",
));
}
if self.now() >= record.expires_at {
return Err(direct(
ErrorCode::InvalidRequestUri,
"request_uri has expired",
));
}
self.validate_direct_authorization_request(&record.as_request())
.await
}
#[cfg(feature = "jar")]
pub async fn validate_signed_authorization_request(
&self,
client_id: &str,
request_object: &str,
) -> 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)",
),
));
}
let claims = self
.verified_request_object(&ClientId::new(client_id), request_object)
.map_err(AuthorizationError::Direct)?;
self.validate_direct_authorization_request(&claims.as_request())
.await
}
#[cfg(feature = "jar")]
fn verified_request_object(
&self,
client_id: &ClientId,
request_object: &str,
) -> Result<RequestObjectClaims, ErrorResponse> {
if self.config().jar.is_none() {
return Err(ErrorResponse::new(ErrorCode::RequestNotSupported)
.with_description("this server does not accept signed request objects"));
}
let keys = self.hooks().request_object_keys().ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("no request object verification keys are installed")
})?;
let registered = keys.registered_key(client_id).ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the client registered no request object key")
})?;
let verifier = self.es256_verifier().ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("no ES256 verifier is installed")
})?;
let mut parts = request_object.split('.');
let (header_b64, payload_b64, signature_b64) =
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(h), Some(p), Some(s), None) => (h, p, s),
_ => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("not a three part JWS compact serialization"))
}
};
let header: serde_json::Value =
serde_json::from_slice(&decode_segment(header_b64, HEADER_NOT_BASE64URL)?).map_err(
|_| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the header is not JSON")
},
)?;
let alg = header
.get("alg")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the header has no alg")
})?;
match header.get("crit") {
None => {}
Some(serde_json::Value::Array(names)) => {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
if names.is_empty() {
"the header has an empty crit, which RFC 7515 s4.1.11 forbids"
.to_string()
} else {
"the header's crit names an extension this server does not implement"
.to_string()
},
),
)
}
Some(_) => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the header's crit is not an array"))
}
}
if alg != registered.alg.as_str() {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("alg does not match the algorithm registered for this client"));
}
if let Some(presented) = header.get("kid").and_then(serde_json::Value::as_str) {
match registered.kid() {
Some(kid) if kid == presented => {}
_ => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description(
"kid does not identify a key registered for this client",
))
}
}
}
if let Some(typ) = header.get("typ").and_then(serde_json::Value::as_str) {
if typ != REQUEST_OBJECT_TYP && typ != "JWT" && typ != "jwt" {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("typ names a JWT that is not a request object"));
}
}
let signature = decode_segment(signature_b64, SIGNATURE_NOT_BASE64URL)?;
let signing_input = &request_object.as_bytes()[..header_b64.len() + 1 + payload_b64.len()];
if !verifier.verify(®istered.key, signing_input, &signature) {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the signature did not verify"));
}
let payload: serde_json::Value =
serde_json::from_slice(&decode_segment(payload_b64, PAYLOAD_NOT_BASE64URL)?).map_err(
|_| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the payload is not JSON")
},
)?;
let claims = payload.as_object().ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the payload is not a JSON object")
})?;
for forbidden in ["request", "request_uri"] {
if claims.contains_key(forbidden) {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
"a request object must not carry request or request_uri (RFC 9101 s4)",
),
);
}
}
match string_claim(claims, "client_id")? {
Some(claimed) if claimed == client_id.as_str() => {}
_ => return Err(
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
"the client_id claim does not match the request's client_id (RFC 9101 s6.3)",
),
),
}
if let Some(aud) = payload.get("aud") {
let issuer = self.issuer_identifier();
let addressed_here = match aud {
serde_json::Value::String(one) => one == issuer,
serde_json::Value::Array(many) => many
.iter()
.any(|v| v.as_str().map(|s| s == issuer).unwrap_or(false)),
_ => false,
};
if !addressed_here {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("aud does not name this authorization server"));
}
}
let numeric_date = |name: &str| -> Result<Option<f64>, ErrorResponse> {
match payload.get(name) {
None => Ok(None),
Some(v) => v.as_f64().map(Some).ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(format!(
"the request object's {name} is not a NumericDate (RFC 7519 s2)"
))
}),
}
};
let now_secs = crate::server::unix_seconds(self.now()).ok_or_else(|| {
ErrorResponse::new(ErrorCode::ServerError)
.with_description("the server clock is outside the representable range")
})? as f64;
let exp = numeric_date("exp")?.ok_or_else(|| {
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
"the request object has no exp, so it would authorize its request for as long as \
the client's key stays registered",
)
})?;
if now_secs >= exp {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the request object has expired"));
}
let ceiling = self
.config()
.jar
.as_ref()
.map(|j| j.max_request_object_lifetime)
.unwrap_or_else(|| std::time::Duration::from_secs(300));
if exp - now_secs > ceiling.as_secs() as f64 {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
"the request object's remaining lifetime exceeds what this server accepts",
),
);
}
if let Some(nbf) = numeric_date("nbf")? {
if now_secs + crate::skew::CLOCK_SKEW_LEEWAY.as_secs() as f64 <= nbf {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the request object is not yet valid"));
}
}
#[cfg(not(feature = "rar"))]
if claims.contains_key("authorization_details") {
return Err(ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
.with_description("this server does not support authorization_details"));
}
let resource = match claims.get("resource") {
None => Vec::new(),
Some(serde_json::Value::String(one)) => vec![one.clone()],
Some(serde_json::Value::Array(many)) => {
let mut out = Vec::with_capacity(many.len());
for value in many {
match value.as_str() {
Some(s) => out.push(s.to_string()),
None => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description(
"every resource claim entry must be a JSON string",
))
}
}
}
out
}
Some(_) => {
return Err(
ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
"the resource claim must be a string or an array of strings",
),
)
}
};
Ok(RequestObjectClaims {
client_id: client_id.as_str().to_string(),
response_type: string_claim(claims, "response_type")?,
redirect_uri: string_claim(claims, "redirect_uri")?,
scope: string_claim(claims, "scope")?,
state: string_claim(claims, "state")?,
code_challenge: string_claim(claims, "code_challenge")?,
code_challenge_method: string_claim(claims, "code_challenge_method")?,
resource,
#[cfg(feature = "rar")]
authorization_details: match claims.get("authorization_details") {
None => None,
Some(value @ serde_json::Value::Array(_)) => Some(value.to_string()),
Some(_) => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the authorization_details claim must be a JSON array"))
}
},
#[cfg(feature = "consent")]
acr_values: string_claim(claims, "acr_values")?,
#[cfg(feature = "consent")]
max_age: match claims.get("max_age") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.clone()),
Some(serde_json::Value::Number(n)) => match n.as_u64() {
Some(secs) => Some(secs.to_string()),
None => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description(
"the max_age claim must be a non-negative number of seconds",
))
}
},
Some(_) => {
return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
.with_description("the max_age claim must be a JSON string or number"))
}
},
})
}
}
#[cfg(test)]
#[path = "tests/par.rs"]
mod tests;