Can define additional claims on per-client basis
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		@@ -16,7 +16,7 @@ use crate::constants::*;
 | 
			
		||||
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
 | 
			
		||||
use crate::data::action_logger::{Action, ActionLogger};
 | 
			
		||||
use crate::data::app_config::AppConfig;
 | 
			
		||||
use crate::data::client::{AuthenticationFlow, ClientID, ClientManager};
 | 
			
		||||
use crate::data::client::{AdditionalClaims, AuthenticationFlow, ClientID, ClientManager};
 | 
			
		||||
use crate::data::code_challenge::CodeChallenge;
 | 
			
		||||
use crate::data::current_user::CurrentUser;
 | 
			
		||||
use crate::data::id_token::IdToken;
 | 
			
		||||
@@ -270,6 +270,7 @@ pub async fn authorize(
 | 
			
		||||
                auth_time: SessionIdentity(Some(&id)).auth_time(),
 | 
			
		||||
                nonce: query.nonce.clone(),
 | 
			
		||||
                email: user.email.clone(),
 | 
			
		||||
                additional_claims: client.claims_id_token(&user),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            log::trace!("New OpenID id token: {:#?}", &id_token);
 | 
			
		||||
@@ -533,7 +534,8 @@ pub async fn token(
 | 
			
		||||
                issued_at: time(),
 | 
			
		||||
                auth_time: session.auth_time,
 | 
			
		||||
                nonce: session.nonce,
 | 
			
		||||
                email: user.email,
 | 
			
		||||
                email: user.email.to_string(),
 | 
			
		||||
                additional_claims: client.claims_id_token(&user),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            OpenIDTokenResponse {
 | 
			
		||||
@@ -641,6 +643,7 @@ pub async fn user_info_post(
 | 
			
		||||
    query: web::Query<UserInfoQuery>,
 | 
			
		||||
    sessions: web::Data<Addr<OpenIDSessionsActor>>,
 | 
			
		||||
    users: web::Data<Addr<UsersActor>>,
 | 
			
		||||
    clients: web::Data<Arc<ClientManager>>,
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    user_info(
 | 
			
		||||
        req,
 | 
			
		||||
@@ -649,6 +652,7 @@ pub async fn user_info_post(
 | 
			
		||||
            .or(query.0.access_token),
 | 
			
		||||
        sessions,
 | 
			
		||||
        users,
 | 
			
		||||
        clients,
 | 
			
		||||
    )
 | 
			
		||||
    .await
 | 
			
		||||
}
 | 
			
		||||
@@ -658,8 +662,18 @@ pub async fn user_info_get(
 | 
			
		||||
    query: web::Query<UserInfoQuery>,
 | 
			
		||||
    sessions: web::Data<Addr<OpenIDSessionsActor>>,
 | 
			
		||||
    users: web::Data<Addr<UsersActor>>,
 | 
			
		||||
    clients: web::Data<Arc<ClientManager>>,
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    user_info(req, query.0.access_token, sessions, users).await
 | 
			
		||||
    user_info(req, query.0.access_token, sessions, users, clients).await
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
pub struct UserInfoWithCustomClaims {
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    info: OpenIDUserInfo,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    additional_claims: Option<AdditionalClaims>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
 | 
			
		||||
@@ -668,6 +682,7 @@ async fn user_info(
 | 
			
		||||
    token: Option<String>,
 | 
			
		||||
    sessions: web::Data<Addr<OpenIDSessionsActor>>,
 | 
			
		||||
    users: web::Data<Addr<UsersActor>>,
 | 
			
		||||
    clients: web::Data<Arc<ClientManager>>,
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    let token = match token {
 | 
			
		||||
        Some(t) => t,
 | 
			
		||||
@@ -711,6 +726,10 @@ async fn user_info(
 | 
			
		||||
        return user_info_error("invalid_request", "Access token has expired!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let client = clients
 | 
			
		||||
        .find_by_id(&session.client)
 | 
			
		||||
        .expect("Could not extract client information!");
 | 
			
		||||
 | 
			
		||||
    let user: Option<User> = users
 | 
			
		||||
        .send(users_actor::GetUserRequest(session.user))
 | 
			
		||||
        .await
 | 
			
		||||
@@ -723,13 +742,16 @@ async fn user_info(
 | 
			
		||||
        Some(u) => u,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    HttpResponse::Ok().json(OpenIDUserInfo {
 | 
			
		||||
        name: Some(user.full_name()),
 | 
			
		||||
        sub: user.uid.0,
 | 
			
		||||
        given_name: Some(user.first_name),
 | 
			
		||||
        family_name: Some(user.last_name),
 | 
			
		||||
        preferred_username: Some(user.username),
 | 
			
		||||
        email: Some(user.email),
 | 
			
		||||
        email_verified: Some(true),
 | 
			
		||||
    HttpResponse::Ok().json(UserInfoWithCustomClaims {
 | 
			
		||||
        info: OpenIDUserInfo {
 | 
			
		||||
            name: Some(user.full_name()),
 | 
			
		||||
            sub: user.uid.0.to_string(),
 | 
			
		||||
            given_name: Some(user.first_name.to_string()),
 | 
			
		||||
            family_name: Some(user.last_name.to_string()),
 | 
			
		||||
            preferred_username: Some(user.username.to_string()),
 | 
			
		||||
            email: Some(user.email.to_string()),
 | 
			
		||||
            email_verified: Some(true),
 | 
			
		||||
        },
 | 
			
		||||
        additional_claims: client.claims_user_info(&user),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
use crate::data::entity_manager::EntityManager;
 | 
			
		||||
use crate::data::user::User;
 | 
			
		||||
use crate::utils::string_utils::apply_env_vars;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
 | 
			
		||||
pub struct ClientID(pub String);
 | 
			
		||||
@@ -10,6 +13,8 @@ pub enum AuthenticationFlow {
 | 
			
		||||
    Implicit,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type AdditionalClaims = HashMap<String, Value>;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct Client {
 | 
			
		||||
    /// The ID of the client
 | 
			
		||||
@@ -39,6 +44,12 @@ pub struct Client {
 | 
			
		||||
    /// Specify whether recent Second Factor Authentication is required to access this client
 | 
			
		||||
    #[serde(default = "bool::default")]
 | 
			
		||||
    pub enforce_2fa_auth: bool,
 | 
			
		||||
 | 
			
		||||
    /// Additional claims to return with the id token
 | 
			
		||||
    claims_id_token: Option<AdditionalClaims>,
 | 
			
		||||
 | 
			
		||||
    /// Additional claims to return through the user info endpoint
 | 
			
		||||
    claims_user_info: Option<AdditionalClaims>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialEq for Client {
 | 
			
		||||
@@ -57,6 +68,68 @@ impl Client {
 | 
			
		||||
            Some(_) => AuthenticationFlow::AuthorizationCode,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Process a single claim value
 | 
			
		||||
    fn process_claim_string(&self, user: &User, str: &str) -> String {
 | 
			
		||||
        str.replace("{username}", &user.username)
 | 
			
		||||
            .replace("{mail}", &user.email)
 | 
			
		||||
            .replace("{first_name}", &user.first_name)
 | 
			
		||||
            .replace("{last_name}", &user.last_name)
 | 
			
		||||
            .replace("{uid}", &user.uid.0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Recurse claims processing
 | 
			
		||||
    fn recurse_claims_processing(&self, user: &User, value: &Value) -> Value {
 | 
			
		||||
        match value {
 | 
			
		||||
            Value::String(s) => Value::String(self.process_claim_string(user, s)),
 | 
			
		||||
            Value::Array(arr) => Value::Array(
 | 
			
		||||
                arr.iter()
 | 
			
		||||
                    .map(|v| self.recurse_claims_processing(user, v))
 | 
			
		||||
                    .collect(),
 | 
			
		||||
            ),
 | 
			
		||||
            Value::Object(obj) => obj
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(|(k, v)| {
 | 
			
		||||
                    (
 | 
			
		||||
                        self.process_claim_string(user, k),
 | 
			
		||||
                        self.recurse_claims_processing(user, v),
 | 
			
		||||
                    )
 | 
			
		||||
                })
 | 
			
		||||
                .collect(),
 | 
			
		||||
            v => v.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Process additional claims, processing placeholders
 | 
			
		||||
    fn process_additional_claims(
 | 
			
		||||
        &self,
 | 
			
		||||
        user: &User,
 | 
			
		||||
        claims: &Option<AdditionalClaims>,
 | 
			
		||||
    ) -> Option<AdditionalClaims> {
 | 
			
		||||
        let claims = claims.as_ref()?;
 | 
			
		||||
 | 
			
		||||
        let res = claims
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|(k, v)| {
 | 
			
		||||
                (
 | 
			
		||||
                    self.process_claim_string(user, k),
 | 
			
		||||
                    self.recurse_claims_processing(user, v),
 | 
			
		||||
                )
 | 
			
		||||
            })
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        Some(res)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get additional claims for id_token for a successful authentication
 | 
			
		||||
    pub fn claims_id_token(&self, user: &User) -> Option<AdditionalClaims> {
 | 
			
		||||
        self.process_additional_claims(user, &self.claims_id_token)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get additional claims for user info for a successful authentication
 | 
			
		||||
    pub fn claims_user_info(&self, user: &User) -> Option<AdditionalClaims> {
 | 
			
		||||
        self.process_additional_claims(user, &self.claims_user_info)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type ClientManager = EntityManager<Client>;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
use crate::data::client::AdditionalClaims;
 | 
			
		||||
use jwt_simple::claims::Audiences;
 | 
			
		||||
use jwt_simple::prelude::{Duration, JWTClaims};
 | 
			
		||||
 | 
			
		||||
@@ -24,12 +25,19 @@ pub struct IdToken {
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    pub nonce: Option<String>,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    /// Additional claims
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    pub additional_claims: Option<AdditionalClaims>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct CustomIdTokenClaims {
 | 
			
		||||
    auth_time: u64,
 | 
			
		||||
    email: String,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    additional_claims: Option<AdditionalClaims>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IdToken {
 | 
			
		||||
@@ -46,6 +54,7 @@ impl IdToken {
 | 
			
		||||
            custom: CustomIdTokenClaims {
 | 
			
		||||
                auth_time: self.auth_time,
 | 
			
		||||
                email: self.email,
 | 
			
		||||
                additional_claims: self.additional_claims,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user