Skip to main content

jacquard_oauth/
error.rs

1use jacquard_common::session::SessionStoreError;
2use miette::Diagnostic;
3#[cfg(feature = "scope-check")]
4use smol_str::SmolStr;
5
6use crate::request::RequestError;
7use crate::resolver::ResolverError;
8
9/// High-level errors emitted by OAuth helpers.
10#[derive(Debug, thiserror::Error, Diagnostic)]
11#[non_exhaustive]
12pub enum OAuthError {
13    /// An error occurred during identity or metadata resolution.
14    #[error(transparent)]
15    #[diagnostic(code(jacquard_oauth::resolver))]
16    Resolver(#[from] ResolverError),
17
18    /// An error occurred while making an OAuth HTTP request.
19    #[error(transparent)]
20    #[diagnostic(code(jacquard_oauth::request))]
21    Request(#[from] RequestError),
22
23    /// An error occurred reading or writing session state.
24    #[error(transparent)]
25    #[diagnostic(code(jacquard_oauth::storage))]
26    Storage(#[from] SessionStoreError),
27
28    /// An error occurred during DPoP proof generation or validation.
29    #[error(transparent)]
30    #[diagnostic(code(jacquard_oauth::dpop))]
31    Dpop(#[from] crate::dpop::DpopError),
32
33    /// An error occurred with the client's key set.
34    #[error(transparent)]
35    #[diagnostic(code(jacquard_oauth::keyset))]
36    Keyset(#[from] crate::keyset::Error),
37
38    /// An ATProto-specific OAuth error (e.g. scope validation, client ID).
39    #[error(transparent)]
40    #[diagnostic(code(jacquard_oauth::atproto))]
41    Atproto(#[from] crate::atproto::Error),
42
43    /// An error occurred managing or refreshing an OAuth session.
44    #[error(transparent)]
45    #[diagnostic(code(jacquard_oauth::session))]
46    Session(#[from] crate::session::Error),
47
48    /// A JSON serialization or deserialization error.
49    #[error(transparent)]
50    #[diagnostic(code(jacquard_oauth::serde_json))]
51    SerdeJson(#[from] serde_json::Error),
52
53    /// A URI parse error.
54    #[error(transparent)]
55    #[diagnostic(code(jacquard_oauth::url))]
56    Url(#[from] jacquard_common::deps::fluent_uri::ParseError),
57
58    /// A form (URL-encoded) serialization error.
59    #[error(transparent)]
60    #[diagnostic(code(jacquard_oauth::form))]
61    Form(#[from] serde_html_form::ser::Error),
62
63    /// Invalid OAuth helper input.
64    #[error("invalid OAuth request: {0}")]
65    #[diagnostic(code(jacquard_oauth::invalid_request))]
66    InvalidRequest(String),
67
68    /// An error validating an authorization callback.
69    #[error(transparent)]
70    #[diagnostic(code(jacquard_oauth::callback))]
71    Callback(#[from] CallbackError),
72
73    /// An error occurred checking request scope permissions.
74    #[cfg(feature = "scope-check")]
75    #[error(transparent)]
76    #[diagnostic(transparent)]
77    ScopeCheck(#[from] ScopeError),
78}
79
80/// Typed callback validation errors (redirect handling).
81#[derive(Debug, thiserror::Error, Diagnostic)]
82#[non_exhaustive]
83pub enum CallbackError {
84    /// The `state` parameter was absent from the authorization callback.
85    ///
86    /// State is required to prevent CSRF attacks per RFC 6749 ยง10.12.
87    #[error("missing state parameter in callback")]
88    #[diagnostic(code(jacquard_oauth::callback::missing_state))]
89    MissingState,
90    /// The `iss` (issuer) parameter was absent from the authorization callback.
91    ///
92    /// RFC 9207 requires `iss` to be present so that clients can reject
93    /// mix-up attacks from malicious authorization servers.
94    #[error("missing `iss` parameter")]
95    #[diagnostic(code(jacquard_oauth::callback::missing_iss))]
96    MissingIssuer,
97    /// The issuer in the callback did not match the expected authorization server.
98    #[error("issuer mismatch: expected {expected}, got {got}")]
99    #[diagnostic(code(jacquard_oauth::callback::issuer_mismatch))]
100    IssuerMismatch {
101        /// The issuer that was expected.
102        expected: String,
103        /// The issuer that was actually present in the callback.
104        got: String,
105    },
106    /// The authorization request timed out before a callback was received.
107    #[error("timeout")]
108    #[diagnostic(code(jacquard_oauth::callback::timeout))]
109    Timeout,
110    /// The local loopback callback server could not start or accept callbacks.
111    #[error("loopback callback server error: {0}")]
112    #[diagnostic(code(jacquard_oauth::callback::loopback_server))]
113    LoopbackServer(String),
114    /// An error occurred resolving permission sets during session creation.
115    #[cfg(feature = "scope-check")]
116    #[error("scope resolution failed: {detail}")]
117    #[diagnostic(code(jacquard_oauth::callback::scope_resolution))]
118    ScopeResolution {
119        /// Description of the resolution failure.
120        detail: String,
121    },
122}
123
124/// Error returned when a request's required scope is not covered by the session's granted scopes.
125#[cfg(feature = "scope-check")]
126#[derive(Debug, thiserror::Error, Diagnostic)]
127#[error("request to `{nsid}` not permitted: no granted scope covers this endpoint")]
128#[diagnostic(
129    code(jacquard_oauth::scope_check),
130    help("granted scopes: {granted}. The endpoint requires an `rpc:` scope covering `{nsid}`.")
131)]
132pub struct ScopeError {
133    /// The NSID of the XRPC method that was denied.
134    pub nsid: SmolStr,
135    /// Human-readable summary of the granted scopes for diagnostic output.
136    pub granted: SmolStr,
137}
138
139/// Convenience alias for `Result<T, OAuthError>`.
140pub type Result<T> = core::result::Result<T, OAuthError>;