Skip to content
Clone
7f0e1a0
464 lines 18.7 KB Rust
pub use crate::share::identity_error::KenAuthError;
use actix_web::http::StatusCode;
pub type AuthResult<T> = std::result::Result<T, AuthError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthError {
#[error("challenge expired")]
Expired,
#[error("proof payload does not match the request")]
InvalidPayload,
#[error("proof does not meet the required difficulty")]
InvalidProof,
#[error("request payload is malformed")]
MalformedRequest,
#[error("username is invalid")]
InvalidUsername,
#[error("display name is invalid")]
InvalidDisplayname,
#[error("email is invalid")]
InvalidEmail,
#[error("email is unchanged")]
EmailUnchanged,
#[error("birth date is invalid")]
InvalidBirthDate,
#[error("you must be at least {min_age} years old")]
UnderMinimumAge { min_age: u32 },
#[error("password does not satisfy the password policy")]
InvalidPassword,
#[error("password hashing failed: {0}")]
PasswordHash(String),
#[error("username is already taken")]
UsernameTaken,
#[error("email is already taken")]
EmailTaken,
#[error("email verification code is invalid")]
InvalidEmailCode,
#[error("too many invalid email verification attempts")]
EmailCodeAttemptsExceeded,
#[error("password reset token is invalid")]
InvalidResetToken,
#[error("authentication required")]
Unauthenticated,
#[error("forbidden")]
Forbidden,
#[error("{0}")]
BadRequest(String),
#[error("{0}")]
Conflict(String),
#[error("username or password is incorrect")]
InvalidCredentials,
#[error("login is forbidden{}", reason.as_deref().map(|reason| format!(": {reason}")).unwrap_or_default())]
LoginForbidden { reason: Option<String> },
#[error("account is unavailable: {reason}")]
AccountUnavailable { reason: String },
#[error("two-factor verification is required")]
MfaRequired,
#[error("totp is not configured")]
TotpNotConfigured,
#[error("totp is already enabled")]
TotpAlreadyEnabled,
#[error("totp code is invalid")]
TotpCodeInvalid,
#[error("totp code was already used")]
TotpStepReused,
#[error("recovery code is invalid")]
RecoveryCodeInvalid,
#[error("recovery codes are exhausted")]
RecoveryCodesExhausted,
#[error("access token is invalid")]
InvalidAccessToken,
#[error("access token does not exist")]
AccessTokenNotFound,
#[error("access key name is invalid")]
InvalidAccessKeyName,
#[error("access token scope is invalid")]
InvalidScope,
#[error("expires_at is invalid")]
InvalidExpiry,
#[error("too many access keys")]
TooManyAccessKeys,
#[error("public key is invalid")]
SshKeyInvalid,
#[error("permission `{permission}` is not recognized")]
InvalidPermission { permission: String },
#[error("organization does not exist")]
OrganizationNotFound,
#[error("organization member does not exist")]
OrganizationMemberNotFound,
#[error("organization role does not exist")]
OrganizationRoleNotFound,
#[error("organization invite does not exist")]
OrganizationInviteNotFound,
#[error("organization slug is already taken")]
OrganizationSlugTaken,
#[error("user is already a member of this organization")]
OrganizationMemberExists,
#[error("organization role key is already used")]
OrganizationRoleKeyTaken,
#[error("organization role is still assigned to members")]
OrganizationRoleInUse,
#[error("organization role is built in and cannot be modified")]
OrganizationRoleSystem,
#[error("organization invite is exhausted or revoked")]
OrganizationInviteExhausted,
#[error("organization invite has expired")]
OrganizationInviteExpired,
#[error("organization must have an owner; transfer ownership first")]
OrganizationOwnerRequired,
#[error("organization still has active members")]
OrganizationHasMembers,
#[error("organization is not active")]
OrganizationInactive,
#[error("organization membership is banned")]
OrganizationMemberBanned,
#[error("resource version mismatch, current version is {current}")]
VersionMismatch { current: i32 },
#[error("public key is already registered")]
SshKeyTaken,
#[error("ssh key name is invalid")]
SshKeyNameInvalid,
#[error("ssh key does not exist")]
SshKeyNotFound,
#[error("agreement `{key}` does not exist")]
AgreementNotFound { key: String },
#[error("agreement `{key}` must be accepted")]
AgreementNotAccepted { key: String },
#[error("account deletion is already requested")]
DeletionAlreadyRequested,
#[error("account deletion is not requested")]
DeletionNotRequested,
#[error("too many requests, retry after {retry_after_secs}s")]
RateLimited { retry_after_secs: u64 },
#[error("user does not exist")]
UserNotFound,
#[error("app does not exist")]
AppNotFound,
#[error("session does not exist")]
SessionNotFound,
#[error("token does not exist")]
TokenNotFound,
#[error("token has expired")]
TokenExpired,
#[error("token has been revoked")]
TokenRevoked,
#[error("credentials are stale, security version is {current}")]
SecurityVersionStale { current: i32 },
#[error("scope `{required}` is missing")]
ScopeInsufficient { required: String },
#[error("dependency is unavailable: {0}")]
Unavailable(String),
#[error("internal error: {0}")]
Internal(String),
#[error("database error: {0}")]
Database(#[from] crate::db::DbError),
#[error("cache error: {0}")]
Cache(#[from] crate::cache::CacheError),
#[error("session error: {0}")]
Session(#[from] crate::session::SessionError),
#[error("email error: {0}")]
Email(#[from] crate::email::EmailError),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl From<sea_orm::DbErr> for AuthError {
fn from(error: sea_orm::DbErr) -> Self {
Self::Database(crate::db::DbError::from(error))
}
}
api::page_error_conversion!(AuthError, BadRequest);
impl AuthError {
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn status_code(&self) -> StatusCode {
match self {
Self::Unauthenticated
| Self::InvalidCredentials
| Self::InvalidAccessToken
| Self::AccessTokenNotFound
| Self::TokenExpired
| Self::TokenRevoked
| Self::SecurityVersionStale { .. }
| Self::MfaRequired => StatusCode::UNAUTHORIZED,
Self::Forbidden
| Self::BadRequest(_)
| Self::LoginForbidden { .. }
| Self::UnderMinimumAge { .. }
| Self::AccountUnavailable { .. }
| Self::ScopeInsufficient { .. }
| Self::OrganizationInactive
| Self::OrganizationMemberBanned
| Self::AgreementNotAccepted { .. } => StatusCode::FORBIDDEN,
Self::UserNotFound
| Self::SessionNotFound
| Self::TokenNotFound
| Self::AppNotFound
| Self::SshKeyNotFound
| Self::OrganizationNotFound
| Self::OrganizationMemberNotFound
| Self::OrganizationRoleNotFound
| Self::OrganizationInviteNotFound
| Self::AgreementNotFound { .. } => StatusCode::NOT_FOUND,
Self::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::UsernameTaken
| Self::Conflict(_)
| Self::EmailTaken
| Self::SshKeyTaken
| Self::OrganizationSlugTaken
| Self::OrganizationMemberExists
| Self::OrganizationRoleKeyTaken
| Self::OrganizationHasMembers
| Self::TotpAlreadyEnabled
| Self::TotpNotConfigured
| Self::DeletionAlreadyRequested => StatusCode::CONFLICT,
Self::OrganizationRoleInUse
| Self::OrganizationRoleSystem
| Self::OrganizationInviteExhausted
| Self::OrganizationInviteExpired
| Self::OrganizationOwnerRequired => StatusCode::PRECONDITION_FAILED,
Self::VersionMismatch { .. } => StatusCode::CONFLICT,
Self::RateLimited { .. }
| Self::EmailCodeAttemptsExceeded
| Self::TooManyAccessKeys
| Self::RecoveryCodesExhausted => StatusCode::TOO_MANY_REQUESTS,
Self::PasswordHash(_)
| Self::Internal(_)
| Self::Database(_)
| Self::Cache(_)
| Self::Session(_)
| Self::Email(_)
| Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::BAD_REQUEST,
}
}
pub fn code(&self) -> &'static str {
match self {
Self::Expired => "expired",
Self::InvalidPayload => "invalid_payload",
Self::InvalidProof => "invalid_proof",
Self::MalformedRequest => "malformed_request",
Self::InvalidUsername => "invalid_username",
Self::InvalidDisplayname => "invalid_displayname",
Self::InvalidEmail => "invalid_email",
Self::EmailUnchanged => "email_unchanged",
Self::InvalidBirthDate => "invalid_birth_date",
Self::UnderMinimumAge { .. } => "under_minimum_age",
Self::InvalidPassword => "invalid_password",
Self::PasswordHash(_) => "password_hash",
Self::UsernameTaken => "username_taken",
Self::EmailTaken => "email_taken",
Self::InvalidEmailCode => "invalid_email_code",
Self::EmailCodeAttemptsExceeded => "email_code_attempts_exceeded",
Self::InvalidResetToken => "invalid_reset_token",
Self::Unauthenticated => "unauthenticated",
Self::Forbidden => "forbidden",
Self::BadRequest(_) => "bad_request",
Self::Conflict(_) => "conflict",
Self::InvalidCredentials => "invalid_credentials",
Self::LoginForbidden { .. } => "login_forbidden",
Self::AccountUnavailable { .. } => "account_unavailable",
Self::MfaRequired => "mfa_required",
Self::TotpNotConfigured => "totp_not_configured",
Self::TotpAlreadyEnabled => "totp_already_enabled",
Self::TotpCodeInvalid => "totp_code_invalid",
Self::TotpStepReused => "totp_step_reused",
Self::RecoveryCodeInvalid => "recovery_code_invalid",
Self::RecoveryCodesExhausted => "recovery_codes_exhausted",
Self::InvalidAccessToken => "invalid_access_token",
Self::AccessTokenNotFound => "access_token_not_found",
Self::InvalidAccessKeyName => "invalid_access_key_name",
Self::InvalidScope => "invalid_scope",
Self::InvalidExpiry => "invalid_expiry",
Self::TooManyAccessKeys => "too_many_access_keys",
Self::SshKeyInvalid => "ssh_key_invalid",
Self::InvalidPermission { .. } => "invalid_permission",
Self::OrganizationNotFound => "organization_not_found",
Self::OrganizationMemberNotFound => "organization_member_not_found",
Self::OrganizationRoleNotFound => "organization_role_not_found",
Self::OrganizationInviteNotFound => "organization_invite_not_found",
Self::OrganizationSlugTaken => "organization_slug_taken",
Self::OrganizationMemberExists => "organization_member_exists",
Self::OrganizationRoleKeyTaken => "organization_role_key_taken",
Self::OrganizationRoleInUse => "organization_role_in_use",
Self::OrganizationRoleSystem => "organization_role_system",
Self::OrganizationInviteExhausted => "organization_invite_exhausted",
Self::OrganizationInviteExpired => "organization_invite_expired",
Self::OrganizationOwnerRequired => "organization_owner_required",
Self::OrganizationHasMembers => "organization_has_members",
Self::OrganizationInactive => "organization_inactive",
Self::OrganizationMemberBanned => "organization_member_banned",
Self::VersionMismatch { .. } => "version_mismatch",
Self::SshKeyTaken => "ssh_key_taken",
Self::SshKeyNameInvalid => "ssh_key_name_invalid",
Self::SshKeyNotFound => "ssh_key_not_found",
Self::AgreementNotFound { .. } => "agreement_not_found",
Self::AgreementNotAccepted { .. } => "agreement_not_accepted",
Self::DeletionAlreadyRequested => "deletion_already_requested",
Self::DeletionNotRequested => "deletion_not_requested",
Self::RateLimited { .. } => "rate_limited",
Self::UserNotFound => "user_not_found",
Self::AppNotFound => "app_not_found",
Self::SessionNotFound => "session_not_found",
Self::TokenNotFound => "token_not_found",
Self::TokenExpired => "token_expired",
Self::TokenRevoked => "token_revoked",
Self::SecurityVersionStale { .. } => "security_version_stale",
Self::ScopeInsufficient { .. } => "scope_insufficient",
Self::Unavailable(_) => "unavailable",
Self::Internal(_) => "internal",
Self::Database(_) => "database",
Self::Cache(_) => "cache",
Self::Session(_) => "session",
Self::Email(_) => "email",
Self::Serialization(_) => "serialization",
}
}
pub fn is_internal(&self) -> bool {
self.status_code().is_server_error()
}
pub fn message(&self) -> String {
if self.is_internal() {
"internal server error".to_string()
} else {
self.to_string()
}
}
}
use std::fmt;
#[derive(Debug)]
pub enum AuthBusinessError {
BadRequest(String),
InvalidCredentials,
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
RateLimited,
Internal(String),
TwoFactorExpired,
TwoFactorCodeInvalid,
RecoveryCodeInvalid,
VerificationCodeExpired,
VerificationCodeInvalid,
ResetTokenInvalid,
}
impl fmt::Display for AuthBusinessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(message) => write!(f, "{message}"),
Self::InvalidCredentials => write!(f, "invalid credentials"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::Forbidden => write!(f, "forbidden"),
Self::NotFound => write!(f, "not found"),
Self::Conflict(message) => write!(f, "{message}"),
Self::RateLimited => write!(f, "rate limit exceeded"),
Self::Internal(message) => write!(f, "{message}"),
Self::TwoFactorExpired => write!(f, "two-factor challenge expired"),
Self::TwoFactorCodeInvalid => write!(f, "invalid two-factor code"),
Self::RecoveryCodeInvalid => write!(f, "invalid or used recovery code"),
Self::VerificationCodeExpired => write!(f, "verification code expired"),
Self::VerificationCodeInvalid => write!(f, "invalid verification code"),
Self::ResetTokenInvalid => write!(f, "invalid or expired reset token"),
}
}
}
impl std::error::Error for AuthBusinessError {}
crate::auth_error_conversion!(AuthBusinessError);
api::db_error_conversions!(AuthBusinessError);
cache::redis_error_conversion!(AuthBusinessError);
api::json_payload_conversion!(AuthBusinessError);
api::page_error_conversion!(AuthBusinessError, BadRequest);
api::api_error! {
log: "auth",
pub enum ApiError {
BadRequest(400, "request failed validation"),
InvalidCredentials(400, "credentials rejected"),
Unauthorized(401, "authentication required"),
Forbidden(403, "operation not allowed"),
NotFound(404, "resource not found"),
Conflict(409, "resource conflict"),
RateLimited(429, "rate limit exceeded"),
PreconditionFailed(412, "precondition failed"),
Unavailable(503, "service unavailable"),
Internal(500, "internal server error"),
}
}
impl From<AuthBusinessError> for ApiError {
fn from(error: AuthBusinessError) -> Self {
match error {
AuthBusinessError::BadRequest(msg) => Self::bad_request(msg),
AuthBusinessError::InvalidCredentials => {
Self::build(400, Self::payload(400, "invalid credentials"))
}
AuthBusinessError::Unauthorized => Self::unauthorized(),
AuthBusinessError::Forbidden => Self::forbidden(),
AuthBusinessError::NotFound => Self::not_found("auth resource not found"),
AuthBusinessError::Conflict(msg) => Self::conflict(msg),
AuthBusinessError::RateLimited => Self::rate_limited_exceeded(),
AuthBusinessError::TwoFactorExpired => Self::build(
400,
Self::payload(400, "two-factor challenge expired")
.with_error_code("two_factor_expired"),
),
AuthBusinessError::TwoFactorCodeInvalid => Self::build(
400,
Self::payload(400, "invalid two-factor code")
.with_error_code("two_factor_code_invalid"),
),
AuthBusinessError::RecoveryCodeInvalid => Self::build(
400,
Self::payload(400, "invalid or used recovery code")
.with_error_code("recovery_code_invalid"),
),
AuthBusinessError::VerificationCodeExpired => Self::build(
400,
Self::payload(400, "verification code expired")
.with_error_code("verification_code_expired"),
),
AuthBusinessError::VerificationCodeInvalid => Self::build(
400,
Self::payload(400, "invalid verification code")
.with_error_code("verification_code_invalid"),
),
AuthBusinessError::ResetTokenInvalid => Self::build(
400,
Self::payload(400, "invalid or expired reset token")
.with_error_code("reset_token_invalid"),
),
AuthBusinessError::Internal(message) => Self::internal(message),
}
}
}
impl From<AuthError> for ApiError {
fn from(error: AuthError) -> Self {
let status = error.status_code().as_u16();
if error.is_internal() {
tracing::warn!(%error, "auth handler failed");
}
let mut response = api::rest::AppResponseData::err(i32::from(status), error.message())
.with_error_code(error.code());
if let AuthError::RateLimited { retry_after_secs } = &error {
response = response.with_retry_after(*retry_after_secs);
}
Self::build(status, response)
}
}