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:
parent
d087c5629d
commit
91ef6c25d5
58
Cargo.lock
generated
58
Cargo.lock
generated
@ -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]]
|
||||
|
21
README.md
21
README.md
@ -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.
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user