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