From 68ed151ab13bd994fc02054ca6a4395f0bdb289a Mon Sep 17 00:00:00 2001 From: Alien Science <16070+alienscience@users.noreply.github.com> Date: Fri, 26 May 2023 10:16:09 +0200 Subject: [PATCH] feat: added optional extra fields to introspection (#443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christoph Bühler --- Cargo.toml | 7 +++++-- src/oidc/introspection.rs | 41 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49504b2..f80295e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,10 @@ interceptors = ["api", "credentials", "dep:time", "dep:tokio"] ## The OIDC module enables basic OIDC (OpenID Connect) features to communicate ## with ZITADEL. Two examples are the `discover` and `introspect` functions. ## The OIDC features are required for some of the web framework features. -oidc = ["credentials", "dep:base64"] +oidc = [ + "credentials", + "dep:base64-compat", +] ## Feature that enables support for the [rocket framework](https://rocket.rs/). ## It enables authentication features for rocket in the form of route guards. @@ -60,7 +63,7 @@ axum = ["credentials", "oidc", "dep:axum", "dep:axum-extra"] [dependencies] axum = { version = "0.6.18", optional = true, features = ["headers", "macros"] } axum-extra = { version = "0.7.4", optional = true } -base64 = { version = "0.21.2", optional = true } +base64-compat = { version = "1", optional = true } custom_error = "1.9.2" document-features = { version = "0.2", optional = true } jsonwebtoken = { version = "8.3.0", optional = true } diff --git a/src/oidc/introspection.rs b/src/oidc/introspection.rs index 389b301..3132890 100644 --- a/src/oidc/introspection.rs +++ b/src/oidc/introspection.rs @@ -5,8 +5,10 @@ use openidconnect::url::{ParseError, Url}; use openidconnect::{ core::CoreTokenType, ExtraTokenFields, HttpRequest, StandardTokenIntrospectionResponse, }; + use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use crate::credentials::{Application, ApplicationError}; @@ -18,13 +20,18 @@ custom_error! { 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}", } /// Introspection response information that is returned by the ZITADEL /// introspection endpoint. Introspection returns the /// [specified information](https://zitadel.com/docs/apis/openidoauth/endpoints#introspect-response) /// by default and [additional claims](https://zitadel.com/docs/apis/openidoauth/claims) -/// if requested by scope. +/// if requested by scope: +/// - When scope contains `urn:zitadel:iam:user:resourceowner`, the fields prefixed with +/// `resource_owner_` are set. +/// - When scope contains `urn:zitadel:iam:user:metadata`, the metadata hashmap will be +/// filled with the user metadata. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ZitadelIntrospectionExtraTokenFields { pub name: Option, @@ -34,6 +41,14 @@ pub struct ZitadelIntrospectionExtraTokenFields { pub email: Option, pub email_verified: Option, pub locale: Option, + #[serde(rename = "urn:zitadel:iam:user:resourceowner:id")] + pub resource_owner_id: Option, + #[serde(rename = "urn:zitadel:iam:user:resourceowner:name")] + 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:user:metadata")] + pub metadata: Option>, } impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields {} @@ -172,8 +187,28 @@ pub async fn introspect( .await .map_err(|source| IntrospectionError::RequestFailed { source })?; - serde_json::from_slice(response.body.as_slice()) - .map_err(|source| IntrospectionError::ParseResponse { source }) + let mut response: ZitadelIntrospectionResponse = + serde_json::from_slice(response.body.as_slice()) + .map_err(|source| IntrospectionError::ParseResponse { source })?; + decode_metadata(&mut response)?; + Ok(response) +} + +// Metadata values are base64 encoded. +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 metadata = HashMap::new(); + for (k, v) in h { + let decoded_v = base64::decode(v) + .map_err(|source| IntrospectionError::DecodeResponse { source })?; + let decoded_v = String::from_utf8_lossy(&decoded_v).into_owned(); + metadata.insert(k.clone(), decoded_v); + } + extra.metadata.replace(metadata); + response.set_extra_fields(extra) + } + Ok(()) } #[cfg(test)]