systemprompt_api/routes/oauth/error/
mod.rs1use axum::Json;
15use axum::http::{HeaderValue, StatusCode, header};
16use axum::response::{IntoResponse, Redirect, Response};
17use serde::Serialize;
18
19mod code;
20mod conversions;
21
22pub use code::OAuthErrorCode;
23
24#[derive(Debug, Clone)]
25pub struct RedirectContext {
26 pub uri: String,
27 pub state: Option<String>,
28}
29
30#[derive(Debug)]
31pub struct OAuthHttpError {
32 code: OAuthErrorCode,
33 status: StatusCode,
34 description: String,
35 redirect: Option<RedirectContext>,
36}
37
38impl OAuthHttpError {
39 #[must_use]
40 pub fn new(code: OAuthErrorCode, description: impl Into<String>) -> Self {
41 Self {
42 status: code.default_status(),
43 code,
44 description: description.into(),
45 redirect: None,
46 }
47 }
48
49 #[must_use]
50 pub fn invalid_request(description: impl Into<String>) -> Self {
51 Self::new(OAuthErrorCode::InvalidRequest, description)
52 }
53
54 #[must_use]
55 pub fn invalid_client(description: impl Into<String>) -> Self {
56 Self::new(OAuthErrorCode::InvalidClient, description)
57 }
58
59 #[must_use]
60 pub fn invalid_grant(description: impl Into<String>) -> Self {
61 Self::new(OAuthErrorCode::InvalidGrant, description)
62 }
63
64 #[must_use]
65 pub fn unauthorized_client(description: impl Into<String>) -> Self {
66 Self::new(OAuthErrorCode::UnauthorizedClient, description)
67 }
68
69 #[must_use]
70 pub fn unsupported_grant_type(description: impl Into<String>) -> Self {
71 Self::new(OAuthErrorCode::UnsupportedGrantType, description)
72 }
73
74 #[must_use]
75 pub fn invalid_scope(description: impl Into<String>) -> Self {
76 Self::new(OAuthErrorCode::InvalidScope, description)
77 }
78
79 #[must_use]
80 pub fn invalid_token(description: impl Into<String>) -> Self {
81 Self::new(OAuthErrorCode::InvalidToken, description)
82 }
83
84 #[must_use]
85 pub fn access_denied(description: impl Into<String>) -> Self {
86 Self::new(OAuthErrorCode::AccessDenied, description)
87 }
88
89 #[must_use]
90 pub fn server_error(description: impl Into<String>) -> Self {
91 Self::new(OAuthErrorCode::ServerError, description)
92 }
93
94 #[must_use]
95 pub fn invalid_client_metadata(description: impl Into<String>) -> Self {
96 Self::new(OAuthErrorCode::InvalidClientMetadata, description)
97 }
98
99 #[must_use]
100 pub fn authentication_failed(description: impl Into<String>) -> Self {
101 Self::new(OAuthErrorCode::AuthenticationFailed, description)
102 }
103
104 #[must_use]
105 pub fn registration_failed(description: impl Into<String>) -> Self {
106 Self::new(OAuthErrorCode::RegistrationFailed, description)
107 }
108
109 #[must_use]
110 pub fn username_unavailable(description: impl Into<String>) -> Self {
111 Self::new(OAuthErrorCode::UsernameUnavailable, description)
112 }
113
114 #[must_use]
115 pub fn email_exists(description: impl Into<String>) -> Self {
116 Self::new(OAuthErrorCode::EmailExists, description)
117 }
118
119 #[must_use]
120 pub fn expired_challenge(description: impl Into<String>) -> Self {
121 Self::new(OAuthErrorCode::ExpiredChallenge, description)
122 }
123
124 #[must_use]
125 pub fn invalid_credential(description: impl Into<String>) -> Self {
126 Self::new(OAuthErrorCode::InvalidCredential, description)
127 }
128
129 #[must_use]
130 pub fn link_failed(description: impl Into<String>) -> Self {
131 Self::new(OAuthErrorCode::LinkFailed, description)
132 }
133
134 #[must_use]
135 pub fn invalid_target(description: impl Into<String>) -> Self {
136 Self::new(OAuthErrorCode::InvalidTarget, description)
137 }
138
139 #[must_use]
140 pub fn not_found(description: impl Into<String>) -> Self {
141 Self::new(OAuthErrorCode::NotFound, description)
142 }
143
144 #[must_use]
146 pub const fn with_status(mut self, status: StatusCode) -> Self {
147 self.status = status;
148 self
149 }
150
151 #[must_use]
152 pub fn with_redirect(mut self, uri: impl Into<String>, state: Option<String>) -> Self {
153 self.redirect = Some(RedirectContext {
154 uri: uri.into(),
155 state,
156 });
157 self
158 }
159
160 #[must_use]
161 pub const fn code(&self) -> OAuthErrorCode {
162 self.code
163 }
164
165 #[must_use]
166 pub fn description(&self) -> &str {
167 &self.description
168 }
169
170 fn log(&self) {
171 if self.status.is_server_error() {
172 tracing::error!(
173 error = self.code.as_str(),
174 description = %self.description,
175 status = self.status.as_u16(),
176 "OAuth server error response"
177 );
178 } else if self.status.is_client_error() {
179 tracing::warn!(
180 error = self.code.as_str(),
181 description = %self.description,
182 status = self.status.as_u16(),
183 "OAuth client error response"
184 );
185 }
186 }
187}
188
189#[derive(Debug, Serialize)]
190struct OAuthErrorBody<'a> {
191 error: &'a str,
192 error_description: &'a str,
193}
194
195impl IntoResponse for OAuthHttpError {
196 fn into_response(self) -> Response {
197 self.log();
198
199 if let Some(redirect) = &self.redirect {
200 let mut target = format!(
201 "{}?error={}&error_description={}",
202 redirect.uri,
203 urlencoding::encode(self.code.as_str()),
204 urlencoding::encode(&self.description),
205 );
206 if let Some(state) = &redirect.state {
207 target.push_str("&state=");
208 target.push_str(&urlencoding::encode(state));
209 }
210 return Redirect::to(&target).into_response();
211 }
212
213 let body = OAuthErrorBody {
214 error: self.code.as_str(),
215 error_description: &self.description,
216 };
217 let mut response = (self.status, Json(body)).into_response();
218
219 if self.status == StatusCode::UNAUTHORIZED
220 && let Ok(value) = HeaderValue::from_str(
221 "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"",
222 )
223 {
224 response
225 .headers_mut()
226 .insert(header::WWW_AUTHENTICATE, value);
227 }
228
229 response
230 }
231}