1use axum::Json;
6use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use cheers_core::Scope;
9use cheers_server::{
10 AuditValidationError, CampAuthorityError, OwnershipValidationError, ServicePrincipalError,
11};
12use serde::Serialize;
13
14#[derive(Debug, thiserror::Error)]
19#[non_exhaustive]
20pub enum RouteError {
21 #[error("missing csrf cookie")]
23 MissingCsrfCookie,
24
25 #[error("csrf cookie / state mismatch")]
28 CsrfStateMismatch,
29
30 #[error("unknown or already-consumed flow")]
33 UnknownFlow,
34
35 #[error("flow expired")]
38 FlowExpired,
39
40 #[error("provider error: {0}")]
42 Provider(String),
43
44 #[error("id_token verification failed: {0}")]
46 IdToken(String),
47
48 #[error("upstream http: {0}")]
50 Upstream(String),
51
52 #[error("store: {0}")]
55 Store(String),
56
57 #[error("config: {0}")]
60 Config(String),
61
62 #[error("malformed callback body: {0}")]
64 MalformedCallback(String),
65
66 #[error("ceremony failed: {0}")]
70 Ceremony(String),
71
72 #[error("unknown credential")]
76 UnknownCredential,
77
78 #[error("token already used")]
80 AlreadyUsed,
81
82 #[error("invalid email")]
84 InvalidEmail,
85
86 #[error("wrong token purpose: {0}")]
89 WrongPurpose(String),
90
91 #[error("magic-link token invalid: {0}")]
93 MagicLinkToken(String),
94
95 #[error("mailer build: {0}")]
98 MailerBuild(String),
99
100 #[error("mailer transport: {0}")]
103 MailerTransport(String),
104
105 #[error("missing bearer token")]
107 MissingBearer,
108
109 #[error("malformed bearer token")]
112 MalformedBearer,
113
114 #[error("unauthorized")]
118 Unauthorized,
119
120 #[error("unknown device")]
123 UnknownDevice,
124
125 #[error("unknown token")]
130 UnknownToken,
131
132 #[error("unentitled_scopes: {0}")]
137 UnentitledScopes(String),
138
139 #[error("aud_not_entitled: {0}")]
146 NotEntitledForAud(String),
147
148 #[error("invalid_token_request: {0}")]
153 InvalidTokenRequest(String),
154
155 #[error("insufficient scope: required '{required}'")]
159 InsufficientScope { required: Scope },
160
161 #[error("ownership_invalid: {0}")]
166 OwnershipInvalid(#[from] OwnershipValidationError),
167
168 #[error("invalid ownership query: {0}")]
171 InvalidOwnershipQuery(String),
172
173 #[error("unknown ownership")]
178 UnknownOwnership,
179
180 #[error("grant forbidden")]
184 GrantForbidden,
185
186 #[error("audit_invalid: {0}")]
193 AuditInvalid(#[from] AuditValidationError),
194
195 #[error("not permitted to read this audit subject")]
203 AuditSubjectForbidden,
204
205 #[error("invalid audit query: {0}")]
211 InvalidAuditQuery(String),
212
213 #[error("already exists: {0}")]
216 AlreadyExists(String),
217
218 #[error("unknown principal: {0}")]
221 UnknownPrincipal(String),
222
223 #[error("not an operator")]
230 NotOperator,
231
232 #[error("invalid delegation: {0}")]
238 InvalidDelegation(String),
239}
240
241impl RouteError {
242 pub fn status_and_code(&self) -> (StatusCode, &'static str) {
244 match self {
245 RouteError::MissingCsrfCookie => (StatusCode::BAD_REQUEST, "missing_csrf_cookie"),
246 RouteError::CsrfStateMismatch => (StatusCode::BAD_REQUEST, "csrf_state_mismatch"),
247 RouteError::UnknownFlow => (StatusCode::BAD_REQUEST, "unknown_flow"),
248 RouteError::FlowExpired => (StatusCode::BAD_REQUEST, "flow_expired"),
249 RouteError::Provider(_) => (StatusCode::BAD_REQUEST, "provider_error"),
250 RouteError::IdToken(_) => (StatusCode::UNAUTHORIZED, "id_token_invalid"),
251 RouteError::Upstream(_) => (StatusCode::BAD_GATEWAY, "upstream_error"),
252 RouteError::Store(_) => (StatusCode::INTERNAL_SERVER_ERROR, "store_error"),
253 RouteError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "config_error"),
254 RouteError::MalformedCallback(_) => {
255 (StatusCode::BAD_REQUEST, "malformed_callback")
256 }
257 RouteError::Ceremony(_) => (StatusCode::BAD_REQUEST, "ceremony_failed"),
258 RouteError::UnknownCredential => (StatusCode::UNAUTHORIZED, "unknown_credential"),
259 RouteError::AlreadyUsed => (StatusCode::BAD_REQUEST, "already_used"),
260 RouteError::InvalidEmail => (StatusCode::BAD_REQUEST, "invalid_email"),
261 RouteError::WrongPurpose(_) => (StatusCode::BAD_REQUEST, "wrong_purpose"),
262 RouteError::MagicLinkToken(_) => (StatusCode::BAD_REQUEST, "magic_link_token"),
263 RouteError::MailerBuild(_) => (StatusCode::INTERNAL_SERVER_ERROR, "mailer_build"),
264 RouteError::MailerTransport(_) => (StatusCode::BAD_GATEWAY, "mailer_transport"),
265 RouteError::MissingBearer => (StatusCode::UNAUTHORIZED, "missing_bearer"),
266 RouteError::MalformedBearer => (StatusCode::UNAUTHORIZED, "malformed_bearer"),
267 RouteError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
268 RouteError::UnknownDevice => (StatusCode::NOT_FOUND, "unknown_device"),
269 RouteError::UnknownToken => (StatusCode::NOT_FOUND, "unknown_token"),
270 RouteError::UnentitledScopes(_) => (StatusCode::BAD_REQUEST, "unentitled_scopes"),
271 RouteError::NotEntitledForAud(_) => (StatusCode::FORBIDDEN, "aud_not_entitled"),
272 RouteError::InvalidTokenRequest(_) => {
273 (StatusCode::BAD_REQUEST, "invalid_token_request")
274 }
275 RouteError::InsufficientScope { .. } => {
276 (StatusCode::FORBIDDEN, "insufficient_scope")
277 }
278 RouteError::OwnershipInvalid(_) => (StatusCode::BAD_REQUEST, "ownership_invalid"),
279 RouteError::InvalidOwnershipQuery(_) => {
280 (StatusCode::BAD_REQUEST, "invalid_ownership_query")
281 }
282 RouteError::UnknownOwnership => (StatusCode::NOT_FOUND, "unknown_ownership"),
283 RouteError::GrantForbidden => (StatusCode::FORBIDDEN, "grant_forbidden"),
284 RouteError::AuditInvalid(_) => (StatusCode::BAD_REQUEST, "audit_invalid"),
285 RouteError::AuditSubjectForbidden => {
286 (StatusCode::FORBIDDEN, "audit_subject_forbidden")
287 }
288 RouteError::InvalidAuditQuery(_) => (StatusCode::BAD_REQUEST, "invalid_audit_query"),
289 RouteError::AlreadyExists(_) => (StatusCode::CONFLICT, "already_exists"),
290 RouteError::UnknownPrincipal(_) => (StatusCode::NOT_FOUND, "unknown_principal"),
291 RouteError::NotOperator => (StatusCode::FORBIDDEN, "not_operator"),
292 RouteError::InvalidDelegation(_) => (StatusCode::BAD_REQUEST, "invalid_delegation"),
293 }
294 }
295}
296
297#[derive(Serialize)]
298struct RouteErrorBody<'a> {
299 error: &'a str,
300 message: String,
301}
302
303impl IntoResponse for RouteError {
304 fn into_response(self) -> Response {
305 let (status, code) = self.status_and_code();
306 let body = RouteErrorBody {
310 error: code,
311 message: self.to_string(),
312 };
313 tracing::warn!(error = code, message = %self, "cheers-axum route error");
314 (status, Json(body)).into_response()
315 }
316}
317
318impl From<cheers_core::StoreError> for RouteError {
319 fn from(value: cheers_core::StoreError) -> Self {
320 RouteError::Store(value.to_string())
321 }
322}
323
324impl From<cheers_core::Error> for RouteError {
325 fn from(value: cheers_core::Error) -> Self {
326 RouteError::Store(value.to_string())
329 }
330}
331
332impl From<CampAuthorityError> for RouteError {
333 fn from(value: CampAuthorityError) -> Self {
334 match value {
342 CampAuthorityError::AlreadyExists(id) => RouteError::AlreadyExists(id.to_string()),
343 CampAuthorityError::InvalidDelegation(e) => RouteError::InvalidDelegation(e.to_string()),
344 CampAuthorityError::DelegationMismatch(msg) => {
345 RouteError::InvalidDelegation(msg.to_owned())
346 }
347 CampAuthorityError::DelegationExpired => {
348 RouteError::InvalidDelegation("delegation expired".into())
349 }
350 CampAuthorityError::UntrustedSigningKey(_)
351 | CampAuthorityError::BadSignature => RouteError::Unauthorized,
352 CampAuthorityError::Store(e) => RouteError::Store(e.to_string()),
353 other => RouteError::Store(other.to_string()),
354 }
355 }
356}
357
358impl From<ServicePrincipalError> for RouteError {
359 fn from(value: ServicePrincipalError) -> Self {
360 match value {
365 ServicePrincipalError::AlreadyExists(id) => RouteError::AlreadyExists(id.to_string()),
366 ServicePrincipalError::UnknownPrincipal(id) => {
367 RouteError::UnknownPrincipal(id.to_string())
368 }
369 ServicePrincipalError::Store(e) => RouteError::Store(e.to_string()),
370 other => RouteError::Store(other.to_string()),
371 }
372 }
373}
374
375#[cfg(any(feature = "google", feature = "apple"))]
376impl From<cheers::providers::oidc_generic::OidcError> for RouteError {
377 fn from(value: cheers::providers::oidc_generic::OidcError) -> Self {
378 use cheers::providers::oidc_generic::OidcError;
379 match value {
380 OidcError::UnknownFlow => RouteError::UnknownFlow,
381 OidcError::FlowExpired => RouteError::FlowExpired,
382 OidcError::StateMismatch => RouteError::CsrfStateMismatch,
383 OidcError::Http(msg) => RouteError::Upstream(msg),
384 OidcError::IdToken(msg) => RouteError::IdToken(msg),
385 OidcError::MissingIdToken => RouteError::IdToken("missing id_token".into()),
386 OidcError::Discovery(msg) | OidcError::Config(msg) => RouteError::Config(msg),
387 OidcError::Store(msg) => RouteError::Store(msg),
388 other => RouteError::Config(other.to_string()),
391 }
392 }
393}
394
395#[cfg(feature = "passkey")]
396impl From<cheers::passkey::PasskeyError> for RouteError {
397 fn from(value: cheers::passkey::PasskeyError) -> Self {
398 use cheers::passkey::PasskeyError;
399 match value {
400 PasskeyError::Config(e) => RouteError::Config(e.to_string()),
401 PasskeyError::Ceremony(e) => RouteError::Ceremony(e.to_string()),
402 PasskeyError::Serialize(e) => RouteError::Store(format!("passkey serialize: {e}")),
406 PasskeyError::Deserialize(e) => {
407 RouteError::Store(format!("passkey deserialize: {e}"))
408 }
409 PasskeyError::WrongBinding { found } => {
410 RouteError::Store(format!("stored credential binding {found:?} is not passkey"))
411 }
412 other => RouteError::Config(other.to_string()),
413 }
414 }
415}
416
417#[cfg(feature = "email")]
418impl From<cheers::email::magic_link::MagicLinkError> for RouteError {
419 fn from(value: cheers::email::magic_link::MagicLinkError) -> Self {
420 use cheers::email::magic_link::MagicLinkError;
421 match value {
422 MagicLinkError::Codec(e) => RouteError::MagicLinkToken(e.to_string()),
423 MagicLinkError::WrongPurpose { got } => RouteError::WrongPurpose(got),
424 MagicLinkError::AlreadyUsed => RouteError::AlreadyUsed,
425 MagicLinkError::InvalidEmail => RouteError::InvalidEmail,
426 MagicLinkError::Store(msg) => RouteError::Store(msg),
427 other => RouteError::Config(other.to_string()),
428 }
429 }
430}
431
432#[cfg(feature = "email")]
433impl From<cheers::email::MailerError> for RouteError {
434 fn from(value: cheers::email::MailerError) -> Self {
435 use cheers::email::MailerError;
436 match value {
437 MailerError::Build(msg) => RouteError::MailerBuild(msg),
438 MailerError::Transport(msg) => RouteError::MailerTransport(msg),
439 other => RouteError::MailerTransport(other.to_string()),
440 }
441 }
442}
443
444#[cfg(feature = "apple")]
445impl From<cheers::providers::apple::AppleRedirectError> for RouteError {
446 fn from(value: cheers::providers::apple::AppleRedirectError) -> Self {
447 use cheers::providers::apple::AppleRedirectError;
448 match value {
449 AppleRedirectError::Oidc(e) => e.into(),
450 AppleRedirectError::MissingFormField(f) => {
451 RouteError::MalformedCallback(format!("missing field `{f}`"))
452 }
453 AppleRedirectError::Provider(msg) => RouteError::Provider(msg),
454 AppleRedirectError::ClientSecret(e) => RouteError::Config(e.to_string()),
455 other => RouteError::Config(other.to_string()),
459 }
460 }
461}