Skip to content

Make it possible to Extend IntrospectedUser or just add the ZitadelIntrospectionResponse to IntrospectedUser #578

@OGDeguy

Description

@OGDeguy

Okay, so I am somewhat new to Rust so let me know if I am way off base here.

In my project, I want access to some extra details that I know are being returned in the introspection response. However, when using the rocket integration I am limited to the content of the IntrospectedUser struct:

#[derive(Debug)]
pub struct IntrospectedUser {
    /// UserID of the introspected user (OIDC Field "sub").
    pub user_id: String,
    pub username: Option<String>,
    pub name: Option<String>,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub preferred_username: Option<String>,
    pub email: Option<String>,
    pub email_verified: Option<bool>,
    pub locale: Option<String>,
    pub project_roles: Option<HashMap<String, HashMap<String, String>>>,
    pub metadata: Option<HashMap<String, String>>,
}

This is likely fine for most people, but I want access to the rest of the details in the ZitadelIntrospectionResponse struct. Now one could just extend the struct in the library:

#[derive(Debug)]
pub struct IntrospectedUser {
    /// UserID of the introspected user (OIDC Field "sub").
    pub user_id: String,
    pub username: Option<String>,
    pub name: Option<String>,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub preferred_username: Option<String>,
    pub email: Option<String>,
    pub email_verified: Option<bool>,
    pub locale: Option<String>,
    pub project_roles: Option<HashMap<String, HashMap<String, String>>>,
    pub metadata: Option<HashMap<String, String>>,
    pub response: ZitadelIntrospectionResponse
}

impl From<ZitadelIntrospectionResponse> for IntrospectedUser {
    fn from(response: ZitadelIntrospectionResponse) -> Self {
        Self {
            user_id: response.sub().unwrap().to_string(),
            username: response.username().map(|s| s.to_string()),
            name: response.extra_fields().name.clone(),
            given_name: response.extra_fields().given_name.clone(),
            family_name: response.extra_fields().family_name.clone(),
            preferred_username: response.extra_fields().preferred_username.clone(),
            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(),
            metadata: response.extra_fields().metadata.clone(),
            response: response.clone()
        }
    }
}

Or we could just change the scoping of the zitadel::rocket::introspection::config struct attributes to be public (without the crate scope limitation):

#[derive(Debug)]
pub struct IntrospectionConfig {
    pub authority: String,
    pub authentication: AuthorityAuthentication,
    pub introspection_uri: IntrospectionUrl,
    #[cfg(feature = "introspection_cache")]
    pub cache: Option<Box<dyn IntrospectionCache>>,
}

This small change allows me to just write my own implementation which also functions just fine:

use std::collections::HashMap;
use rocket::{async_trait, Request};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use zitadel::oidc::introspection::{introspect, ZitadelIntrospectionResponse};
use zitadel::rocket::introspection::{IntrospectionConfig, IntrospectionGuardError};
use oauth2::TokenIntrospectionResponse;
///Users/rylanmerritt/.cargo/registry/src/index.crates.io-6f17d22bba15001f/oauth2-4.4.2/src/lib.rs
#[derive(Debug)]
pub struct IntrospectedUser {
    /// UserID of the introspected user (OIDC Field "sub").
    pub user_id: String,
    pub username: Option<String>,
    pub name: Option<String>,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub preferred_username: Option<String>,
    pub email: Option<String>,
    pub email_verified: Option<bool>,
    pub locale: Option<String>,
    pub project_roles: Option<HashMap<String, HashMap<String, String>>>,
    pub metadata: Option<HashMap<String, String>>,
    pub response: Option<ZitadelIntrospectionResponse>,
}


pub type MYIntrospectionResponse = ZitadelIntrospectionResponse;
impl From<MYIntrospectionResponse> for IntrospectedUser {
    fn from(response: MYIntrospectionResponse) -> Self {
        println!("Response: {response:?}");
        Self {
            user_id: response.sub().unwrap().to_string(),
            username: response.username().map(|s| s.to_string()),
            name: response.extra_fields().name.clone(),
            given_name: response.extra_fields().given_name.clone(),
            family_name: response.extra_fields().family_name.clone(),
            preferred_username: response.extra_fields().preferred_username.clone(),
            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(),
            metadata: response.extra_fields().metadata.clone(),
            response: Some(response)
        }
    }
}

#[async_trait]
impl<'request> FromRequest<'request> for &'request IntrospectedUser {
    type Error = &'request IntrospectionGuardError;

    async fn from_request(request: &'request Request<'_>) -> Outcome<Self, Self::Error> {
        let auth: Vec<_> = request.headers().get("authorization").collect();
        if auth.len() > 1 {
            return Outcome::Error((Status::BadRequest, &IntrospectionGuardError::InvalidHeader));
        } else if auth.is_empty() {
            return Outcome::Error((Status::Unauthorized, &IntrospectionGuardError::Unauthorized));
        }

        let token = auth[0];
        if !token.starts_with("Bearer ") {
            return Outcome::Error((Status::Unauthorized, &IntrospectionGuardError::WrongScheme));
        }

        let result = request
            .local_cache_async(async {
                let token = token.replace("Bearer ", "");

                let config = request.rocket().state::<IntrospectionConfig>();
                if config.is_none() {
                    return Err((
                        Status::InternalServerError,
                        IntrospectionGuardError::MissingConfig,
                    ));
                }

                let config = config.unwrap();
                #[cfg(feature = "introspection_cache")]
                let result = async {
                    if let Some(cache) = &config.cache {
                        if let Some(response) = cache.get(&token).await {
                            return Ok(response);
                        }
                    }

                    let response = introspect(
                        &config.introspection_uri,
                        &config.authority,
                        &config.authentication,
                        &token,
                    )
                    .await;

                    if let Some(cache) = &config.cache {
                        if let Ok(response) = &response {
                            cache.set(&token, response.clone()).await;
                        }
                    }

                    response
                }
                .await;

                #[cfg(not(feature = "introspection_cache"))]
                let result = introspect(
                    &config.introspection_uri,
                    &config.authority,
                    &config.authentication,
                    &token,
                )
                .await;

                if let Err(source) = result {
                    return Err((
                        Status::InternalServerError,
                        IntrospectionGuardError::Introspection { source },
                    ));
                }

                let result = result.unwrap();
                match result.active() {
                    true if result.sub().is_some() => Ok(result.into()),
                    false => Err((Status::Unauthorized, IntrospectionGuardError::Inactive)),
                    _ => Err((Status::Unauthorized, IntrospectionGuardError::NoUserId)),
                }
            })
            .await;

        match result {
            Ok(user) => Outcome::Success(user),
            Err((status, error)) => Outcome::Error((*status, error)),
        }
    }
}

I am not sure what approach the community here thinks is best and is ultimately more supportable. Also if I missed an easier way to get the extra details then please let me know :-)

The extra details I am looking for are part of the ZitadelIntrospectionExtraTokenFields struct and having the following URNs
urn:zitadel:iam:user:resourceowner:id urn:zitadel:iam:user:resourceowner:name urn:zitadel:iam:user:resourceowner:primary_domain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions