Browse Source

Add SSO ACR support

Signed-off-by: Kowalski Dragon (kowalski7cc) <kowalski7cc@users.noreply.github.com>
pull/7749/head
Kowalski Dragon (kowalski7cc) 2 weeks ago
parent
commit
f876829c73
No known key found for this signature in database GPG Key ID: C4E819BD2BC6233E
  1. 3
      .env.template
  2. 9
      src/api/identity.rs
  3. 8
      src/config.rs
  4. 1
      src/db/models/sso_auth.rs
  5. 3
      src/sso.rs
  6. 16
      src/sso_client.rs

3
.env.template

@ -552,6 +552,9 @@
## Optional Master password policy (minComplexity=[0-4]), `enforceOnLogin` is not supported at the moment. ## Optional Master password policy (minComplexity=[0-4]), `enforceOnLogin` is not supported at the moment.
# SSO_MASTER_PASSWORD_POLICY='{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":false}' # SSO_MASTER_PASSWORD_POLICY='{"enforceOnLogin":false,"minComplexity":3,"minLength":12,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"requireUpper":false}'
## Skip Vaultwarden 2FA for SSO: false (never), true (always), auto (only when the IdP returns ACR level 2)
# SSO_SKIP_2FA=false
## Use sso only for authentication not the session lifecycle ## Use sso only for authentication not the session lifecycle
# SSO_AUTH_ONLY_NOT_SESSION=false # SSO_AUTH_ONLY_NOT_SESSION=false

9
src/api/identity.rs

@ -37,7 +37,7 @@ use crate::{
}, },
error::MapResult, error::MapResult,
mail, sso, mail, sso,
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState, SSO_2FA_ACR},
util, util,
}; };
@ -353,7 +353,12 @@ async fn sso_login(
Some((mut user, sso_user)) => { Some((mut user, sso_user)) => {
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = if CONFIG.sso_skip_2fa() { let skip_2fa = match CONFIG.sso_skip_2fa().as_str() {
"true" => true,
"auto" => user_infos.acr.as_deref() == Some(SSO_2FA_ACR),
_ => false,
};
let twofactor_token = if skip_2fa {
None None
} else { } else {
twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await? twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?

8
src/config.rs

@ -845,8 +845,8 @@ make_config! {
sso_auth_only_not_session: bool, true, def, false; sso_auth_only_not_session: bool, true, def, false;
/// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache /// Client cache for discovery endpoint. |> Duration in seconds (0 or less to disable). More details: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect#client-cache
sso_client_cache_expiration: u64, true, def, 0; sso_client_cache_expiration: u64, true, def, 0;
/// Skip 2FA for SSO login |> Disable two-factor authentication requirement for SSO login /// Skip 2FA for SSO login |> `false` keeps Vaultwarden 2FA, `true` always skips it, `auto` skips it only when the IdP returns the requested ACR level
sso_skip_2fa: bool, true, def, false; sso_skip_2fa: String, true, def, "false".to_string();
/// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required
sso_debug_tokens: bool, true, def, false; sso_debug_tokens: bool, true, def, false;
}, },
@ -1118,6 +1118,10 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?;
} }
if !matches!(cfg.sso_skip_2fa.as_str(), "false" | "true" | "auto") {
err!("`SSO_SKIP_2FA` must be one of: false, true, auto");
}
if cfg._enable_yubico { if cfg._enable_yubico {
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support") err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support")

1
src/db/models/sso_auth.rs

@ -35,6 +35,7 @@ pub struct OIDCAuthenticatedUser {
pub email: String, pub email: String,
pub email_verified: Option<bool>, pub email_verified: Option<bool>,
pub user_name: Option<String>, pub user_name: Option<String>,
pub acr: Option<String>,
} }
impl_FromToSqlText!(OIDCAuthenticatedUser); impl_FromToSqlText!(OIDCAuthenticatedUser);

3
src/sso.rs

@ -17,6 +17,8 @@ use crate::{
sso_client::Client, sso_client::Client,
}; };
pub const SSO_2FA_ACR: &str = "2";
pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000"; pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000";
static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin())); static SSO_JWT_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin()));
@ -305,6 +307,7 @@ pub async fn exchange_code(
email: email.clone(), email: email.clone(),
email_verified, email_verified,
user_name: user_name.clone(), user_name: user_name.clone(),
acr: id_claims.auth_context_ref().map(|acr| acr.as_str().to_string()),
}; };
debug!("Authenticated user {authenticated_user:?}"); debug!("Authenticated user {authenticated_user:?}");

16
src/sso_client.rs

@ -202,6 +202,22 @@ impl Client {
.add_scopes(scopes) .add_scopes(scopes)
.add_extra_params(CONFIG.sso_authorize_extra_params_vec()); .add_extra_params(CONFIG.sso_authorize_extra_params_vec());
if CONFIG.sso_skip_2fa() == "auto" {
auth_req = auth_req
.add_extra_param(
"claims",
serde_json::json!({
"id_token": {
"acr": {
"essential": true,
"values": [crate::sso::SSO_2FA_ACR]
}
}
})
.to_string(),
);
}
if CONFIG.sso_pkce() { if CONFIG.sso_pkce() {
auth_req = auth_req auth_req = auth_req
.add_extra_param::<&str, String>("code_challenge", client_challenge.clone().into()) .add_extra_param::<&str, String>("code_challenge", client_challenge.clone().into())

Loading…
Cancel
Save