1use crate::errors::{ErrorSeverity, UserFriendlyError};
19use std::fmt;
20use thiserror::Error;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum OAuth2CookieKind {
25 State,
27 Pkce,
29 Auth,
31}
32
33impl fmt::Display for OAuth2CookieKind {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 OAuth2CookieKind::State => f.write_str("state"),
37 OAuth2CookieKind::Pkce => f.write_str("pkce"),
38 OAuth2CookieKind::Auth => f.write_str("auth"),
39 }
40 }
41}
42
43#[derive(Debug, Error)]
49pub enum OAuth2Error {
50 #[error("OAuth2 misconfiguration: missing {field}")]
53 ConfigMissing {
54 field: &'static str,
56 },
57
58 #[error("Invalid OAuth2 URL for {field}: {reason}")]
60 InvalidUrl {
61 field: &'static str,
63 reason: String,
65 },
66
67 #[error("Invalid {which} cookie template: {reason}")]
69 CookieTemplateInvalid {
70 which: OAuth2CookieKind,
72 reason: String,
74 },
75
76 #[error("Missing OAuth2 state cookie")]
79 MissingStateCookie,
80
81 #[error("Missing OAuth2 PKCE cookie")]
83 MissingPkceCookie,
84
85 #[error("OAuth2 provider returned error: {error}")]
87 ProviderReturnedError {
88 error: String,
90 description: Option<String>,
92 },
93
94 #[error("OAuth2 state mismatch")]
96 StateMismatch,
97
98 #[error("OAuth2 callback missing authorization code")]
100 MissingAuthorizationCode,
101
102 #[error("OAuth2 token exchange failed: {message}")]
104 TokenExchange {
105 message: String,
107 },
108
109 #[error("OAuth2 account mapping failed: {message}")]
112 AccountMapping {
113 message: String,
115 },
116
117 #[error("OAuth2 account persistence failed: {message}")]
119 AccountPersistence {
120 message: String,
122 },
123
124 #[error("OAuth2 JWT encoding failed: {message}")]
126 JwtEncoding {
127 message: String,
129 },
130
131 #[error("OAuth2 JWT is not valid UTF‑8")]
133 JwtNotUtf8,
134}
135
136impl OAuth2Error {
137 #[must_use]
141 pub fn missing(field: &'static str) -> Self {
142 Self::ConfigMissing { field }
143 }
144
145 #[must_use]
147 pub fn invalid_url(field: &'static str, reason: impl Into<String>) -> Self {
148 Self::InvalidUrl {
149 field,
150 reason: reason.into(),
151 }
152 }
153
154 #[must_use]
156 pub fn cookie_invalid(which: OAuth2CookieKind, reason: impl Into<String>) -> Self {
157 Self::CookieTemplateInvalid {
158 which,
159 reason: reason.into(),
160 }
161 }
162
163 #[must_use]
165 pub fn provider_error(error: impl Into<String>, description: Option<String>) -> Self {
166 Self::ProviderReturnedError {
167 error: error.into(),
168 description,
169 }
170 }
171
172 #[must_use]
174 pub fn token_exchange(message: impl Into<String>) -> Self {
175 Self::TokenExchange {
176 message: message.into(),
177 }
178 }
179
180 #[must_use]
182 pub fn account_mapping(message: impl Into<String>) -> Self {
183 Self::AccountMapping {
184 message: message.into(),
185 }
186 }
187
188 #[must_use]
190 pub fn account_persistence(message: impl Into<String>) -> Self {
191 Self::AccountPersistence {
192 message: message.into(),
193 }
194 }
195
196 #[must_use]
198 pub fn jwt_encoding(message: impl Into<String>) -> Self {
199 Self::JwtEncoding {
200 message: message.into(),
201 }
202 }
203}
204
205pub type Result<T> = std::result::Result<T, OAuth2Error>;
207
208impl UserFriendlyError for OAuth2Error {
209 fn user_message(&self) -> String {
210 match self {
211 OAuth2Error::ConfigMissing { .. }
213 | OAuth2Error::InvalidUrl { .. }
214 | OAuth2Error::CookieTemplateInvalid { .. } => {
215 "We’re experiencing a technical issue with sign-in. Please try again later."
216 .to_string()
217 }
218
219 OAuth2Error::MissingStateCookie
221 | OAuth2Error::MissingPkceCookie
222 | OAuth2Error::ProviderReturnedError { .. }
223 | OAuth2Error::StateMismatch
224 | OAuth2Error::MissingAuthorizationCode
225 | OAuth2Error::TokenExchange { .. } => {
226 "We couldn’t complete the sign-in with your provider. Please try again.".to_string()
227 }
228
229 OAuth2Error::AccountMapping { .. }
231 | OAuth2Error::AccountPersistence { .. }
232 | OAuth2Error::JwtEncoding { .. }
233 | OAuth2Error::JwtNotUtf8 => {
234 "We signed you in, but couldn’t complete the session setup. Please try again."
235 .to_string()
236 }
237 }
238 }
239
240 fn developer_message(&self) -> String {
241 match self {
242 OAuth2Error::ConfigMissing { field } => {
243 format!("OAuth2Gate configuration missing required field: {field}")
244 }
245 OAuth2Error::InvalidUrl { field, reason } => {
246 format!("Invalid OAuth2 URL for {field}: {reason}")
247 }
248 OAuth2Error::CookieTemplateInvalid { which, reason } => {
249 format!("Invalid {which} cookie template: {reason}")
250 }
251 OAuth2Error::MissingStateCookie => "Missing OAuth2 state cookie at callback".into(),
252 OAuth2Error::MissingPkceCookie => "Missing OAuth2 PKCE cookie at callback".into(),
253 OAuth2Error::ProviderReturnedError { error, description } => format!(
254 "OAuth2 provider returned error: {error} {:?}",
255 description.as_deref()
256 ),
257 OAuth2Error::StateMismatch => "OAuth2 state parameter mismatch".into(),
258 OAuth2Error::MissingAuthorizationCode => {
259 "OAuth2 callback missing authorization code".into()
260 }
261 OAuth2Error::TokenExchange { message } => {
262 format!("OAuth2 token exchange failed: {message}")
263 }
264 OAuth2Error::AccountMapping { message } => {
265 format!("OAuth2 account mapping failed: {message}")
266 }
267 OAuth2Error::AccountPersistence { message } => {
268 format!("OAuth2 account persistence failed: {message}")
269 }
270 OAuth2Error::JwtEncoding { message } => {
271 format!("OAuth2 JWT encoding failed: {message}")
272 }
273 OAuth2Error::JwtNotUtf8 => "OAuth2 JWT is not valid UTF‑8".into(),
274 }
275 }
276
277 fn support_code(&self) -> String {
278 match self {
280 OAuth2Error::ConfigMissing { .. } => "OAUTH2-CONFIG-MISSING-001".into(),
281 OAuth2Error::InvalidUrl { .. } => "OAUTH2-URL-INVALID-002".into(),
282 OAuth2Error::CookieTemplateInvalid { .. } => "OAUTH2-COOKIE-INVALID-003".into(),
283 OAuth2Error::MissingStateCookie => "OAUTH2-STATE-MISSING-004".into(),
284 OAuth2Error::MissingPkceCookie => "OAUTH2-PKCE-MISSING-005".into(),
285 OAuth2Error::ProviderReturnedError { .. } => "OAUTH2-PROVIDER-ERROR-006".into(),
286 OAuth2Error::StateMismatch => "OAUTH2-STATE-MISMATCH-007".into(),
287 OAuth2Error::MissingAuthorizationCode => "OAUTH2-CODE-MISSING-008".into(),
288 OAuth2Error::TokenExchange { .. } => "OAUTH2-TOKEN-EXCHANGE-009".into(),
289 OAuth2Error::AccountMapping { .. } => "OAUTH2-ACCOUNT-MAP-010".into(),
290 OAuth2Error::AccountPersistence { .. } => "OAUTH2-ACCOUNT-PERSIST-011".into(),
291 OAuth2Error::JwtEncoding { .. } => "OAUTH2-JWT-ENCODE-012".into(),
292 OAuth2Error::JwtNotUtf8 => "OAUTH2-JWT-NONUTF8-013".into(),
293 }
294 }
295
296 fn severity(&self) -> ErrorSeverity {
297 match self {
298 OAuth2Error::ConfigMissing { .. }
300 | OAuth2Error::InvalidUrl { .. }
301 | OAuth2Error::CookieTemplateInvalid { .. } => ErrorSeverity::Error,
302
303 OAuth2Error::MissingStateCookie
305 | OAuth2Error::MissingPkceCookie
306 | OAuth2Error::ProviderReturnedError { .. }
307 | OAuth2Error::StateMismatch
308 | OAuth2Error::MissingAuthorizationCode => ErrorSeverity::Warning,
309
310 OAuth2Error::TokenExchange { .. }
312 | OAuth2Error::AccountMapping { .. }
313 | OAuth2Error::AccountPersistence { .. }
314 | OAuth2Error::JwtEncoding { .. }
315 | OAuth2Error::JwtNotUtf8 => ErrorSeverity::Error,
316 }
317 }
318
319 fn suggested_actions(&self) -> Vec<String> {
320 match self {
321 OAuth2Error::ConfigMissing { field } => vec![format!(
322 "Set OAuth2Gate builder field: {field} (auth_url, token_url, client_id, redirect_url)"
323 )],
324 OAuth2Error::InvalidUrl { field, .. } => {
325 vec![format!("Verify URL format and scheme for {field}")]
326 }
327 OAuth2Error::CookieTemplateInvalid { which, .. } => vec![format!(
328 "Review {} cookie template (SameSite/Secure/Max-Age). SameSite=None requires Secure=true",
329 which
330 )],
331 OAuth2Error::MissingStateCookie | OAuth2Error::MissingPkceCookie => vec![
332 "Ensure cookies are set for the same domain and path during /login → /callback"
333 .into(),
334 "Check SameSite and Secure attributes for OAuth redirect round-trip".into(),
335 ],
336 OAuth2Error::ProviderReturnedError { .. } => vec![
337 "Verify client id/secret and callback URL in provider settings".into(),
338 "Check provider status and retry later".into(),
339 ],
340 OAuth2Error::StateMismatch => vec![
341 "Ensure the same domain/protocol is used during the OAuth redirect round-trip"
342 .into(),
343 "Avoid navigating away or opening multiple OAuth tabs simultaneously".into(),
344 ],
345 OAuth2Error::MissingAuthorizationCode => {
346 vec!["Retry sign-in; ensure the provider granted access".into()]
347 }
348 OAuth2Error::TokenExchange { .. } => vec![
349 "Verify token endpoint URL and client credentials".into(),
350 "Check network egress, DNS, and request timeouts".into(),
351 ],
352 OAuth2Error::AccountMapping { .. } => {
353 vec!["Review userinfo call and mapping logic; handle missing fields".into()]
354 }
355 OAuth2Error::AccountPersistence { .. } => {
356 vec!["Check repository connectivity and unique constraints".into()]
357 }
358 OAuth2Error::JwtEncoding { .. } => {
359 vec!["Verify JWT codec configuration and payload serialization".into()]
360 }
361 OAuth2Error::JwtNotUtf8 => {
362 vec!["Ensure JWT codec returns UTF‑8 compatible bytes for transport".into()]
363 }
364 }
365 }
366
367 fn is_retryable(&self) -> bool {
368 matches!(
369 self,
370 OAuth2Error::ProviderReturnedError { .. }
371 | OAuth2Error::TokenExchange { .. }
372 | OAuth2Error::AccountPersistence { .. }
373 )
374 }
375}