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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions