You've already forked AstralRinth
forked from didirus/AstralRinth
* WIP end-of-day push * Authorize endpoint, accept endpoints, DB stuff for oauth clients, their redirects, and client authorizations * OAuth Client create route * Get user clients * Client delete * Edit oauth client * Include redirects in edit client route * Database stuff for tokens * Reorg oauth stuff out of auth/flows and into its own module * Impl OAuth get access token endpoint * Accept oauth access tokens as auth and update through AuthQueue * User OAuth authorization management routes * Forgot to actually add the routes lol * Bit o cleanup * Happy path test for OAuth and minor fixes for things it found * Add dummy data oauth client (and detect/handle dummy data version changes) * More tests * Another test * More tests and reject endpoint * Test oauth client and authorization management routes * cargo sqlx prepare * dead code warning * Auto clippy fixes * Uri refactoring * minor name improvement * Don't compile-time check the test sqlx queries * Trying to fix db concurrency problem to get tests to pass * Try fix from test PR * Fixes for updated sqlx * Prevent restricted scopes from being requested or issued * Get OAuth client(s) * Remove joined oauth client info from authorization returns * Add default conversion to OAuthError::error so we can use ? * Rework routes * Consolidate scopes into SESSION_ACCESS * Cargo sqlx prepare * Parse to OAuthClientId automatically through serde and actix * Cargo clippy * Remove validation requiring 1 redirect URI on oauth client creation * Use serde(flatten) on OAuthClientCreationResult
177 lines
6.6 KiB
Rust
177 lines
6.6 KiB
Rust
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<String>,
|
|
pub valid_redirect_uri: Option<ValidatedRedirectUri>,
|
|
}
|
|
|
|
impl<T> From<T> for OAuthError
|
|
where
|
|
T: Into<OAuthErrorType>,
|
|
{
|
|
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<OAuthErrorType>) -> 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<OAuthErrorType>,
|
|
state: &Option<String>,
|
|
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<String>),
|
|
#[error("The provided grant type ({0}) must be \"authorization_code\"")]
|
|
OnlySupportsAuthorizationCodeGrant(String),
|
|
#[error("The resource owner denied the request")]
|
|
AccessDenied,
|
|
}
|
|
|
|
impl From<crate::database::models::DatabaseError> for OAuthErrorType {
|
|
fn from(value: crate::database::models::DatabaseError) -> Self {
|
|
OAuthErrorType::AuthenticationError(value.into())
|
|
}
|
|
}
|
|
|
|
impl From<sqlx::Error> 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()
|
|
}
|
|
}
|