Can define additional claims on per-client basis
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Pierre HUBERT 2024-03-31 18:37:08 +02:00
parent d087c5629d
commit 91ef6c25d5
5 changed files with 162 additions and 43 deletions

58
Cargo.lock generated
View File

@ -106,7 +106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -246,7 +246,7 @@ dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -257,7 +257,7 @@ checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -460,7 +460,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -845,7 +845,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -1090,7 +1090,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -1283,7 +1283,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -1723,7 +1723,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -1815,9 +1815,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "mime"
@ -2008,7 +2008,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -2019,9 +2019,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.101"
version = "0.9.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
dependencies = [
"cc",
"libc",
@ -2099,9 +2099,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@ -2454,9 +2454,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.9.2"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@ -2467,9 +2467,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.9.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys",
"libc",
@ -2508,7 +2508,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -2673,9 +2673,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.55"
version = "2.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
dependencies = [
"proc-macro2",
"quote",
@ -2750,7 +2750,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -2801,9 +2801,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@ -2875,7 +2875,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]
@ -3060,7 +3060,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
"wasm-bindgen-shared",
]
@ -3094,7 +3094,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3373,7 +3373,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.57",
]
[[package]]

View File

@ -23,12 +23,27 @@ You can configure a list of clients (Relying Parties) in a `clients.yaml` file w
secret: TOP_SECRET
# The URL where user shall be redirected after authentication
redirect_uri: https://mygit.mywebsite.com/
# If you want new accounts to be granted access to this client by default
# Optional, If you want new accounts to be granted access to this client by default
default: true
# If you want the client to be granted to every user, regardless their account configuration
# Optional, If you want the client to be granted to every user, regardless their account configuration
granted_to_all_users: true
# If you want users to have performed recent second factor authentication before accessing this client, set this setting to true
# Optional, If you want users to have performed recent second factor authentication before accessing this client, set this setting to true
enforce_2fa_auth: true
# Optional, claims to be added to the ID token payload.
# The following placeholders can be set, they will the replaced when the token is created:
# * {username}: user name of the user
# * {mail}: email address of the user
# * {first_name}: first name of the user
# * {last_name}: last name of the user
# * {uid}: user id of the user
claims_id_token:
groups: ["group_{user}"]
service: "auth"
# Optional, claims to be added to the user info endpoint response
# The placeholders of `claims_id_token` can also be used here
claims_user_info:
groups: ["group_{user}"]
service: "auth"
```
On the first run, BasicOIDC will create a new administrator with credentials `admin` / `admin`. On first login you will have to change these default credentials.

View File

@ -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 {
HttpResponse::Ok().json(UserInfoWithCustomClaims {
info: 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),
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),
})
}

View File

@ -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>;

View File

@ -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,
},
}
}