systemprompt_api/routes/oauth/
error.rs1use axum::Json;
25use axum::http::{HeaderValue, StatusCode, header};
26use axum::response::{IntoResponse, Redirect, Response};
27use serde::Serialize;
28use systemprompt_config::SecretsBootstrapError;
29use systemprompt_oauth::OauthError;
30use systemprompt_traits::auth::AuthProviderError;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum OAuthErrorCode {
34 InvalidRequest,
35 InvalidClient,
36 InvalidGrant,
37 UnauthorizedClient,
38 UnsupportedGrantType,
39 InvalidScope,
40 InvalidToken,
41 AccessDenied,
42 ServerError,
43 TemporarilyUnavailable,
44 InvalidClientMetadata,
45 AuthenticationFailed,
46 RegistrationFailed,
47 UsernameUnavailable,
48 EmailExists,
49 ExpiredChallenge,
50 InvalidCredential,
51 LinkFailed,
52 InvalidTarget,
53 NotFound,
54}
55
56impl OAuthErrorCode {
57 #[must_use]
58 pub const fn as_str(self) -> &'static str {
59 match self {
60 Self::InvalidRequest => "invalid_request",
61 Self::InvalidClient => "invalid_client",
62 Self::InvalidGrant => "invalid_grant",
63 Self::UnauthorizedClient => "unauthorized_client",
64 Self::UnsupportedGrantType => "unsupported_grant_type",
65 Self::InvalidScope => "invalid_scope",
66 Self::InvalidToken => "invalid_token",
67 Self::AccessDenied => "access_denied",
68 Self::ServerError => "server_error",
69 Self::TemporarilyUnavailable => "temporarily_unavailable",
70 Self::InvalidClientMetadata => "invalid_client_metadata",
71 Self::AuthenticationFailed => "authentication_failed",
72 Self::RegistrationFailed => "registration_failed",
73 Self::UsernameUnavailable => "username_unavailable",
74 Self::EmailExists => "email_exists",
75 Self::ExpiredChallenge => "expired_challenge",
76 Self::InvalidCredential => "invalid_credential",
77 Self::LinkFailed => "link_failed",
78 Self::InvalidTarget => "invalid_target",
79 Self::NotFound => "not_found",
80 }
81 }
82
83 #[must_use]
84 pub const fn default_status(self) -> StatusCode {
85 match self {
86 Self::InvalidRequest
87 | Self::InvalidGrant
88 | Self::UnauthorizedClient
89 | Self::UnsupportedGrantType
90 | Self::InvalidScope
91 | Self::InvalidClientMetadata
92 | Self::ExpiredChallenge
93 | Self::InvalidCredential
94 | Self::LinkFailed
95 | Self::InvalidTarget
96 | Self::RegistrationFailed => StatusCode::BAD_REQUEST,
97 Self::InvalidClient
98 | Self::AccessDenied
99 | Self::AuthenticationFailed
100 | Self::InvalidToken => StatusCode::UNAUTHORIZED,
101 Self::UsernameUnavailable | Self::EmailExists => StatusCode::CONFLICT,
102 Self::NotFound => StatusCode::NOT_FOUND,
103 Self::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
104 Self::TemporarilyUnavailable => StatusCode::SERVICE_UNAVAILABLE,
105 }
106 }
107}
108
109#[derive(Debug, Clone)]
110pub struct RedirectContext {
111 pub uri: String,
112 pub state: Option<String>,
113}
114
115#[derive(Debug)]
116pub struct OAuthHttpError {
117 code: OAuthErrorCode,
118 status: StatusCode,
119 description: String,
120 redirect: Option<RedirectContext>,
121}
122
123impl OAuthHttpError {
124 #[must_use]
125 pub fn new(code: OAuthErrorCode, description: impl Into<String>) -> Self {
126 Self {
127 status: code.default_status(),
128 code,
129 description: description.into(),
130 redirect: None,
131 }
132 }
133
134 #[must_use]
135 pub fn invalid_request(description: impl Into<String>) -> Self {
136 Self::new(OAuthErrorCode::InvalidRequest, description)
137 }
138
139 #[must_use]
140 pub fn invalid_client(description: impl Into<String>) -> Self {
141 Self::new(OAuthErrorCode::InvalidClient, description)
142 }
143
144 #[must_use]
145 pub fn invalid_grant(description: impl Into<String>) -> Self {
146 Self::new(OAuthErrorCode::InvalidGrant, description)
147 }
148
149 #[must_use]
150 pub fn unauthorized_client(description: impl Into<String>) -> Self {
151 Self::new(OAuthErrorCode::UnauthorizedClient, description)
152 }
153
154 #[must_use]
155 pub fn unsupported_grant_type(description: impl Into<String>) -> Self {
156 Self::new(OAuthErrorCode::UnsupportedGrantType, description)
157 }
158
159 #[must_use]
160 pub fn invalid_scope(description: impl Into<String>) -> Self {
161 Self::new(OAuthErrorCode::InvalidScope, description)
162 }
163
164 #[must_use]
165 pub fn invalid_token(description: impl Into<String>) -> Self {
166 Self::new(OAuthErrorCode::InvalidToken, description)
167 }
168
169 #[must_use]
170 pub fn access_denied(description: impl Into<String>) -> Self {
171 Self::new(OAuthErrorCode::AccessDenied, description)
172 }
173
174 #[must_use]
175 pub fn server_error(description: impl Into<String>) -> Self {
176 Self::new(OAuthErrorCode::ServerError, description)
177 }
178
179 #[must_use]
180 pub fn invalid_client_metadata(description: impl Into<String>) -> Self {
181 Self::new(OAuthErrorCode::InvalidClientMetadata, description)
182 }
183
184 #[must_use]
185 pub fn authentication_failed(description: impl Into<String>) -> Self {
186 Self::new(OAuthErrorCode::AuthenticationFailed, description)
187 }
188
189 #[must_use]
190 pub fn registration_failed(description: impl Into<String>) -> Self {
191 Self::new(OAuthErrorCode::RegistrationFailed, description)
192 }
193
194 #[must_use]
195 pub fn username_unavailable(description: impl Into<String>) -> Self {
196 Self::new(OAuthErrorCode::UsernameUnavailable, description)
197 }
198
199 #[must_use]
200 pub fn email_exists(description: impl Into<String>) -> Self {
201 Self::new(OAuthErrorCode::EmailExists, description)
202 }
203
204 #[must_use]
205 pub fn expired_challenge(description: impl Into<String>) -> Self {
206 Self::new(OAuthErrorCode::ExpiredChallenge, description)
207 }
208
209 #[must_use]
210 pub fn invalid_credential(description: impl Into<String>) -> Self {
211 Self::new(OAuthErrorCode::InvalidCredential, description)
212 }
213
214 #[must_use]
215 pub fn link_failed(description: impl Into<String>) -> Self {
216 Self::new(OAuthErrorCode::LinkFailed, description)
217 }
218
219 #[must_use]
220 pub fn invalid_target(description: impl Into<String>) -> Self {
221 Self::new(OAuthErrorCode::InvalidTarget, description)
222 }
223
224 #[must_use]
225 pub fn not_found(description: impl Into<String>) -> Self {
226 Self::new(OAuthErrorCode::NotFound, description)
227 }
228
229 #[must_use]
231 pub const fn with_status(mut self, status: StatusCode) -> Self {
232 self.status = status;
233 self
234 }
235
236 #[must_use]
237 pub fn with_redirect(mut self, uri: impl Into<String>, state: Option<String>) -> Self {
238 self.redirect = Some(RedirectContext {
239 uri: uri.into(),
240 state,
241 });
242 self
243 }
244
245 #[must_use]
246 pub const fn code(&self) -> OAuthErrorCode {
247 self.code
248 }
249
250 #[must_use]
251 pub fn description(&self) -> &str {
252 &self.description
253 }
254
255 fn log(&self) {
256 if self.status.is_server_error() {
257 tracing::error!(
258 error = self.code.as_str(),
259 description = %self.description,
260 status = self.status.as_u16(),
261 "OAuth server error response"
262 );
263 } else if self.status.is_client_error() {
264 tracing::warn!(
265 error = self.code.as_str(),
266 description = %self.description,
267 status = self.status.as_u16(),
268 "OAuth client error response"
269 );
270 }
271 }
272}
273
274#[derive(Debug, Serialize)]
275struct OAuthErrorBody<'a> {
276 error: &'a str,
277 error_description: &'a str,
278}
279
280impl IntoResponse for OAuthHttpError {
281 fn into_response(self) -> Response {
282 self.log();
283
284 if let Some(redirect) = &self.redirect {
285 let mut target = format!(
286 "{}?error={}&error_description={}",
287 redirect.uri,
288 urlencoding::encode(self.code.as_str()),
289 urlencoding::encode(&self.description),
290 );
291 if let Some(state) = &redirect.state {
292 target.push_str("&state=");
293 target.push_str(&urlencoding::encode(state));
294 }
295 return Redirect::to(&target).into_response();
296 }
297
298 let body = OAuthErrorBody {
299 error: self.code.as_str(),
300 error_description: &self.description,
301 };
302 let mut response = (self.status, Json(body)).into_response();
303
304 if self.status == StatusCode::UNAUTHORIZED
305 && let Ok(value) = HeaderValue::from_str(
306 "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"",
307 )
308 {
309 response
310 .headers_mut()
311 .insert(header::WWW_AUTHENTICATE, value);
312 }
313
314 response
315 }
316}
317
318impl From<OauthError> for OAuthHttpError {
319 fn from(err: OauthError) -> Self {
320 match &err {
321 OauthError::InvalidClient(_) | OauthError::ClientNotFound(_) => {
322 Self::invalid_client(err.to_string())
323 },
324 OauthError::InvalidGrant(_)
325 | OauthError::CodeNotFound(_)
326 | OauthError::TokenNotFound(_)
327 | OauthError::PkceMismatch(_)
328 | OauthError::Expired(_) => Self::invalid_grant(err.to_string()),
329 OauthError::Validation(_) => Self::invalid_request(err.to_string()),
330 OauthError::Unauthorized(_) => Self::access_denied(err.to_string()),
331 OauthError::UsernameTaken(_) => Self::username_unavailable(
332 "Username is already taken. Please choose a different username.",
333 ),
334 OauthError::EmailRegistered(_) => {
335 Self::email_exists("An account with this email already exists.")
336 },
337 OauthError::UserNotFound(_) => Self::not_found(err.to_string()),
338 OauthError::RegistrationStateExpired => Self::expired_challenge(
339 "Registration challenge has expired. Please start the registration process again.",
340 ),
341 OauthError::WebAuthnVerificationFailed(_) => Self::invalid_credential(
342 "WebAuthn verification failed. Please ensure your authenticator and browser are \
343 compatible.",
344 ),
345 OauthError::WebAuthn(_)
346 | OauthError::User(_)
347 | OauthError::Session(_)
348 | OauthError::TokenInvalid(_)
349 | OauthError::TokenAlgMismatch { .. }
350 | OauthError::TokenMissingKid
351 | OauthError::TokenUnknownKid { .. }
352 | OauthError::Provider(_)
353 | OauthError::Repository(_)
354 | OauthError::DatabaseRepository(_)
355 | OauthError::Config(_)
356 | OauthError::Crypto(_)
357 | OauthError::Internal(_) => Self::server_error(err.to_string()),
358 }
359 }
360}
361
362impl From<AuthProviderError> for OAuthHttpError {
363 fn from(err: AuthProviderError) -> Self {
364 match &err {
365 AuthProviderError::InvalidCredentials | AuthProviderError::InvalidToken => {
366 Self::invalid_client(err.to_string())
367 },
368 AuthProviderError::UserNotFound => Self::not_found(err.to_string()),
369 AuthProviderError::TokenExpired => Self::invalid_grant(err.to_string()),
370 AuthProviderError::InsufficientPermissions => Self::access_denied(err.to_string()),
371 _ => Self::server_error(err.to_string()),
372 }
373 }
374}
375
376impl From<SecretsBootstrapError> for OAuthHttpError {
377 fn from(err: SecretsBootstrapError) -> Self {
378 Self::server_error(err.to_string())
379 }
380}
381
382impl From<sqlx::Error> for OAuthHttpError {
383 fn from(err: sqlx::Error) -> Self {
384 if let sqlx::Error::Database(db_err) = &err
385 && db_err.is_unique_violation()
386 {
387 return Self::new(OAuthErrorCode::UsernameUnavailable, err.to_string());
388 }
389 Self::server_error(err.to_string())
390 }
391}
392
393impl From<anyhow::Error> for OAuthHttpError {
394 fn from(err: anyhow::Error) -> Self {
395 Self::server_error(err.to_string())
396 }
397}