Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
OGDeguy opened this issue Oct 22, 2024 · 0 comments

Comments

@OGDeguy
Copy link

OGDeguy commented Oct 22, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant