diff --git a/Cargo.toml b/Cargo.toml index 48320b6a..34c212c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,7 +151,6 @@ async-trait = { version = "0.1.80", optional = true } axum = { version = "0.8.0", optional = true, features = ["macros"] } axum-extra = { version = "0.12.0", optional = true, features = ["typed-header"] } base64-compat = { version = "1", optional = true } -custom_error = "1.9.2" document-features = { version = "0.2.8", optional = true } jsonwebtoken = { version = "10.3.0", optional = true, features = ["aws_lc_rs"] } moka = { version = "0.12.8", features = ["future"], optional = true } @@ -165,6 +164,7 @@ rocket = { version = "0.5.0", optional = true } serde = { version = "1.0.200", features = ["derive"], optional = true } serde_json = { version = "1.0.116", optional = true } serde_urlencoded = { version = "0.7.1", optional = true } +thiserror = "2.0.19" time = { version = "0.3.36", optional = true } tokio = { version = "1.37.0", optional = true, features = [ "macros", diff --git a/examples/axum_webapi_oauth_interception_basic.rs b/examples/axum_webapi_oauth_interception_basic.rs index c41cf5dc..f59dfe35 100644 --- a/examples/axum_webapi_oauth_interception_basic.rs +++ b/examples/axum_webapi_oauth_interception_basic.rs @@ -19,7 +19,7 @@ async fn authed(user: IntrospectedUser) -> impl IntoResponse { } #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { let is = IntrospectionStateBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud") .with_basic_auth( "194339055499018497@zitadel_rust_test", diff --git a/examples/fetch_profile_with_pat.rs b/examples/fetch_profile_with_pat.rs index b7110f6b..c5eb4369 100644 --- a/examples/fetch_profile_with_pat.rs +++ b/examples/fetch_profile_with_pat.rs @@ -1,7 +1,7 @@ use zitadel::api::{clients::ClientBuilder, zitadel::auth::v1::GetMyUserRequest}; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { const PERSONAL_ACCESS_TOKEN: &str = "dEnGhIFs3VnqcQU5D2zRSeiarB1nwH6goIKY0J8MWZbsnWcTuu1C59lW9DgCq1y096GYdXA"; const ZITADEL_URL: &str = "https://zitadel-libraries-l8boqa.zitadel.cloud"; diff --git a/examples/fetch_profile_with_service_account.rs b/examples/fetch_profile_with_service_account.rs index 794a9258..ca67c93a 100644 --- a/examples/fetch_profile_with_service_account.rs +++ b/examples/fetch_profile_with_service_account.rs @@ -4,7 +4,7 @@ use zitadel::{ }; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { const SERVICE_ACCOUNT: &str = r#" { "type": "serviceaccount", diff --git a/examples/service_account_authentication.rs b/examples/service_account_authentication.rs index d54be726..b2fcca67 100644 --- a/examples/service_account_authentication.rs +++ b/examples/service_account_authentication.rs @@ -1,7 +1,7 @@ use zitadel::credentials::ServiceAccount; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { const SERVICE_ACCOUNT: &str = r#" { "type": "serviceaccount", diff --git a/src/actix/introspection/config_builder.rs b/src/actix/introspection/config_builder.rs index fa596826..0938e994 100644 --- a/src/actix/introspection/config_builder.rs +++ b/src/actix/introspection/config_builder.rs @@ -1,16 +1,21 @@ -use custom_error::custom_error; - use crate::actix::introspection::config::IntrospectionConfig; use crate::credentials::Application; use crate::oidc::discovery::{discover, DiscoveryError}; use crate::oidc::introspection::AuthorityAuthentication; - -custom_error! { - /// Error type for introspection config builder related errors. - pub IntrospectionConfigBuilderError - NoAuthSchema = "no authentication for authority defined", - Discovery{source: DiscoveryError} = "could not fetch discovery document: {source}", - NoIntrospectionUrl = "discovery document did not contain an introspection url", +use thiserror::Error; + +/// Error type for introspection config builder related errors. +#[derive(Debug, Error)] +pub enum IntrospectionConfigBuilderError { + #[error("no authentication for authority defined")] + NoAuthSchema, + #[error("could not fetch discovery document: {source}")] + Discovery { + #[from] + source: DiscoveryError, + }, + #[error("discovery document did not contain an introspection url")] + NoIntrospectionUrl, } /// Builder for [IntrospectionConfig]s. @@ -74,7 +79,7 @@ impl IntrospectionConfigBuilder { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # use zitadel::credentials::Application; /// # use zitadel::actix::introspection::IntrospectionConfigBuilder; /// # const APPLICATION: &str = r#" @@ -99,7 +104,7 @@ impl IntrospectionConfigBuilder { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # use zitadel::actix::introspection::IntrospectionConfigBuilder; /// let config = IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud") /// .with_basic_auth( diff --git a/src/actix/introspection/extractor.rs b/src/actix/introspection/extractor.rs index 4089a91f..a36486d1 100644 --- a/src/actix/introspection/extractor.rs +++ b/src/actix/introspection/extractor.rs @@ -1,25 +1,34 @@ use std::{future::Future, pin::Pin}; +use crate::actix::introspection::config::IntrospectionConfig; +use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use actix_web::dev::Payload; use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; use actix_web::{Error, FromRequest, HttpRequest}; -use custom_error::custom_error; use openidconnect::TokenIntrospectionResponse; use std::collections::HashMap; - -use crate::actix::introspection::config::IntrospectionConfig; -use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; - -custom_error! { - /// Error type for extractor related errors. - pub IntrospectionExtractorError - MissingConfig = "no introspection config given to actix app data", - Unauthorized = "no HTTP authorization header found", - InvalidHeader = "authorization header is invalid", - WrongScheme = "Authorization header is not a bearer token", - Introspection{source: IntrospectionError} = "introspection returned an error: {source}", - Inactive = "access token is inactive", - NoUserId = "introspection result contained no user id", +use thiserror::Error; + +/// Error type for extractor related errors. +#[derive(Debug, Error)] +pub enum IntrospectionExtractorError { + #[error("no introspection config given to actix app data")] + MissingConfig, + #[error("no HTTP authorization header found")] + Unauthorized, + #[error("authorization header is invalid")] + InvalidHeader, + #[error("Authorization header is not a bearer token")] + WrongScheme, + #[error("introspection returned an error: {source}")] + Introspection { + #[from] + source: IntrospectionError, + }, + #[error("access token is inactive")] + Inactive, + #[error("introspection result contained no user id")] + NoUserId, } /// Struct for the handler function that requires an authenticated user. diff --git a/src/api/clients.rs b/src/api/clients.rs index 89e8b380..cc26e97c 100644 --- a/src/api/clients.rs +++ b/src/api/clients.rs @@ -4,8 +4,7 @@ //! specific interceptors for authentication. use std::error::Error; - -use custom_error::custom_error; +use thiserror::Error; use tonic::codegen::{Body, Bytes, InterceptedService, StdError}; use tonic::service::Interceptor; @@ -37,12 +36,15 @@ use crate::api::zitadel::system::v1::system_service_client::SystemServiceClient; #[cfg(feature = "interceptors")] use crate::credentials::{AuthenticationOptions, ServiceAccount}; -custom_error! { - /// Errors that may occur when creating a client. - pub ClientError - InvalidUrl = "the provided url is invalid", - ConnectionError = "could not connect to provided endpoint", - TlsInitializationError = "could not setup tls connection", +/// Errors that may occur when creating a client. +#[derive(Debug, Error)] +pub enum ClientError { + #[error("the provided url is invalid")] + InvalidUrl, + #[error("could not connect to provided endpoint")] + ConnectionError, + #[error("could not setup tls connection")] + TlsInitializationError, } /// A builder to create configured gRPC clients for ZITADEL API access. @@ -146,7 +148,9 @@ where /// cannot be parsed into a valid URL or if the connection to the endpoint /// is not possible. #[cfg(feature = "api-admin-v1")] - pub async fn build_admin_client(self) -> Result, Box> { + pub async fn build_admin_client( + self, + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -161,7 +165,9 @@ where /// cannot be parsed into a valid URL or if the connection to the endpoint /// is not possible. #[cfg(feature = "api-auth-v1")] - pub async fn build_auth_client(self) -> Result, Box> { + pub async fn build_auth_client( + self, + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -178,7 +184,7 @@ where #[cfg(feature = "api-management-v1")] pub async fn build_management_client( self, - ) -> Result, Box> { + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -193,7 +199,9 @@ where /// cannot be parsed into a valid URL or if the connection to the endpoint /// is not possible. #[cfg(feature = "api-oidc-v2")] - pub async fn build_oidc_client(self) -> Result, Box> { + pub async fn build_oidc_client( + self, + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -210,7 +218,7 @@ where #[cfg(feature = "api-org-v2")] pub async fn build_organization_client( self, - ) -> Result, Box> { + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -227,7 +235,7 @@ where #[cfg(feature = "api-session-v2")] pub async fn build_session_client( self, - ) -> Result, Box> { + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -244,7 +252,7 @@ where #[cfg(feature = "api-settings-v2")] pub async fn build_settings_client( self, - ) -> Result, Box> { + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -261,7 +269,7 @@ where #[cfg(feature = "api-system-v1")] pub async fn build_system_client( self, - ) -> Result, Box> { + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); @@ -276,7 +284,9 @@ where /// cannot be parsed into a valid URL or if the connection to the endpoint /// is not possible. #[cfg(feature = "api-user-v2")] - pub async fn build_user_client(self) -> Result, Box> { + pub async fn build_user_client( + self, + ) -> Result, Box> { let channel = self .interceptor .build_service(get_channel(&self.api_endpoint).await?); diff --git a/src/api/interceptors.rs b/src/api/interceptors.rs index 52f93d9f..7d6ac272 100644 --- a/src/api/interceptors.rs +++ b/src/api/interceptors.rs @@ -31,7 +31,7 @@ use crate::credentials::{AuthenticationOptions, ServiceAccount}; /// /// ``` /// # #[tokio::main] -/// # async fn main() -> Result<(), Box> { +/// # async fn main() -> Result<(), Box> { /// use zitadel::api::{ /// clients::ClientBuilder, zitadel::auth::v1::GetMyUserRequest, /// }; @@ -99,7 +99,7 @@ impl Interceptor for AccessTokenInterceptor { /// /// ``` /// # #[tokio::main] -/// # async fn main() -> Result<(), Box> { +/// # async fn main() -> Result<(), Box> { /// use zitadel::{ /// api::{clients::ClientBuilder, zitadel::auth::v1::GetMyUserRequest}, /// credentials::{AuthenticationOptions, ServiceAccount}, diff --git a/src/axum/introspection/state_builder.rs b/src/axum/introspection/state_builder.rs index 7d0fcae3..0dba6d04 100644 --- a/src/axum/introspection/state_builder.rs +++ b/src/axum/introspection/state_builder.rs @@ -1,22 +1,27 @@ -use custom_error::custom_error; -use std::sync::Arc; - use crate::axum::introspection::state::IntrospectionConfig; use crate::credentials::Application; use crate::oidc::discovery::{discover, DiscoveryError}; use crate::oidc::introspection::AuthorityAuthentication; +use std::sync::Arc; +use thiserror::Error; #[cfg(feature = "introspection_cache")] use crate::oidc::introspection::cache::IntrospectionCache; use super::state::IntrospectionState; -custom_error! { - /// Error type for introspection config builder related errors. - pub IntrospectionStateBuilderError - NoAuthSchema = "no authentication for authority defined", - Discovery{source: DiscoveryError} = "could not fetch discovery document: {source}", - NoIntrospectionUrl = "discovery document did not contain an introspection url", +/// Error type for introspection config builder related errors. +#[derive(Debug, Error)] +pub enum IntrospectionStateBuilderError { + #[error("no authentication for authority defined")] + NoAuthSchema, + #[error("could not fetch discovery document: {source}")] + Discovery { + #[from] + source: DiscoveryError, + }, + #[error("discovery document did not contain an introspection url")] + NoIntrospectionUrl, } pub struct IntrospectionStateBuilder { diff --git a/src/axum/introspection/user.rs b/src/axum/introspection/user.rs index 959a44e1..a53838e3 100644 --- a/src/axum/introspection/user.rs +++ b/src/axum/introspection/user.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fmt::Debug; use crate::axum::introspection::IntrospectionState; +use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use axum::http::StatusCode; use axum::{ extract::{FromRef, FromRequestParts}, @@ -12,22 +13,30 @@ use axum::{ use axum_extra::headers::authorization::Bearer; use axum_extra::headers::Authorization; use axum_extra::TypedHeader; -use custom_error::custom_error; use openidconnect::TokenIntrospectionResponse; use serde_json::json; - -use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; - -custom_error! { - /// Error type for guard related errors. - pub IntrospectionGuardError - MissingConfig = "no introspection config given to rocket managed state", - Unauthorized = "no HTTP authorization header found", - InvalidHeader = "authorization header is invalid", - WrongScheme = "Authorization header is not a bearer token", - Introspection{source: IntrospectionError} = "introspection returned an error: {source}", - Inactive = "access token is inactive", - NoUserId = "introspection result contained no user id", +use thiserror::Error; + +/// Error type for guard related errors. +#[derive(Debug, Error)] +pub enum IntrospectionGuardError { + #[error("no introspection config given to rocket managed state")] + MissingConfig, + #[error("no HTTP authorization header found")] + Unauthorized, + #[error("authorization header is invalid")] + InvalidHeader, + #[error("Authorization header is not a bearer token")] + WrongScheme, + #[error("introspection returned an error: {source}")] + Introspection { + #[from] + source: IntrospectionError, + }, + #[error("access token is inactive")] + Inactive, + #[error("introspection result contained no user id")] + NoUserId, } impl IntoResponse for IntrospectionGuardError { diff --git a/src/credentials/application.rs b/src/credentials/application.rs index bfe4b1a2..0b226a81 100644 --- a/src/credentials/application.rs +++ b/src/credentials/application.rs @@ -1,10 +1,9 @@ -use custom_error::custom_error; +use crate::credentials::jwt::JwtClaims; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use std::fs::read_to_string; use std::path::Path; - -use crate::credentials::jwt::JwtClaims; +use thiserror::Error; /// Application for [ZITADEL](https://zitadel.ch/). An application is an OIDC application type /// that allows a backend (for example an API for some single page application) to @@ -28,12 +27,24 @@ pub struct Application { key: String, } -custom_error! { - /// Error type for application credential related errors. - pub ApplicationError - Io{source: std::io::Error} = "unable to read from file: {source}", - Json{source: serde_json::Error} = "could not parse json: {source}", - Key{source: jsonwebtoken::errors::Error} = "could not parse RSA key: {source}", +/// Error type for application credential related errors. +#[derive(Debug, Error)] +pub enum ApplicationError { + #[error("unable to read from file: {source}")] + Io { + #[from] + source: std::io::Error, + }, + #[error("could not parse json: {source}")] + Json { + #[from] + source: serde_json::Error, + }, + #[error("could not parse RSA key: {source}")] + Key { + #[from] + source: jsonwebtoken::errors::Error, + }, } impl Application { @@ -51,7 +62,7 @@ impl Application { /// use zitadel::credentials::Application; /// let application = Application::load_from_file("./my_json_key.json")?; /// println!("{:#?}", application); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn load_from_file>(file_path: P) -> Result { let data = read_to_string(file_path).map_err(|e| ApplicationError::Io { source: e })?; @@ -71,7 +82,7 @@ impl Application { /// use zitadel::credentials::Application; /// let application = Application::load_from_json(r#"{"keyId": "1337", "clientId": "testing", "userId": "42", "key": "foobar", "appId": "myapp"}"#)?; /// println!("{:#?}", application); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn load_from_json(json: &str) -> Result { let sa: Application = diff --git a/src/credentials/service_account.rs b/src/credentials/service_account.rs index e37033f5..b3968f82 100644 --- a/src/credentials/service_account.rs +++ b/src/credentials/service_account.rs @@ -1,4 +1,4 @@ -use custom_error::custom_error; +use crate::credentials::jwt::JwtClaims; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use openidconnect::{ core::{CoreProviderMetadata, CoreTokenType}, @@ -11,8 +11,7 @@ use reqwest::{ }; use serde::{Deserialize, Serialize}; use std::fs::read_to_string; - -use crate::credentials::jwt::JwtClaims; +use thiserror::Error; /// A service account for [ZITADEL](https://zitadel.ch/). The service /// account can be loaded from a valid JSON string or from a file containing the JSON string. @@ -63,19 +62,46 @@ pub struct AuthenticationOptions { pub project_audiences: Vec, } -custom_error! { - /// Error type for service account related errors. - pub ServiceAccountError - Io{source: std::io::Error} = "unable to read from file: {source}", - Json{source: serde_json::Error} = "could not parse json: {source}", - Key{source: jsonwebtoken::errors::Error} = "could not parse RSA key: {source}", - AudienceUrl{source: openidconnect::url::ParseError} = "audience url could not be parsed: {source}", - DiscoveryError{source: Box} = "could not discover OIDC document: {source}", - TokenEndpointMissing = "OIDC document does not contain token endpoint", - HttpError{source: openidconnect::reqwest::Error} = "http error: {source}", - UrlEncodeError = "could not encode url params for token request", - TokenError = "could not fetch token from endpoint", - AccessTokenMissing = "token response does not contain access token", +/// Error type for service account related errors. +#[derive(Debug, Error)] +pub enum ServiceAccountError { + #[error("unable to read from file: {source}")] + Io { + #[from] + source: std::io::Error, + }, + #[error("could not parse json: {source}")] + Json { + #[from] + source: serde_json::Error, + }, + #[error("could not parse RSA key: {source}")] + Key { + #[from] + source: jsonwebtoken::errors::Error, + }, + #[error("audience url could not be parsed: {source}")] + AudienceUrl { + #[from] + source: openidconnect::url::ParseError, + }, + #[error("could not discover OIDC document: {source}")] + DiscoveryError { + source: Box, + }, + #[error("OIDC document does not contain token endpoint")] + TokenEndpointMissing, + #[error("http error: {source}")] + HttpError { + #[from] + source: openidconnect::reqwest::Error, + }, + #[error("could not encode url params for token request")] + UrlEncodeError, + #[error("could not fetch token from endpoint")] + TokenError, + #[error("token response does not contain access token")] + AccessTokenMissing, } impl ServiceAccount { @@ -93,7 +119,7 @@ impl ServiceAccount { /// use zitadel::credentials::ServiceAccount; /// let service_account = ServiceAccount::load_from_file("./my_json_key.json")?; /// println!("{:#?}", service_account); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn load_from_file(file_path: &str) -> Result { let data = read_to_string(file_path).map_err(|e| ServiceAccountError::Io { source: e })?; @@ -113,7 +139,7 @@ impl ServiceAccount { /// use zitadel::credentials::ServiceAccount; /// let service_account = ServiceAccount::load_from_json(r#"{"keyId": "1337", "userId": "42", "key": "foobar"}"#)?; /// println!("{:#?}", service_account); - /// # Ok::<(), Box>(()) + /// # Ok::<(), Box>(()) /// ``` pub fn load_from_json(json: &str) -> Result { let sa: ServiceAccount = @@ -142,7 +168,7 @@ impl ServiceAccount { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # const SERVICE_ACCOUNT: &str = r#" /// # { /// # "type": "serviceaccount", @@ -186,7 +212,7 @@ impl ServiceAccount { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # const SERVICE_ACCOUNT: &str = r#" /// # { /// # "type": "serviceaccount", @@ -211,7 +237,7 @@ impl ServiceAccount { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # const SERVICE_ACCOUNT: &str = r#" /// # { /// # "type": "serviceaccount", diff --git a/src/oidc/discovery.rs b/src/oidc/discovery.rs index 2265fab0..3fbcc009 100644 --- a/src/oidc/discovery.rs +++ b/src/oidc/discovery.rs @@ -1,4 +1,3 @@ -use custom_error::custom_error; use openidconnect::{ core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, @@ -8,13 +7,23 @@ use openidconnect::{ url, AdditionalProviderMetadata, IntrospectionUrl, IssuerUrl, ProviderMetadata, RevocationUrl, }; use serde::{Deserialize, Serialize}; +use thiserror::Error; -custom_error! { - /// Error type for discovery related errors. - pub DiscoveryError - IssuerUrl{source: url::ParseError} = "could not parse issuer url: {source}", - DiscoveryDocument = "could not discover OIDC document", - DiscoveryClientError{source: reqwest::Error} = "could not fetch discovery document: {source}", +/// Error type for discovery related errors. +#[derive(Debug, Error)] +pub enum DiscoveryError { + #[error("could not parse issuer url: {source}")] + IssuerUrl { + #[from] + source: url::ParseError, + }, + #[error("could not discover OIDC document")] + DiscoveryDocument, + #[error("could not fetch discovery document: {source}")] + DiscoveryClientError { + #[from] + source: reqwest::Error, + }, } /// Fetch the well-known [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) @@ -38,7 +47,7 @@ custom_error! { /// /// ``` /// # #[tokio::main] -/// # async fn main() -> Result<(), Box>{ +/// # async fn main() -> Result<(), Box>{ /// use zitadel::oidc::discovery::discover; /// let authority = "https://zitadel-libraries-l8boqa.zitadel.cloud"; /// let metadata = discover(authority).await?; diff --git a/src/oidc/introspection/mod.rs b/src/oidc/introspection/mod.rs index a1c39df1..69ae3de6 100644 --- a/src/oidc/introspection/mod.rs +++ b/src/oidc/introspection/mod.rs @@ -1,6 +1,5 @@ use crate::credentials::{Application, ApplicationError}; use crate::oidc::discovery::{discover, DiscoveryError}; -use custom_error::custom_error; use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet}; use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Header, TokenData, Validation}; use openidconnect::url::{ParseError, Url}; @@ -14,30 +13,75 @@ use std::collections::HashMap; use std::error::Error; use std::fmt::{Debug, Display}; use std::str::FromStr; +use thiserror::Error; #[cfg(feature = "introspection_cache")] pub mod cache; -custom_error! { - /// Error type for introspection related errors. - pub IntrospectionError - HttpClientError{source: reqwest::Error} = "could not create http client: {source}", - RequestFailed{origin: String, source: openidconnect::reqwest::Error} = "{origin} request did fail: {source}", - PayloadSerialization = "could not correctly serialize introspection payload", - JWTProfile{source: ApplicationError} = "could not create signed jwt key: {source}", - ParseUrl{source: ParseError} = "could not parse url: {source}", - ParseResponse{source: serde_json::Error} = "could not parse introspection response: {source}", - DecodeResponse{source: base64::DecodeError} = "could not decode base64 metadata: {source}", - ResponseError{source: ZitadelResponseError} = "received error response from Zitadel: {source}", - DiscoveryError{source: DiscoveryError} = "Discovery error during introspection: {source}", - JWTUnsupportedAlgorithm = "unsupported algorithm in JWT", - MissingJwksKey = "missing key in jwks", - JsonWebTokenErrors{source: jsonwebtoken::errors::Error} = @{ match source.kind() { +/// Error type for introspection related errors. +#[derive(Debug, Error)] +pub enum IntrospectionError { + #[error("could not create http client: {source}")] + HttpClientError { + #[from] + source: reqwest::Error, + }, + #[error("{origin} request did fail: {source}")] + RequestFailed { + origin: String, + source: openidconnect::reqwest::Error, + }, + #[error("could not correctly serialize introspection payload")] + PayloadSerialization, + #[error("could not create signed jwt key: {source}")] + JWTProfile { + #[from] + source: ApplicationError, + }, + #[error("could not parse url: {source}")] + ParseUrl { + #[from] + source: ParseError, + }, + #[error("could not parse introspection response: {source}")] + ParseResponse { + #[from] + source: serde_json::Error, + }, + #[error("could not decode base64 metadata: {source}")] + DecodeResponse { + #[from] + source: base64::DecodeError, + }, + #[error("received error response from Zitadel: {source}")] + ResponseError { + #[from] + source: ZitadelResponseError, + }, + #[error("Discovery error during introspection: {source}")] + DiscoveryError { + #[from] + source: DiscoveryError, + }, + #[error("unsupported algorithm in JWT")] + JWTUnsupportedAlgorithm, + #[error("missing key in jwks")] + MissingJwksKey, + #[error("{}", Self::format_jsonwebtoken_error(&source))] + JsonWebTokenErrors { + #[from] + source: jsonwebtoken::errors::Error, + }, +} + +impl IntrospectionError { + fn format_jsonwebtoken_error(err: &jsonwebtoken::errors::Error) -> &str { + match err.kind() { jsonwebtoken::errors::ErrorKind::InvalidToken => "Invalid JWT string, missing the 3 . segments, JKWS validation won't work with opaque tokens, make sure you've switched to JWT tokens or use instead the instropect method", jsonwebtoken::errors::ErrorKind::InvalidAlgorithmName => "Invalid Algorithm in JWKS", _ => "Other JWT error" - }}, - + } + } } /// Introspection response information that is returned by the ZITADEL @@ -239,7 +283,7 @@ fn payload( /// /// ``` /// # #[tokio::main] -/// # async fn main() -> Result<(), Box>{ +/// # async fn main() -> Result<(), Box>{ /// # use zitadel::oidc::discovery::discover; /// # use zitadel::oidc::introspection::{AuthorityAuthentication, introspect}; /// let auth = AuthorityAuthentication::Basic { diff --git a/src/rocket/introspection/config_builder.rs b/src/rocket/introspection/config_builder.rs index ac7db3a8..13067bc2 100644 --- a/src/rocket/introspection/config_builder.rs +++ b/src/rocket/introspection/config_builder.rs @@ -1,21 +1,25 @@ -use custom_error::custom_error; - use crate::credentials::Application; use crate::oidc::discovery::{discover, DiscoveryError}; use crate::oidc::introspection::AuthorityAuthentication; use crate::rocket::introspection::config::IntrospectionConfig; +use thiserror::Error; #[cfg(feature = "introspection_cache")] use crate::oidc::introspection::cache::IntrospectionCache; -custom_error! { - /// Error type for introspection config builder related errors. - pub IntrospectionConfigBuilderError - NoAuthSchema = "no authentication for authority defined", - Discovery{source: DiscoveryError} = "could not fetch discovery document: {source}", - NoIntrospectionUrl = "discovery document did not contain an introspection url", +/// Error type for introspection config builder related errors. +#[derive(Debug, Error)] +pub enum IntrospectionConfigBuilderError { + #[error("no authentication for authority defined")] + NoAuthSchema, + #[error("could not fetch discovery document: {source}")] + Discovery { + #[from] + source: DiscoveryError, + }, + #[error("discovery document did not contain an introspection url")] + NoIntrospectionUrl, } - /// Builder for [IntrospectionConfig]s. /// The authority is mandatory when creating the builder. /// Then, either one of the authentication mechanisms must be chosen or the @@ -92,7 +96,7 @@ impl IntrospectionConfigBuilder { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # use zitadel::credentials::Application; /// # use zitadel::rocket::introspection::IntrospectionConfigBuilder; /// # const APPLICATION: &str = r#" @@ -117,7 +121,7 @@ impl IntrospectionConfigBuilder { /// /// ``` /// # #[tokio::main] - /// # async fn main() -> Result<(), Box>{ + /// # async fn main() -> Result<(), Box>{ /// # use zitadel::rocket::introspection::IntrospectionConfigBuilder; /// let config = IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud") /// .with_basic_auth( diff --git a/src/rocket/introspection/guard.rs b/src/rocket/introspection/guard.rs index 23ae4245..9200fb22 100644 --- a/src/rocket/introspection/guard.rs +++ b/src/rocket/introspection/guard.rs @@ -1,4 +1,3 @@ -use custom_error::custom_error; use openidconnect::TokenIntrospectionResponse; use rocket::figment::Figment; use rocket::http::Status; @@ -7,6 +6,7 @@ use rocket::{async_trait, Request}; use std::collections::BTreeSet; use std::collections::HashMap; +use super::config::IntrospectionRocketConfig; #[cfg(feature = "rocket_okapi")] use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; use crate::rocket::introspection::IntrospectionConfig; @@ -22,19 +22,28 @@ use rocket_okapi::{ }; #[cfg(feature = "rocket_okapi")] use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject}; - -use super::config::IntrospectionRocketConfig; - -custom_error! { - /// Error type for guard related errors. - pub IntrospectionGuardError - MissingConfig = "no introspection config given to rocket managed state", - Unauthorized = "no HTTP authorization header found", - InvalidHeader = "authorization header is invalid", - WrongScheme = "Authorization header is not a bearer token", - Introspection{source: IntrospectionError} = "introspection returned an error: {source}", - Inactive = "access token is inactive", - NoUserId = "introspection result contained no user id", +use thiserror::Error; + +/// Error type for guard related errors. +#[derive(Debug, Error)] +pub enum IntrospectionGuardError { + #[error("no introspection config given to rocket managed state")] + MissingConfig, + #[error("no HTTP authorization header found")] + Unauthorized, + #[error("authorization header is invalid")] + InvalidHeader, + #[error("Authorization header is not a bearer token")] + WrongScheme, + #[error("introspection returned an error: {source}")] + Introspection { + #[from] + source: IntrospectionError, + }, + #[error("access token is inactive")] + Inactive, + #[error("introspection result contained no user id")] + NoUserId, } /// Struct for the injected route guard that requires an authenticated user.