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