soma_auth/upstream/types.rs
1//! Shared types for outbound upstream OAuth.
2
3use serde::Serialize;
4use thiserror::Error;
5
6/// Stable error kinds for upstream OAuth flows.
7///
8/// These must be kept in sync with `docs/dev/ERRORS.md`.
9#[derive(Debug, Error)]
10pub enum OauthError {
11 /// Refresh token was rejected (`invalid_grant`) or decryption failed after key
12 /// rotation. User must re-initiate the authorization flow.
13 #[error("oauth_needs_reauth: {0}")]
14 NeedsReauth(String),
15
16 /// Callback state is missing, expired, replayed, or bound to a different
17 /// subject / upstream.
18 #[allow(dead_code)]
19 #[error("oauth_state_invalid: {0}")]
20 StateInvalid(String),
21
22 /// Upstream AS refused the `resource` parameter or issued a token with the
23 /// wrong audience (RFC 8707).
24 #[allow(dead_code)]
25 #[error("oauth_resource_mismatch: {0}")]
26 ResourceMismatch(String),
27
28 /// AS metadata `issuer` did not match the discovered AS URL (RFC 8414 ยง3.3).
29 #[error("oauth_issuer_mismatch: {0}")]
30 IssuerMismatch(String),
31
32 /// AS only offered `plain` PKCE or omitted `code_challenge_methods_supported`.
33 #[error("oauth_unsupported_method: {0}")]
34 UnsupportedMethod(String),
35
36 /// Internal / configuration errors that are not caller-recoverable.
37 #[error("internal_error: {0}")]
38 Internal(String),
39}
40
41impl OauthError {
42 /// Stable `kind` string for structured log / envelope fields.
43 pub fn kind(&self) -> &'static str {
44 match self {
45 Self::NeedsReauth(_) => "oauth_needs_reauth",
46 Self::StateInvalid(_) => "oauth_state_invalid",
47 Self::ResourceMismatch(_) => "oauth_resource_mismatch",
48 Self::IssuerMismatch(_) => "oauth_issuer_mismatch",
49 Self::UnsupportedMethod(_) => "oauth_unsupported_method",
50 Self::Internal(_) => "internal_error",
51 }
52 }
53
54 /// Transport-neutral HTTP status code for this error.
55 ///
56 /// Returns a bare `u16` rather than `axum::http::StatusCode` so the upstream
57 /// OAuth runtime carries no transport dependency; the product binary maps it
58 /// onto its own response type at the route boundary.
59 #[allow(dead_code)]
60 #[must_use]
61 pub const fn http_status_code(&self) -> u16 {
62 match self {
63 Self::NeedsReauth(_) => 401,
64 Self::StateInvalid(_) => 400,
65 Self::ResourceMismatch(_) | Self::IssuerMismatch(_) | Self::UnsupportedMethod(_) => 502,
66 Self::Internal(_) => 500,
67 }
68 }
69}
70
71/// Return value of [`crate::upstream::manager::UpstreamOauthManager::begin_authorization`].
72#[derive(Debug, Serialize)]
73pub struct BeginAuthorization {
74 /// URL the operator's browser must navigate to.
75 pub authorization_url: String,
76}