use http::StatusCode;
use crate::nuts_auth::presentation::Mismatch;
pub const TEXT_LIMIT: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidInput {
#[error(
"the authorization server is not an https URL (or http on a loopback host) without userinfo, query or fragment (RFC 8414 §2), in canonical form"
)]
AuthorizationServer,
#[error("the scope is not a space-delimited list of RFC 6749 §3.3 scope tokens")]
Scope,
#[error("the client_id is empty or holds a control character")]
ClientId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Step {
Metadata,
Definition,
Token,
}
impl std::fmt::Display for Step {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Metadata => "the authorization server metadata",
Self::Definition => "the presentation definition endpoint",
Self::Token => "the token endpoint",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum MetadataError {
#[error("the metadata names another issuer (RFC 8414 §3.3)")]
Issuer,
#[error("the metadata names no token_endpoint URL")]
TokenEndpoint,
#[error("the metadata names no presentation_definition_endpoint URL (Nuts RFC021 §5)")]
DefinitionEndpoint,
#[error("an endpoint of the metadata is not on the issuer's origin")]
OtherOrigin,
#[error("an endpoint of the metadata carries userinfo or a fragment")]
InsecureEndpoint,
#[error(
"the metadata's vp_formats admits no jwt_vp signed with {algorithm} (Nuts RFC021 §3.1)"
)]
PresentationFormat {
algorithm: String,
},
#[error(
"the metadata's dpop_signing_alg_values_supported does not list {algorithm} (RFC 9449 §5.1)"
)]
ProofAlgorithm {
algorithm: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Malformation {
#[error("the answer exceeds {limit} bytes")]
TooLarge {
limit: usize,
},
#[error("the answer is not application/json")]
MediaType,
#[error("the answer is not JSON (line {line}, column {column})")]
NotJson {
line: usize,
column: usize,
},
#[error("an object of the answer repeats a name (line {line}, column {column})")]
RepeatedName {
line: usize,
column: usize,
},
#[error("the answer is not of the shape the step defines (line {line}, column {column})")]
Shape {
line: usize,
column: usize,
},
}
impl Malformation {
pub(super) fn of(error: &serde_json::Error) -> Self {
let (line, column) = (error.line(), error.column());
if error.is_data() {
Self::Shape { line, column }
} else {
Self::NotJson { line, column }
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("the DPoP proof could not be made")]
pub struct ProofError(#[source] Box<dyn std::error::Error + Send + Sync>);
impl ProofError {
pub fn new(source: impl std::error::Error + Send + Sync + 'static) -> Self {
Self(Box::new(source))
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NutsAuthError {
#[error("{step} did not answer within the timeout")]
Timeout {
step: Step,
},
#[error("{step} could not be reached")]
Transport {
step: Step,
#[source]
source: reqwest::Error,
},
#[error("{step} answered {status}")]
Status {
step: Step,
status: StatusCode,
},
#[error("{step} gave a malformed answer")]
Malformed {
step: Step,
#[source]
malformation: Malformation,
},
#[error("the authorization server metadata does not support the grant")]
Metadata(#[from] MetadataError),
#[error("the holder's credentials do not answer the presentation definition")]
Definition(#[from] Mismatch),
#[error("the credential answering {descriptor:?} has expired")]
Expired {
descriptor: String,
},
#[error("the verifiable presentation could not be signed")]
Sign(#[source] jsonwebtoken::errors::Error),
#[error("the presentation submission could not be written")]
Submission(#[source] serde_json::Error),
#[error(transparent)]
Proof(#[from] ProofError),
#[error(
"the token endpoint refused the request with {status}: {error}{}",
description.as_deref().map(|text| format!(" ({text})")).unwrap_or_default()
)]
Refused {
status: StatusCode,
error: String,
description: Option<String>,
},
#[error("the token endpoint issued a token of type {token_type}, not DPoP")]
TokenType {
token_type: String,
},
}
impl NutsAuthError {
pub(super) fn transport(step: Step, error: reqwest::Error) -> Self {
let error = error.without_url();
if error.is_timeout() {
Self::Timeout { step }
} else {
Self::Transport {
step,
source: error,
}
}
}
}
pub(super) fn bounded(text: &str) -> String {
text.chars().take(TEXT_LIMIT).collect()
}