use super::ValidatedRedirectUri; use crate::auth::AuthenticationError; use crate::models::error::ApiError; use crate::models::ids::DecodingError; use actix_web::http::StatusCode; use actix_web::HttpResponse; #[derive(thiserror::Error, Debug)] #[error("{}", .error_type)] pub struct OAuthError { #[source] pub error_type: OAuthErrorType, pub state: Option, pub valid_redirect_uri: Option, } impl From for OAuthError where T: Into, { fn from(value: T) -> Self { OAuthError::error(value.into()) } } impl OAuthError { /// The OAuth request failed either because of an invalid redirection URI /// or before we could validate the one we were given, so return an error /// directly to the caller /// /// See: IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) pub fn error(error_type: impl Into) -> Self { Self { error_type: error_type.into(), valid_redirect_uri: None, state: None, } } /// The OAuth request failed for a reason other than an invalid redirection URI /// So send the error in url-encoded form to the redirect URI /// /// See: IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) pub fn redirect( err: impl Into, state: &Option, valid_redirect_uri: &ValidatedRedirectUri, ) -> Self { Self { error_type: err.into(), state: state.clone(), valid_redirect_uri: Some(valid_redirect_uri.clone()), } } } impl actix_web::ResponseError for OAuthError { fn status_code(&self) -> StatusCode { match self.error_type { OAuthErrorType::AuthenticationError(_) | OAuthErrorType::FailedScopeParse(_) | OAuthErrorType::ScopesTooBroad | OAuthErrorType::AccessDenied => { if self.valid_redirect_uri.is_some() { StatusCode::FOUND } else { StatusCode::INTERNAL_SERVER_ERROR } } OAuthErrorType::RedirectUriNotConfigured(_) | OAuthErrorType::ClientMissingRedirectURI { client_id: _ } | OAuthErrorType::InvalidAcceptFlowId | OAuthErrorType::MalformedId(_) | OAuthErrorType::InvalidClientId(_) | OAuthErrorType::InvalidAuthCode | OAuthErrorType::OnlySupportsAuthorizationCodeGrant(_) | OAuthErrorType::RedirectUriChanged(_) | OAuthErrorType::UnauthorizedClient => StatusCode::BAD_REQUEST, OAuthErrorType::ClientAuthenticationFailed => StatusCode::UNAUTHORIZED, } } fn error_response(&self) -> HttpResponse { if let Some(ValidatedRedirectUri(mut redirect_uri)) = self.valid_redirect_uri.clone() { redirect_uri = format!( "{}?error={}&error_description={}", redirect_uri, self.error_type.error_name(), self.error_type, ); if let Some(state) = self.state.as_ref() { redirect_uri = format!("{}&state={}", redirect_uri, state); } redirect_uri = urlencoding::encode(&redirect_uri).to_string(); HttpResponse::Found() .append_header(("Location".to_string(), redirect_uri)) .finish() } else { HttpResponse::build(self.status_code()).json(ApiError { error: &self.error_type.error_name(), description: &self.error_type.to_string(), }) } } } #[derive(thiserror::Error, Debug)] pub enum OAuthErrorType { #[error(transparent)] AuthenticationError(#[from] AuthenticationError), #[error("Client {} has no redirect URIs specified", .client_id.0)] ClientMissingRedirectURI { client_id: crate::database::models::OAuthClientId, }, #[error("The provided redirect URI did not match any configured in the client")] RedirectUriNotConfigured(String), #[error("The provided scope was malformed or did not correspond to known scopes ({0})")] FailedScopeParse(bitflags::parser::ParseError), #[error( "The provided scope requested scopes broader than the developer app is configured with" )] ScopesTooBroad, #[error("The provided flow id was invalid")] InvalidAcceptFlowId, #[error("The provided client id was invalid")] InvalidClientId(crate::database::models::OAuthClientId), #[error("The provided ID could not be decoded: {0}")] MalformedId(#[from] DecodingError), #[error("Failed to authenticate client")] ClientAuthenticationFailed, #[error("The provided authorization grant code was invalid")] InvalidAuthCode, #[error("The provided client id did not match the id this authorization code was granted to")] UnauthorizedClient, #[error("The provided redirect URI did not exactly match the uri originally provided when this flow began")] RedirectUriChanged(Option), #[error("The provided grant type ({0}) must be \"authorization_code\"")] OnlySupportsAuthorizationCodeGrant(String), #[error("The resource owner denied the request")] AccessDenied, } impl From for OAuthErrorType { fn from(value: crate::database::models::DatabaseError) -> Self { OAuthErrorType::AuthenticationError(value.into()) } } impl From for OAuthErrorType { fn from(value: sqlx::Error) -> Self { OAuthErrorType::AuthenticationError(value.into()) } } impl OAuthErrorType { pub fn error_name(&self) -> String { // IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#autoid-38) // And 5.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) match self { Self::RedirectUriNotConfigured(_) | Self::ClientMissingRedirectURI { client_id: _ } => { "invalid_uri" } Self::AuthenticationError(_) | Self::InvalidAcceptFlowId => "server_error", Self::RedirectUriChanged(_) | Self::MalformedId(_) => "invalid_request", Self::FailedScopeParse(_) | Self::ScopesTooBroad => "invalid_scope", Self::InvalidClientId(_) | Self::ClientAuthenticationFailed => "invalid_client", Self::InvalidAuthCode | Self::OnlySupportsAuthorizationCodeGrant(_) => "invalid_grant", Self::UnauthorizedClient => "unauthorized_client", Self::AccessDenied => "access_denied", } .to_string() } }