use crate::errors::{ErrorSeverity, UserFriendlyError};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OAuth2CookieKind {
State,
Pkce,
Auth,
}
impl fmt::Display for OAuth2CookieKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OAuth2CookieKind::State => f.write_str("state"),
OAuth2CookieKind::Pkce => f.write_str("pkce"),
OAuth2CookieKind::Auth => f.write_str("auth"),
}
}
}
#[derive(Debug, Error)]
pub enum OAuth2Error {
#[error("OAuth2 misconfiguration: missing {field}")]
ConfigMissing {
field: &'static str,
},
#[error("Invalid OAuth2 URL for {field}: {reason}")]
InvalidUrl {
field: &'static str,
reason: String,
},
#[error("Invalid {which} cookie template: {reason}")]
CookieTemplateInvalid {
which: OAuth2CookieKind,
reason: String,
},
#[error("Missing OAuth2 state cookie")]
MissingStateCookie,
#[error("Missing OAuth2 PKCE cookie")]
MissingPkceCookie,
#[error("OAuth2 provider returned error: {error}")]
ProviderReturnedError {
error: String,
description: Option<String>,
},
#[error("OAuth2 state mismatch")]
StateMismatch,
#[error("OAuth2 callback missing authorization code")]
MissingAuthorizationCode,
#[error("OAuth2 token exchange failed: {message}")]
TokenExchange {
message: String,
},
#[error("OAuth2 account mapping failed: {message}")]
AccountMapping {
message: String,
},
#[error("OAuth2 account persistence failed: {message}")]
AccountPersistence {
message: String,
},
#[error("OAuth2 JWT encoding failed: {message}")]
JwtEncoding {
message: String,
},
#[error("OAuth2 JWT is not valid UTF‑8")]
JwtNotUtf8,
}
impl OAuth2Error {
#[must_use]
pub fn missing(field: &'static str) -> Self {
Self::ConfigMissing { field }
}
#[must_use]
pub fn invalid_url(field: &'static str, reason: impl Into<String>) -> Self {
Self::InvalidUrl {
field,
reason: reason.into(),
}
}
#[must_use]
pub fn cookie_invalid(which: OAuth2CookieKind, reason: impl Into<String>) -> Self {
Self::CookieTemplateInvalid {
which,
reason: reason.into(),
}
}
#[must_use]
pub fn provider_error(error: impl Into<String>, description: Option<String>) -> Self {
Self::ProviderReturnedError {
error: error.into(),
description,
}
}
#[must_use]
pub fn token_exchange(message: impl Into<String>) -> Self {
Self::TokenExchange {
message: message.into(),
}
}
#[must_use]
pub fn account_mapping(message: impl Into<String>) -> Self {
Self::AccountMapping {
message: message.into(),
}
}
#[must_use]
pub fn account_persistence(message: impl Into<String>) -> Self {
Self::AccountPersistence {
message: message.into(),
}
}
#[must_use]
pub fn jwt_encoding(message: impl Into<String>) -> Self {
Self::JwtEncoding {
message: message.into(),
}
}
}
pub type Result<T> = std::result::Result<T, OAuth2Error>;
impl UserFriendlyError for OAuth2Error {
fn user_message(&self) -> String {
match self {
OAuth2Error::ConfigMissing { .. }
| OAuth2Error::InvalidUrl { .. }
| OAuth2Error::CookieTemplateInvalid { .. } => {
"We’re experiencing a technical issue with sign-in. Please try again later."
.to_string()
}
OAuth2Error::MissingStateCookie
| OAuth2Error::MissingPkceCookie
| OAuth2Error::ProviderReturnedError { .. }
| OAuth2Error::StateMismatch
| OAuth2Error::MissingAuthorizationCode
| OAuth2Error::TokenExchange { .. } => {
"We couldn’t complete the sign-in with your provider. Please try again.".to_string()
}
OAuth2Error::AccountMapping { .. }
| OAuth2Error::AccountPersistence { .. }
| OAuth2Error::JwtEncoding { .. }
| OAuth2Error::JwtNotUtf8 => {
"We signed you in, but couldn’t complete the session setup. Please try again."
.to_string()
}
}
}
fn developer_message(&self) -> String {
match self {
OAuth2Error::ConfigMissing { field } => {
format!("OAuth2Gate configuration missing required field: {field}")
}
OAuth2Error::InvalidUrl { field, reason } => {
format!("Invalid OAuth2 URL for {field}: {reason}")
}
OAuth2Error::CookieTemplateInvalid { which, reason } => {
format!("Invalid {which} cookie template: {reason}")
}
OAuth2Error::MissingStateCookie => "Missing OAuth2 state cookie at callback".into(),
OAuth2Error::MissingPkceCookie => "Missing OAuth2 PKCE cookie at callback".into(),
OAuth2Error::ProviderReturnedError { error, description } => format!(
"OAuth2 provider returned error: {error} {:?}",
description.as_deref()
),
OAuth2Error::StateMismatch => "OAuth2 state parameter mismatch".into(),
OAuth2Error::MissingAuthorizationCode => {
"OAuth2 callback missing authorization code".into()
}
OAuth2Error::TokenExchange { message } => {
format!("OAuth2 token exchange failed: {message}")
}
OAuth2Error::AccountMapping { message } => {
format!("OAuth2 account mapping failed: {message}")
}
OAuth2Error::AccountPersistence { message } => {
format!("OAuth2 account persistence failed: {message}")
}
OAuth2Error::JwtEncoding { message } => {
format!("OAuth2 JWT encoding failed: {message}")
}
OAuth2Error::JwtNotUtf8 => "OAuth2 JWT is not valid UTF‑8".into(),
}
}
fn support_code(&self) -> String {
match self {
OAuth2Error::ConfigMissing { .. } => "OAUTH2-CONFIG-MISSING-001".into(),
OAuth2Error::InvalidUrl { .. } => "OAUTH2-URL-INVALID-002".into(),
OAuth2Error::CookieTemplateInvalid { .. } => "OAUTH2-COOKIE-INVALID-003".into(),
OAuth2Error::MissingStateCookie => "OAUTH2-STATE-MISSING-004".into(),
OAuth2Error::MissingPkceCookie => "OAUTH2-PKCE-MISSING-005".into(),
OAuth2Error::ProviderReturnedError { .. } => "OAUTH2-PROVIDER-ERROR-006".into(),
OAuth2Error::StateMismatch => "OAUTH2-STATE-MISMATCH-007".into(),
OAuth2Error::MissingAuthorizationCode => "OAUTH2-CODE-MISSING-008".into(),
OAuth2Error::TokenExchange { .. } => "OAUTH2-TOKEN-EXCHANGE-009".into(),
OAuth2Error::AccountMapping { .. } => "OAUTH2-ACCOUNT-MAP-010".into(),
OAuth2Error::AccountPersistence { .. } => "OAUTH2-ACCOUNT-PERSIST-011".into(),
OAuth2Error::JwtEncoding { .. } => "OAUTH2-JWT-ENCODE-012".into(),
OAuth2Error::JwtNotUtf8 => "OAUTH2-JWT-NONUTF8-013".into(),
}
}
fn severity(&self) -> ErrorSeverity {
match self {
OAuth2Error::ConfigMissing { .. }
| OAuth2Error::InvalidUrl { .. }
| OAuth2Error::CookieTemplateInvalid { .. } => ErrorSeverity::Error,
OAuth2Error::MissingStateCookie
| OAuth2Error::MissingPkceCookie
| OAuth2Error::ProviderReturnedError { .. }
| OAuth2Error::StateMismatch
| OAuth2Error::MissingAuthorizationCode => ErrorSeverity::Warning,
OAuth2Error::TokenExchange { .. }
| OAuth2Error::AccountMapping { .. }
| OAuth2Error::AccountPersistence { .. }
| OAuth2Error::JwtEncoding { .. }
| OAuth2Error::JwtNotUtf8 => ErrorSeverity::Error,
}
}
fn suggested_actions(&self) -> Vec<String> {
match self {
OAuth2Error::ConfigMissing { field } => vec![format!(
"Set OAuth2Gate builder field: {field} (auth_url, token_url, client_id, redirect_url)"
)],
OAuth2Error::InvalidUrl { field, .. } => {
vec![format!("Verify URL format and scheme for {field}")]
}
OAuth2Error::CookieTemplateInvalid { which, .. } => vec![format!(
"Review {} cookie template (SameSite/Secure/Max-Age). SameSite=None requires Secure=true",
which
)],
OAuth2Error::MissingStateCookie | OAuth2Error::MissingPkceCookie => vec![
"Ensure cookies are set for the same domain and path during /login → /callback"
.into(),
"Check SameSite and Secure attributes for OAuth redirect round-trip".into(),
],
OAuth2Error::ProviderReturnedError { .. } => vec![
"Verify client id/secret and callback URL in provider settings".into(),
"Check provider status and retry later".into(),
],
OAuth2Error::StateMismatch => vec![
"Ensure the same domain/protocol is used during the OAuth redirect round-trip"
.into(),
"Avoid navigating away or opening multiple OAuth tabs simultaneously".into(),
],
OAuth2Error::MissingAuthorizationCode => {
vec!["Retry sign-in; ensure the provider granted access".into()]
}
OAuth2Error::TokenExchange { .. } => vec![
"Verify token endpoint URL and client credentials".into(),
"Check network egress, DNS, and request timeouts".into(),
],
OAuth2Error::AccountMapping { .. } => {
vec!["Review userinfo call and mapping logic; handle missing fields".into()]
}
OAuth2Error::AccountPersistence { .. } => {
vec!["Check repository connectivity and unique constraints".into()]
}
OAuth2Error::JwtEncoding { .. } => {
vec!["Verify JWT codec configuration and payload serialization".into()]
}
OAuth2Error::JwtNotUtf8 => {
vec!["Ensure JWT codec returns UTF‑8 compatible bytes for transport".into()]
}
}
}
fn is_retryable(&self) -> bool {
matches!(
self,
OAuth2Error::ProviderReturnedError { .. }
| OAuth2Error::TokenExchange { .. }
| OAuth2Error::AccountPersistence { .. }
)
}
}