-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(actix): add support for actix framework (#538)
Closes #509. This adds actix-web framework support for zitadel-rust. For now, only oauth introspection is supported by using the provided config and request extractor.
- Loading branch information
Showing
11 changed files
with
694 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"details": { | ||
"path": "/authed", | ||
"method": "GET", | ||
"headers": [], | ||
"pathParams": [] | ||
}, | ||
"requests": [ | ||
{ | ||
"contentType": "none" | ||
} | ||
], | ||
"authId": "483e44fe-3c97-4e75-9ec8-0237764b6a3b", | ||
"operationType": "unary", | ||
"invokerName": "rest", | ||
"typeHint": "GET" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"details": { | ||
"path": "/unauthed", | ||
"method": "GET", | ||
"headers": [], | ||
"pathParams": [] | ||
}, | ||
"requests": [ | ||
{ | ||
"contentType": "none" | ||
} | ||
], | ||
"operationType": "unary", | ||
"invokerName": "rest", | ||
"typeHint": "GET" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"settings": [ | ||
{ | ||
"options": { | ||
"rest": { | ||
"endpoint": "http://127.0.0.1:8080", | ||
"pathParams": [] | ||
} | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
use actix_web::{get, App, HttpResponse, HttpServer, Responder}; | ||
use zitadel::actix::introspection::{IntrospectedUser, IntrospectionConfigBuilder}; | ||
|
||
#[get("/unauthed")] | ||
async fn unauthed() -> impl Responder { | ||
println!("Hello Unauthorized User!"); | ||
HttpResponse::Ok().body("Hello Unauthorized User!") | ||
} | ||
|
||
#[get("/authed")] | ||
async fn authed(user: IntrospectedUser) -> impl Responder { | ||
println!("Hello Authorized User!"); | ||
format!( | ||
"Hello Authorized {:?} with id {}", | ||
user.username, user.user_id | ||
) | ||
} | ||
|
||
#[actix_web::main] | ||
async fn main() -> std::io::Result<()> { | ||
println!("Starting server."); | ||
let auth = IntrospectionConfigBuilder::new("https://zitadel-libraries-l8boqa.zitadel.cloud") | ||
.with_basic_auth( | ||
"194339055499018497@zitadel_rust_test", | ||
"Ip56oGzxKL1rJ8JaleUVKL7qUlpZ1tqHQYRSd6JE1mTlTJ3pDkDzoObHdZsOg88B", | ||
) | ||
.build() | ||
.await | ||
.unwrap(); | ||
HttpServer::new(move || { | ||
App::new() | ||
.app_data(auth.clone()) | ||
.service(unauthed) | ||
.service(authed) | ||
}) | ||
.bind(("0.0.0.0", 8080))? | ||
.run() | ||
.await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
use openidconnect::IntrospectionUrl; | ||
|
||
use crate::oidc::introspection::AuthorityAuthentication; | ||
|
||
/// Configuration that must be injected into | ||
/// [state](https://actix.rs/docs/application#state) of actix | ||
/// to enable the OAuth token introspection authentication method. | ||
/// | ||
/// Use the [IntrospectionConfigBuilder](super::IntrospectionConfigBuilder) | ||
/// to construct a config. | ||
#[derive(Clone, Debug)] | ||
pub struct IntrospectionConfig { | ||
pub(crate) authority: String, | ||
pub(crate) authentication: AuthorityAuthentication, | ||
pub(crate) introspection_uri: IntrospectionUrl, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
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", | ||
} | ||
|
||
/// 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::actix::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::actix::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() | ||
); | ||
} | ||
} |
Oops, something went wrong.