Skip to content

Commit

Permalink
feat: added optional extra fields to introspection (#443)
Browse files Browse the repository at this point in the history
Co-authored-by: Christoph Bühler <buehler@users.noreply.github.com>
  • Loading branch information
alienscience and buehler committed May 26, 2023
1 parent f5d21f6 commit 68ed151
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 5 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 }
Expand Down
41 changes: 38 additions & 3 deletions src/oidc/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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<String>,
Expand All @@ -34,6 +41,14 @@ pub struct ZitadelIntrospectionExtraTokenFields {
pub email: Option<String>,
pub email_verified: Option<bool>,
pub locale: Option<String>,
#[serde(rename = "urn:zitadel:iam:user:resourceowner:id")]
pub resource_owner_id: Option<String>,
#[serde(rename = "urn:zitadel:iam:user:resourceowner:name")]
pub resource_owner_name: Option<String>,
#[serde(rename = "urn:zitadel:iam:user:resourceowner:primary_domain")]
pub resource_owner_primary_domain: Option<String>,
#[serde(rename = "urn:zitadel:iam:user:metadata")]
pub metadata: Option<HashMap<String, String>>,
}

impl ExtraTokenFields for ZitadelIntrospectionExtraTokenFields {}
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit 68ed151

Please sign in to comment.