use axum::Json;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Redirect, Response};
use serde::Serialize;
use systemprompt_traits::BoxedSource;
mod code;
mod conversions;
pub use code::OAuthErrorCode;
#[derive(Debug, Clone)]
pub struct RedirectContext {
pub uri: String,
pub state: Option<String>,
}
#[derive(Debug)]
pub struct OAuthHttpError {
code: OAuthErrorCode,
status: StatusCode,
description: String,
redirect: Option<RedirectContext>,
source: Option<BoxedSource>,
}
impl OAuthHttpError {
#[must_use]
pub fn new(code: OAuthErrorCode, description: impl Into<String>) -> Self {
Self {
status: code.default_status(),
code,
description: description.into(),
redirect: None,
source: None,
}
}
#[must_use]
pub fn invalid_request(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidRequest, description)
}
#[must_use]
pub fn invalid_client(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidClient, description)
}
#[must_use]
pub fn invalid_grant(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidGrant, description)
}
#[must_use]
pub fn unauthorized_client(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::UnauthorizedClient, description)
}
#[must_use]
pub fn unsupported_grant_type(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::UnsupportedGrantType, description)
}
#[must_use]
pub fn invalid_scope(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidScope, description)
}
#[must_use]
pub fn invalid_token(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidToken, description)
}
#[must_use]
pub fn access_denied(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::AccessDenied, description)
}
#[must_use]
pub fn server_error(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::ServerError, description)
}
#[must_use]
pub fn invalid_client_metadata(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidClientMetadata, description)
}
#[must_use]
pub fn authentication_failed(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::AuthenticationFailed, description)
}
#[must_use]
pub fn registration_failed(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::RegistrationFailed, description)
}
#[must_use]
pub fn username_unavailable(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::UsernameUnavailable, description)
}
#[must_use]
pub fn email_exists(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::EmailExists, description)
}
#[must_use]
pub fn expired_challenge(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::ExpiredChallenge, description)
}
#[must_use]
pub fn invalid_credential(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidCredential, description)
}
#[must_use]
pub fn link_failed(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::LinkFailed, description)
}
#[must_use]
pub fn invalid_target(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::InvalidTarget, description)
}
#[must_use]
pub fn not_found(description: impl Into<String>) -> Self {
Self::new(OAuthErrorCode::NotFound, description)
}
#[must_use]
pub const fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
#[must_use]
pub fn with_redirect(mut self, uri: impl Into<String>, state: Option<String>) -> Self {
self.redirect = Some(RedirectContext {
uri: uri.into(),
state,
});
self
}
#[must_use]
pub fn with_source(mut self, source: impl Into<BoxedSource>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub const fn code(&self) -> OAuthErrorCode {
self.code
}
#[must_use]
pub fn description(&self) -> &str {
&self.description
}
fn log(&self) {
let cause = self.source.as_deref().map(cause_chain);
if self.status.is_server_error() {
tracing::error!(
error = self.code.as_str(),
description = %self.description,
status = self.status.as_u16(),
cause = cause.as_deref(),
"OAuth server error response"
);
} else if self.status.is_client_error() {
tracing::warn!(
error = self.code.as_str(),
description = %self.description,
status = self.status.as_u16(),
cause = cause.as_deref(),
"OAuth client error response"
);
}
}
}
fn cause_chain(error: &(dyn std::error::Error + Send + Sync + 'static)) -> String {
let mut chain = error.to_string();
let mut next = error.source();
while let Some(cause) = next {
chain.push_str(": ");
chain.push_str(&cause.to_string());
next = cause.source();
}
chain
}
const SERVER_ERROR_DESCRIPTION: &str = "The authorization server encountered an internal error";
#[derive(Debug, Serialize)]
struct OAuthErrorBody<'a> {
error: &'a str,
error_description: &'a str,
}
impl IntoResponse for OAuthHttpError {
fn into_response(self) -> Response {
self.log();
let description = if self.status.is_server_error() {
SERVER_ERROR_DESCRIPTION
} else {
self.description.as_str()
};
if let Some(redirect) = &self.redirect {
let separator = if redirect.uri.contains('?') { '&' } else { '?' };
let mut target = format!(
"{}{separator}error={}&error_description={}",
redirect.uri,
urlencoding::encode(self.code.as_str()),
urlencoding::encode(description),
);
if let Some(state) = &redirect.state {
target.push_str("&state=");
target.push_str(&urlencoding::encode(state));
}
return Redirect::to(&target).into_response();
}
let body = OAuthErrorBody {
error: self.code.as_str(),
error_description: description,
};
let mut response = (self.status, Json(body)).into_response();
if self.status == StatusCode::UNAUTHORIZED
&& let Ok(value) = HeaderValue::from_str(
"Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"",
)
{
response
.headers_mut()
.insert(header::WWW_AUTHENTICATE, value);
}
response
}
}