From f132dee35a4e20033a2e7d56b34410c94ddaaae6 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Wed, 17 Apr 2024 16:12:41 +0200 Subject: [PATCH] feat: Fetch project roles from introspection --- src/actix/introspection/config.rs | 2 +- src/axum/introspection/user.rs | 26 ++++++++++++++----- src/oidc/introspection/mod.rs | 42 ++++++++++++++++++++----------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/actix/introspection/config.rs b/src/actix/introspection/config.rs index 7338e24e..75b4f180 100644 --- a/src/actix/introspection/config.rs +++ b/src/actix/introspection/config.rs @@ -3,7 +3,7 @@ use openidconnect::IntrospectionUrl; use crate::oidc::introspection::AuthorityAuthentication; /// Configuration that must be injected into -/// [state](https://actix.rs/docs/application#state) of actix +/// [state](https://actix.rs/docs/application#state) of actix /// to enable the OAuth token introspection authentication method. /// /// Use the [IntrospectionConfigBuilder](super::IntrospectionConfigBuilder) diff --git a/src/axum/introspection/user.rs b/src/axum/introspection/user.rs index 768e0c34..3b0b9e59 100644 --- a/src/axum/introspection/user.rs +++ b/src/axum/introspection/user.rs @@ -1,3 +1,8 @@ +use std::cmp::Eq; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; + use axum::http::StatusCode; use axum::{ async_trait, @@ -11,6 +16,8 @@ use axum_extra::headers::Authorization; use axum_extra::TypedHeader; use custom_error::custom_error; use openidconnect::TokenIntrospectionResponse; +use serde::de::DeserializeOwned; +use serde::Serialize; use serde_json::json; use crate::oidc::introspection::{introspect, IntrospectionError, ZitadelIntrospectionResponse}; @@ -56,7 +63,7 @@ impl IntoResponse for IntrospectionGuardError { /// Struct for the extracted user. The extracted user will always be valid, when fetched in a /// request function arguments. If not the api will return with an appropriate error. #[derive(Debug)] -pub struct IntrospectedUser { +pub struct IntrospectedUser { /// UserID of the introspected user (OIDC Field "sub"). pub user_id: String, pub username: Option, @@ -67,13 +74,15 @@ pub struct IntrospectedUser { pub email: Option, pub email_verified: Option, pub locale: Option, + pub project_roles: Option>>, } #[async_trait] -impl FromRequestParts for IntrospectedUser +impl FromRequestParts for IntrospectedUser where IntrospectionConfig: FromRef, S: Send + Sync, + Role: Hash + Eq + Debug + Serialize + DeserializeOwned + Clone, { type Rejection = IntrospectionGuardError; @@ -85,7 +94,7 @@ where let config = IntrospectionConfig::from_ref(state); - let res = introspect( + let res = introspect::( &config.introspection_uri, &config.authority, &config.authentication, @@ -93,7 +102,7 @@ where ) .await; - let user: Result = match res { + let user: Result, IntrospectionGuardError> = match res { Ok(res) => match res.active() { true if res.sub().is_some() => Ok(res.into()), false => Err(IntrospectionGuardError::Inactive), @@ -106,8 +115,12 @@ where } } -impl From for IntrospectedUser { - fn from(response: ZitadelIntrospectionResponse) -> Self { +impl + From> for IntrospectedUser +where + Role: Hash, +{ + fn from(response: ZitadelIntrospectionResponse) -> Self { Self { user_id: response.sub().unwrap().to_string(), username: response.username().map(|s| s.to_string()), @@ -118,6 +131,7 @@ impl From for IntrospectedUser { email: response.extra_fields().email.clone(), email_verified: response.extra_fields().email_verified, locale: response.extra_fields().locale.clone(), + project_roles: response.extra_fields().project_roles.clone(), } } } diff --git a/src/oidc/introspection/mod.rs b/src/oidc/introspection/mod.rs index 257deeaf..62fa3e03 100644 --- a/src/oidc/introspection/mod.rs +++ b/src/oidc/introspection/mod.rs @@ -7,8 +7,12 @@ use openidconnect::{ }; use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, CONTENT_TYPE}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use std::cmp::Eq; use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; use crate::credentials::{Application, ApplicationError}; @@ -36,7 +40,10 @@ custom_error! { /// - When scope contains `urn:zitadel:iam:user:metadata`, the metadata hashmap will be /// filled with the user metadata. #[derive(Clone, Debug, Serialize, Deserialize, Default)] -pub struct ZitadelIntrospectionExtraTokenFields { +pub struct ZitadelIntrospectionExtraTokenFields +where + Role: Hash + Eq + Clone, +{ pub name: Option, pub given_name: Option, pub family_name: Option, @@ -50,15 +57,20 @@ pub struct ZitadelIntrospectionExtraTokenFields { pub resource_owner_name: Option, #[serde(rename = "urn:zitadel:iam:user:resourceowner:primary_domain")] pub resource_owner_primary_domain: Option, + #[serde(rename = "urn:zitadel:iam:org:project:roles")] + pub project_roles: Option>>, #[serde(rename = "urn:zitadel:iam:user:metadata")] pub metadata: Option>, } -impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields {} +impl ExtraTokenFields + for ZitadelIntrospectionExtraTokenFields +{ +} /// Type alias for the ZITADEL introspection response. -pub type ZitadelIntrospectionResponse = - StandardTokenIntrospectionResponse; +pub type ZitadelIntrospectionResponse = + StandardTokenIntrospectionResponse, CoreTokenType>; /// Definition of the authentication scheme against the authority (or issuer). This authentication /// is required when performing actions like introspection against any ZITADEL instance. @@ -163,7 +175,7 @@ fn payload( /// let token = "dEnGhIFs3VnqcQU5D2zRSeiarB1nwH6goIKY0J8MWZbsnWcTuu1C59lW9DgCq1y096GYdXA"; /// let metadata = discover(authority).await?; /// -/// let result = introspect( +/// let result = introspect::( /// metadata.additional_metadata().introspection_endpoint.as_ref().unwrap(), /// authority, /// &auth, @@ -174,12 +186,12 @@ fn payload( /// # Ok(()) /// # } /// ``` -pub async fn introspect( +pub async fn introspect( introspection_uri: &str, authority: &str, authentication: &AuthorityAuthentication, token: &str, -) -> Result { +) -> Result, IntrospectionError> { let response = async_http_client(HttpRequest { url: Url::parse(introspection_uri) .map_err(|source| IntrospectionError::ParseUrl { source })?, @@ -190,17 +202,19 @@ pub async fn introspect( .await .map_err(|source| IntrospectionError::RequestFailed { source })?; - let mut response: ZitadelIntrospectionResponse = + let mut response: ZitadelIntrospectionResponse = serde_json::from_slice(response.body.as_slice()) .map_err(|source| IntrospectionError::ParseResponse { source })?; - decode_metadata(&mut response)?; + decode_metadata::(&mut response)?; Ok(response) } // Metadata values are base64 encoded. -fn decode_metadata(response: &mut ZitadelIntrospectionResponse) -> Result<(), IntrospectionError> { +fn decode_metadata( + response: &mut ZitadelIntrospectionResponse, +) -> Result<(), IntrospectionError> { if let Some(h) = &response.extra_fields().metadata { - let mut extra = response.extra_fields().clone(); + let mut extra: ZitadelIntrospectionExtraTokenFields = response.extra_fields().clone(); let mut metadata = HashMap::new(); for (k, v) in h { let decoded_v = base64::decode(v) @@ -229,7 +243,7 @@ mod tests { #[tokio::test] async fn introspect_fails_with_invalid_url() { - let result = introspect( + let result = introspect::( "foobar", "foobar", &AuthorityAuthentication::Basic { @@ -250,7 +264,7 @@ mod tests { #[tokio::test] async fn introspect_fails_with_invalid_endpoint() { let meta = discover(ZITADEL_URL).await.unwrap(); - let result = introspect( + let result = introspect::( &meta.token_endpoint().unwrap().to_string(), ZITADEL_URL, &AuthorityAuthentication::Basic { @@ -267,7 +281,7 @@ mod tests { #[tokio::test] async fn introspect_succeeds() { let meta = discover(ZITADEL_URL).await.unwrap(); - let result = introspect( + let result = introspect::( &meta .additional_metadata() .introspection_endpoint