mz_frontegg_auth/
error.rs1use std::sync::Arc;
11
12use reqwest::StatusCode;
13use thiserror::Error;
14
15use crate::AppPasswordParseError;
16
17#[derive(Clone, Error, Debug)]
18pub enum Error {
19 #[error(transparent)]
20 InvalidPasswordFormat(#[from] AppPasswordParseError),
21 #[error("invalid token format: {0}")]
22 InvalidTokenFormat(#[from] jsonwebtoken::errors::Error),
23 #[error("authentication token exchange failed: {0}")]
24 ReqwestError(Arc<reqwest::Error>),
25 #[error("middleware programming error: {0}")]
26 MiddlewareError(Arc<anyhow::Error>),
27 #[error("authentication token missing claims")]
28 MissingClaims,
29 #[error("authentication token expired")]
30 TokenExpired,
31 #[error("unauthorized organization")]
32 UnauthorizedTenant,
33 #[error("the app password was not valid")]
34 InvalidAppPassword,
35 #[error("user in access token did not match the expected user")]
36 WrongUser,
37 #[error("user name too long")]
38 UserNameTooLong,
39 #[error("user declared by tenant access token cannot be an email address")]
40 InvalidTenantApiTokenUser,
41 #[error("tenant access token does not declare a user")]
42 MissingTenantApiTokenUser,
43 #[error("request timeout")]
44 Timeout(Arc<tokio::time::error::Elapsed>),
45 #[error("internal error")]
46 Internal(Arc<anyhow::Error>),
47}
48
49impl Error {
50 pub fn is_credential_rejection(&self) -> bool {
57 match self {
58 Error::ReqwestError(e) => e
59 .status()
60 .is_some_and(|s| s == StatusCode::UNAUTHORIZED || s == StatusCode::FORBIDDEN),
61 Error::InvalidPasswordFormat(_)
62 | Error::InvalidAppPassword
63 | Error::UnauthorizedTenant
64 | Error::WrongUser
65 | Error::UserNameTooLong
66 | Error::InvalidTenantApiTokenUser
67 | Error::MissingTenantApiTokenUser => true,
68 Error::InvalidTokenFormat(_)
69 | Error::MissingClaims
70 | Error::TokenExpired
71 | Error::Timeout(_)
72 | Error::MiddlewareError(_)
73 | Error::Internal(_) => false,
74 }
75 }
76}
77
78impl From<anyhow::Error> for Error {
79 fn from(value: anyhow::Error) -> Self {
80 Error::Internal(Arc::new(value))
81 }
82}
83
84impl From<tokio::time::error::Elapsed> for Error {
85 fn from(value: tokio::time::error::Elapsed) -> Self {
86 Error::Timeout(Arc::new(value))
87 }
88}
89
90impl From<reqwest::Error> for Error {
91 fn from(value: reqwest::Error) -> Self {
92 Error::ReqwestError(Arc::new(value))
93 }
94}
95
96impl From<reqwest_middleware::Error> for Error {
97 fn from(value: reqwest_middleware::Error) -> Self {
98 match value {
99 reqwest_middleware::Error::Middleware(e) => Error::MiddlewareError(Arc::new(e)),
100 reqwest_middleware::Error::Reqwest(e) => Error::ReqwestError(Arc::new(e)),
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
108
109 use axum::Router;
110 use axum::http::StatusCode as AxumStatusCode;
111 use axum::routing::post;
112 use tokio::net::TcpListener;
113
114 use super::*;
115
116 #[mz_ore::test(tokio::test)]
117 #[cfg_attr(miri, ignore)] async fn only_rejected_credentials_are_the_clients_fault() {
119 let app = Router::new().route(
121 "/{:status_code}",
122 post(
123 |axum::extract::Path(code): axum::extract::Path<u16>| async move {
124 AxumStatusCode::from_u16(code).unwrap()
125 },
126 ),
127 );
128 let tcp = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
129 .await
130 .unwrap();
131 let addr = tcp.local_addr().unwrap();
132 mz_ore::task::spawn(|| "test-server", async move {
133 axum::serve(tcp, app.into_make_service()).await.unwrap();
134 });
135
136 for (status, rejection) in [
137 (401, true),
138 (403, true),
139 (408, false),
140 (429, false),
141 (503, false),
142 ] {
143 let err = reqwest::Client::new()
144 .post(format!("http://{addr}/{status}"))
145 .send()
146 .await
147 .unwrap()
148 .error_for_status()
149 .expect_err("an error status");
150 assert_eq!(
151 Error::from(err).is_credential_rejection(),
152 rejection,
153 "status {status}"
154 );
155 }
156
157 assert!(!Error::TokenExpired.is_credential_rejection());
159 assert!(!Error::MissingClaims.is_credential_rejection());
160 assert!(Error::WrongUser.is_credential_rejection());
162 assert!(Error::UserNameTooLong.is_credential_rejection());
163 assert!(Error::MissingTenantApiTokenUser.is_credential_rejection());
165 }
166}