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;
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",
}
/// Builder for [IntrospectionConfig]s.
/// The authority is mandatory when creating the builder.
/// Then, either one of the authentication mechanisms must be chosen or the
/// builder will throw an error during [build](IntrospectionConfigBuilder::build).
pub struct IntrospectionConfigBuilder {
authority: String,
authentication: Option<AuthorityAuthentication>,
}
impl IntrospectionConfigBuilder {
/// Create a new config builder with the given authority.
/// Returns the chainable config builder.
pub fn new(authority: &str) -> Self {
Self {
authority: authority.to_string(),
authentication: None,
}
}
/// Set the authentication method to [AuthorityAuthentication::Basic].
pub fn with_basic_auth(
&mut self,
client_id: &str,
client_secret: &str,
) -> &mut IntrospectionConfigBuilder {
self.authentication = Some(AuthorityAuthentication::Basic {
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
});
self
}
/// Set the authentication method to [AuthorityAuthentication::JWTProfile]
/// by using the given [Application].
pub fn with_jwt_profile(
&mut self,
application: Application,
) -> &mut IntrospectionConfigBuilder {
self.authentication = Some(AuthorityAuthentication::JWTProfile { application });
self
}
/// Build the [IntrospectionConfig]. This asynchronous method fetches the discovery document
/// of the ZITADEL instance and gets the introspection endpoint.
///
/// ### Errors
///
/// The construction may fail if:
/// - No authentication ([IntrospectionConfigBuilder::with_basic_auth] or
/// [IntrospectionConfigBuilder::with_jwt_profile]) was set for the config.
/// - The [discover] call throws an error.
/// - No introspection endpoint is defined in the discovery document.
///
/// ### Examples
///
/// #### Build config with JWT Profile (recommended)
///
/// ```
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>>{
/// # use zitadel::credentials::Application;
/// # use zitadel::rocket::introspection::IntrospectionConfigBuilder;
/// # const APPLICATION: &str = r#"
/// # {
/// # "type": "application",
/// # "keyId": "181963758610940161",
/// # "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwT2YZJytkkZ1DDM3dcu1OA8YPzHu6XR8HotdMNRnV75GhOT4\nB7zDtdtoP8w/1NHHPEJ859e0kYhrrnKikOKLS6fS1KRsmqR5ZvTq8SlZ2mq3RcX2\nebZx5dQt36INij/WXdsBmjM/yfWvqqWBSb0L/186DaWwmmIxoXWe873vxRmlzblg\nGd8Nu07s9YTREbGPbtFVHEUM6xI4oIe8HJ0e1+JBkiGqk31Cogo0FoAxrOAg0Sf4\n5XiUMYIjzqh8673F9SC4IpVxG22mpFk3vDFuAITaStWYbiH2hPJNKWyX9HDCZb1D\nDqa3wZBDiLqWxh22hNZ6ZIe+3UoSGWsPBH+E1wIDAQABAoIBAD2v5QsRPRN57HmF\njAnNir8nimz6CrN53Pl/MbOZypenBSn9UfReXPeb3+6lzCarBPgGnYsBQAJJU16v\n95daym7PVy1Mg+Ll6F9mhe2Qbr+b23+pj2IRTNC6aB6Aw+PDNzJk7GEGRTG6fWZz\nSQ96Cu9tvcGHiBXwjLlnK+PRWU5IsCiLsjT4xBXsMLMw3YOdMK5z58sqr+SnNEyq\nRHoEvi9aC94WrargVB45Yx+81YNW8uQ5rMDmYaJC5a7ENz522SlAuf4T+fAGJ/HE\n/qbZGD4YwlLqAFDgewQ+5tEWEus3zgY2MIR7vN2zXU1Ptk+mQkXZl/Pxdp7q1xU+\nvr/kcykCgYEAy7MiIAzc1ctQDvkk3HiespzdQ/sC7+CGsBzkyubRc9Oq/YR7GfVK\nGTuDEDlWwx92VAvJGDWRa3T426YDyqiPj66uo836sgL15Uigg5afZun2bqGC78le\nBhSy9b+0YDHPa87GxtKt9UmMoB6WdmoPzOkLEEGS7eesmk2DDgY+QSUCgYEA8tr/\n3PawigL1cxuFpcO1lH6XUspGeAo5yB8FXvfW5g50e37LgooIvOFgUlYuchxwr6uh\nW+CUAWmm4farsgvMBMPYw+PbkCTi/xemiiDmMHUYd7sJkTl0JXApq3pZsNMg4Fw/\n29RynmcG8TGe2dkwrWp1aBYjvIHwEHuNHHTTA0sCgYBtSUFAwsXkaj0cm2y8YHZ8\nS46mv1AXFHYOnKHffjDXnLN7ao2FIsXLfdNWa/zxmLqqYtxUAcFwToSJi6szGnZT\nVxvZRFSBFveIOQvtLW1+EH4nYr3WGko4pvhQwrZqea7YH0skNrogBILPEToWc9bg\nUBOgeB31R7uh2X47kvvphQKBgQDWc60dYnniZVp5mwQZrQjbaC4YXaZ8ugrsPPhx\nNEoAPSN/KihrzZiJsjtsec3p1lNrzRNgHqCT3sgPIdPcFa7DRm5UDRIF54zL1gaq\nUwLyJ3TDxdZc928o4DLryc8J5mZRuSRq6t+MIU5wDnFHzhK+EBQ9Jc/I1rU22ONz\nDXaIoQKBgH14Apggo0o4Eo+OnEBRFbbDulaOfVLPTK9rktikbwO1vzDch8kdcwCU\nsvtRXHjDQL93Ih/8S9aDJZoSDulwr3VUsuDiDEb4jfYmP2sbNO4nIJt+SBMhVOXV\nt7E/uWK28X0GL/bIUzSMMgTfdjhXEtJW+s6hQU1fG+9U1qVTQ2R/\n-----END RSA PRIVATE KEY-----\n",
/// # "appId": "181963751145079041",
/// # "clientId": "181963751145144577@zitadel_rust_test"
/// # }"#;
/// let config = IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud")
/// .with_jwt_profile(Application::load_from_json(APPLICATION).unwrap())
/// .build()
/// .await?;
///
/// println!("{:?}", config);
/// # Ok(())
/// # }
/// ```
///
/// #### Build config with Basic Auth
///
/// ```
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>>{
/// # use zitadel::rocket::introspection::IntrospectionConfigBuilder;
/// let config = IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud")
/// .with_basic_auth(
/// "194339055499018497@zitadel_rust_test",
/// "Ip56oGzxKL1rJ8JaleUVKL7qUlpZ1tqHQYRSd6JE1mTlTJ3pDkDzoObHdZsOg88B",
/// )
/// .build()
/// .await?;
///
/// println!("{:?}", config);
/// # Ok(())
/// # }
/// ```
pub async fn build(&mut self) -> Result<IntrospectionConfig, IntrospectionConfigBuilderError> {
if self.authentication.is_none() {
return Err(IntrospectionConfigBuilderError::NoAuthSchema);
}
let metadata = discover(&self.authority)
.await
.map_err(|source| IntrospectionConfigBuilderError::Discovery { source })?;
let introspection_uri = metadata
.additional_metadata()
.introspection_endpoint
.clone();
if introspection_uri.is_none() {
return Err(IntrospectionConfigBuilderError::NoIntrospectionUrl);
}
Ok(IntrospectionConfig {
authority: self.authority.clone(),
introspection_uri: introspection_uri.unwrap(),
authentication: self.authentication.as_ref().unwrap().clone(),
})
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::all)]
use super::*;
const ZITADEL_URL: &str = "https://zitadel-libraries-l8boqa.zitadel.cloud";
const APPLICATION: &str = r#"
{
"type": "application",
"keyId": "181963758610940161",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAwT2YZJytkkZ1DDM3dcu1OA8YPzHu6XR8HotdMNRnV75GhOT4\nB7zDtdtoP8w/1NHHPEJ859e0kYhrrnKikOKLS6fS1KRsmqR5ZvTq8SlZ2mq3RcX2\nebZx5dQt36INij/WXdsBmjM/yfWvqqWBSb0L/186DaWwmmIxoXWe873vxRmlzblg\nGd8Nu07s9YTREbGPbtFVHEUM6xI4oIe8HJ0e1+JBkiGqk31Cogo0FoAxrOAg0Sf4\n5XiUMYIjzqh8673F9SC4IpVxG22mpFk3vDFuAITaStWYbiH2hPJNKWyX9HDCZb1D\nDqa3wZBDiLqWxh22hNZ6ZIe+3UoSGWsPBH+E1wIDAQABAoIBAD2v5QsRPRN57HmF\njAnNir8nimz6CrN53Pl/MbOZypenBSn9UfReXPeb3+6lzCarBPgGnYsBQAJJU16v\n95daym7PVy1Mg+Ll6F9mhe2Qbr+b23+pj2IRTNC6aB6Aw+PDNzJk7GEGRTG6fWZz\nSQ96Cu9tvcGHiBXwjLlnK+PRWU5IsCiLsjT4xBXsMLMw3YOdMK5z58sqr+SnNEyq\nRHoEvi9aC94WrargVB45Yx+81YNW8uQ5rMDmYaJC5a7ENz522SlAuf4T+fAGJ/HE\n/qbZGD4YwlLqAFDgewQ+5tEWEus3zgY2MIR7vN2zXU1Ptk+mQkXZl/Pxdp7q1xU+\nvr/kcykCgYEAy7MiIAzc1ctQDvkk3HiespzdQ/sC7+CGsBzkyubRc9Oq/YR7GfVK\nGTuDEDlWwx92VAvJGDWRa3T426YDyqiPj66uo836sgL15Uigg5afZun2bqGC78le\nBhSy9b+0YDHPa87GxtKt9UmMoB6WdmoPzOkLEEGS7eesmk2DDgY+QSUCgYEA8tr/\n3PawigL1cxuFpcO1lH6XUspGeAo5yB8FXvfW5g50e37LgooIvOFgUlYuchxwr6uh\nW+CUAWmm4farsgvMBMPYw+PbkCTi/xemiiDmMHUYd7sJkTl0JXApq3pZsNMg4Fw/\n29RynmcG8TGe2dkwrWp1aBYjvIHwEHuNHHTTA0sCgYBtSUFAwsXkaj0cm2y8YHZ8\nS46mv1AXFHYOnKHffjDXnLN7ao2FIsXLfdNWa/zxmLqqYtxUAcFwToSJi6szGnZT\nVxvZRFSBFveIOQvtLW1+EH4nYr3WGko4pvhQwrZqea7YH0skNrogBILPEToWc9bg\nUBOgeB31R7uh2X47kvvphQKBgQDWc60dYnniZVp5mwQZrQjbaC4YXaZ8ugrsPPhx\nNEoAPSN/KihrzZiJsjtsec3p1lNrzRNgHqCT3sgPIdPcFa7DRm5UDRIF54zL1gaq\nUwLyJ3TDxdZc928o4DLryc8J5mZRuSRq6t+MIU5wDnFHzhK+EBQ9Jc/I1rU22ONz\nDXaIoQKBgH14Apggo0o4Eo+OnEBRFbbDulaOfVLPTK9rktikbwO1vzDch8kdcwCU\nsvtRXHjDQL93Ih/8S9aDJZoSDulwr3VUsuDiDEb4jfYmP2sbNO4nIJt+SBMhVOXV\nt7E/uWK28X0GL/bIUzSMMgTfdjhXEtJW+s6hQU1fG+9U1qVTQ2R/\n-----END RSA PRIVATE KEY-----\n",
"appId": "181963751145079041",
"clientId": "181963751145144577@zitadel_rust_test"
}"#;
#[test]
fn create_builder_with_authority() {
let builder = IntrospectionConfigBuilder::new("auth");
assert_eq!(builder.authority, "auth");
assert!(builder.authentication.is_none());
}
#[test]
fn create_builder_with_jwt_auth() {
let mut builder = IntrospectionConfigBuilder::new("auth");
let builder = builder.with_jwt_profile(Application::load_from_json(APPLICATION).unwrap());
assert!(builder.authentication.is_some());
assert!(matches!(
builder.authentication.as_ref().unwrap(),
AuthorityAuthentication::JWTProfile { .. }
));
}
#[test]
fn create_builder_with_basic_auth() {
let mut builder = IntrospectionConfigBuilder::new("auth");
let builder = builder.with_basic_auth("foo", "bar");
assert!(builder.authentication.is_some());
assert!(matches!(
builder.authentication.as_ref().unwrap(),
AuthorityAuthentication::Basic { .. }
));
}
#[tokio::test]
async fn build_throws_on_missing_auth() {
let result = IntrospectionConfigBuilder::new(ZITADEL_URL).build().await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
IntrospectionConfigBuilderError::NoAuthSchema
));
}
#[tokio::test]
async fn build_should_introspect_the_authority() {
let result = IntrospectionConfigBuilder::new(ZITADEL_URL)
.with_jwt_profile(Application::load_from_json(APPLICATION).unwrap())
.build()
.await
.unwrap();
assert_eq!(
result.introspection_uri.to_string(),
"https://zitadel-libraries-l8boqa.zitadel.cloud/oauth/v2/introspect".to_string()
);
}
}