From 86d716f4a7d976e4d8432c2944b1b8f9d5b3b210 Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Sun, 15 Feb 2026 12:47:31 +0100 Subject: [PATCH 1/6] SSO config to skip 2FA on login Signed-off-by: Kowalski Dragon (kowalski7cc) --- src/api/identity.rs | 6 +++++- src/config.rs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 6808ddde..525bca60 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -353,7 +353,11 @@ async fn sso_login( Some((mut user, sso_user)) => { let mut device = get_device(&data, conn, &user).await?; - let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; + let twofactor_token = if CONFIG.sso_skip_2fa() { + None + } else { + twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await? + }; if user.private_key.is_none() { // User was invited a stub was created diff --git a/src/config.rs b/src/config.rs index 37fc3e85..a6e782fa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -845,6 +845,8 @@ make_config! { 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 sso_client_cache_expiration: u64, true, def, 0; + /// Skip 2FA for SSO login |> Disable two-factor authentication requirement for SSO login + sso_skip_2fa: bool, true, def, false; /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required sso_debug_tokens: bool, true, def, false; }, From f876829c73a57f9eb6033b5959134440f318a3f4 Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Fri, 11 Sep 2026 18:42:46 +0200 Subject: [PATCH 2/6] Add SSO ACR support Signed-off-by: Kowalski Dragon (kowalski7cc) --- .env.template | 3 +++ src/api/identity.rs | 9 +++++++-- src/config.rs | 8 ++++++-- src/db/models/sso_auth.rs | 1 + src/sso.rs | 3 +++ src/sso_client.rs | 16 ++++++++++++++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.env.template b/.env.template index d22145b8..449b73c3 100644 --- a/.env.template +++ b/.env.template @@ -552,6 +552,9 @@ ## 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}' +## 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 # SSO_AUTH_ONLY_NOT_SESSION=false diff --git a/src/api/identity.rs b/src/api/identity.rs index 525bca60..52bfc6a4 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -37,7 +37,7 @@ use crate::{ }, error::MapResult, mail, sso, - sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, + sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState, SSO_2FA_ACR}, util, }; @@ -353,7 +353,12 @@ async fn sso_login( Some((mut user, sso_user)) => { 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 } else { twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await? diff --git a/src/config.rs b/src/config.rs index a6e782fa..55dc0184 100644 --- a/src/config.rs +++ b/src/config.rs @@ -845,8 +845,8 @@ make_config! { 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 sso_client_cache_expiration: u64, true, def, 0; - /// Skip 2FA for SSO login |> Disable two-factor authentication requirement for SSO login - sso_skip_2fa: bool, true, def, false; + /// 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: String, true, def, "false".to_string(); /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required 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())?; } + 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.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") diff --git a/src/db/models/sso_auth.rs b/src/db/models/sso_auth.rs index 311e9bf9..0bb9232f 100644 --- a/src/db/models/sso_auth.rs +++ b/src/db/models/sso_auth.rs @@ -35,6 +35,7 @@ pub struct OIDCAuthenticatedUser { pub email: String, pub email_verified: Option, pub user_name: Option, + pub acr: Option, } impl_FromToSqlText!(OIDCAuthenticatedUser); diff --git a/src/sso.rs b/src/sso.rs index 01fbd906..33f1b005 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -17,6 +17,8 @@ use crate::{ sso_client::Client, }; +pub const SSO_2FA_ACR: &str = "2"; + pub static FAKE_SSO_IDENTIFIER: &str = "00000000-01DC-01DC-01DC-000000000000"; static SSO_JWT_ISSUER: LazyLock = LazyLock::new(|| format!("{}|sso", CONFIG.domain_origin())); @@ -305,6 +307,7 @@ pub async fn exchange_code( email: email.clone(), email_verified, user_name: user_name.clone(), + acr: id_claims.auth_context_ref().map(|acr| acr.as_str().to_string()), }; debug!("Authenticated user {authenticated_user:?}"); diff --git a/src/sso_client.rs b/src/sso_client.rs index bc766586..336b2281 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -202,6 +202,22 @@ impl Client { .add_scopes(scopes) .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() { auth_req = auth_req .add_extra_param::<&str, String>("code_challenge", client_challenge.clone().into()) From 1776d8c30cf7599c8c83211ec6e4e22b81ac31fc Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Fri, 11 Sep 2026 20:37:24 +0200 Subject: [PATCH 3/6] Add support for SSO AMR claims Signed-off-by: Kowalski Dragon (kowalski7cc) --- .env.template | 2 +- src/api/identity.rs | 9 ++++++++- src/db/models/sso_auth.rs | 1 + src/sso.rs | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 449b73c3..baa12767 100644 --- a/.env.template +++ b/.env.template @@ -552,7 +552,7 @@ ## 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}' -## Skip Vaultwarden 2FA for SSO: false (never), true (always), auto (only when the IdP returns ACR level 2) +## Skip Vaultwarden 2FA for SSO: false (never), true (always), auto (only when the IdP returns an MFA ACR/AMR claim) # SSO_SKIP_2FA=false ## Use sso only for authentication not the session lifecycle diff --git a/src/api/identity.rs b/src/api/identity.rs index 52bfc6a4..81e37017 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -355,7 +355,14 @@ async fn sso_login( let skip_2fa = match CONFIG.sso_skip_2fa().as_str() { "true" => true, - "auto" => user_infos.acr.as_deref() == Some(SSO_2FA_ACR), + "auto" => { + user_infos.acr.as_deref() == Some(SSO_2FA_ACR) + || user_infos.amr.as_ref().is_some_and(|amr| { + amr.iter().any(|method| { + matches!(method.as_str(), "mfa" | "otp" | "fido2" | "webauthn" | "hwk") + }) + }) + } _ => false, }; let twofactor_token = if skip_2fa { diff --git a/src/db/models/sso_auth.rs b/src/db/models/sso_auth.rs index 0bb9232f..958a85f1 100644 --- a/src/db/models/sso_auth.rs +++ b/src/db/models/sso_auth.rs @@ -36,6 +36,7 @@ pub struct OIDCAuthenticatedUser { pub email_verified: Option, pub user_name: Option, pub acr: Option, + pub amr: Option>, } impl_FromToSqlText!(OIDCAuthenticatedUser); diff --git a/src/sso.rs b/src/sso.rs index 33f1b005..6dcf3303 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -308,6 +308,9 @@ pub async fn exchange_code( email_verified, user_name: user_name.clone(), acr: id_claims.auth_context_ref().map(|acr| acr.as_str().to_string()), + amr: id_claims + .auth_method_refs() + .map(|amr| amr.iter().map(|method| method.as_str().to_string()).collect()), }; debug!("Authenticated user {authenticated_user:?}"); From 09d50dc5a36ee0294bf610a0b447be2f725bfc33 Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Fri, 11 Sep 2026 21:50:34 +0200 Subject: [PATCH 4/6] Make SSO AMR customizable Signed-off-by: Kowalski Dragon (kowalski7cc) --- .env.template | 7 +++++-- src/api/identity.rs | 13 +++++-------- src/config.rs | 14 ++++++++++---- src/sso_client.rs | 23 +++++++++++------------ 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.env.template b/.env.template index baa12767..6126cc2f 100644 --- a/.env.template +++ b/.env.template @@ -552,8 +552,11 @@ ## 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}' -## Skip Vaultwarden 2FA for SSO: false (never), true (always), auto (only when the IdP returns an MFA ACR/AMR claim) -# SSO_SKIP_2FA=false +## Skip Vaultwarden 2FA for SSO/social login: false (never), true (always), auto (only when the IdP returns an MFA ACR/AMR claim) +# SSO_2FA_SKIP=false + +## In auto mode, comma-separated OIDC AMR values that indicate MFA. Set blank to disable AMR matching. +# SSO_2FA_AMR="mfa,otp,fido2,webauthn,hwk" ## Use sso only for authentication not the session lifecycle # SSO_AUTH_ONLY_NOT_SESSION=false diff --git a/src/api/identity.rs b/src/api/identity.rs index 81e37017..db2fbfe6 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -37,7 +37,7 @@ use crate::{ }, error::MapResult, mail, sso, - sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState, SSO_2FA_ACR}, + sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState}, util, }; @@ -353,15 +353,12 @@ async fn sso_login( Some((mut user, sso_user)) => { let mut device = get_device(&data, conn, &user).await?; - let skip_2fa = match CONFIG.sso_skip_2fa().as_str() { + let skip_2fa = match CONFIG.sso_2fa_skip().as_str() { "true" => true, "auto" => { - user_infos.acr.as_deref() == Some(SSO_2FA_ACR) - || user_infos.amr.as_ref().is_some_and(|amr| { - amr.iter().any(|method| { - matches!(method.as_str(), "mfa" | "otp" | "fido2" | "webauthn" | "hwk") - }) - }) + let amr_values = CONFIG.sso_2fa_amr_vec(); + user_infos.acr.as_deref() == Some(crate::sso::SSO_2FA_ACR) + || user_infos.amr.as_ref().is_some_and(|amr| amr.iter().any(|method| amr_values.contains(method))) } _ => false, }; diff --git a/src/config.rs b/src/config.rs index 55dc0184..f8369083 100644 --- a/src/config.rs +++ b/src/config.rs @@ -845,8 +845,10 @@ make_config! { 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 sso_client_cache_expiration: u64, true, def, 0; - /// 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: String, true, def, "false".to_string(); + /// Skip 2FA for SSO/social login |> `false` keeps Vaultwarden 2FA, `true` always skips it, `auto` skips it only when the IdP returns an MFA AMR or ACR claim + sso_2fa_skip: String, true, def, "false".to_string(); + /// SSO 2FA AMR values |> Comma-separated AMR values that satisfy `SSO_2FA_SKIP=auto` + sso_2fa_amr: String, true, def, "mfa,otp,fido2,webauthn,hwk".to_string(); /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required sso_debug_tokens: bool, true, def, false; }, @@ -1118,8 +1120,8 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { 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 !matches!(cfg.sso_2fa_skip.as_str(), "false" | "true" | "auto") { + err!("`SSO_2FA_SKIP` must be one of: false, true, auto"); } if cfg._enable_yubico { @@ -1710,6 +1712,10 @@ impl Config { pub fn sso_authorize_extra_params_vec(&self) -> Vec<(String, String)> { url::form_urlencoded::parse(self.sso_authorize_extra_params().as_bytes()).into_owned().collect() } + + pub fn sso_2fa_amr_vec(&self) -> Vec { + self.sso_2fa_amr().split(',').map(str::trim).filter(|v| !v.is_empty()).map(str::to_owned).collect() + } } use handlebars::{ diff --git a/src/sso_client.rs b/src/sso_client.rs index 336b2281..9fa19664 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -202,20 +202,19 @@ impl Client { .add_scopes(scopes) .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] + if CONFIG.sso_2fa_skip() == "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(), - ); + }) + .to_string(), + ); } if CONFIG.sso_pkce() { From 0c0974498cafec6d0016b01cf3b2b189bac96664 Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Sat, 12 Sep 2026 12:20:59 +0200 Subject: [PATCH 5/6] Fix remove the unnecessary path segments Signed-off-by: Kowalski Dragon (kowalski7cc) --- src/api/identity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index db2fbfe6..112446fb 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -357,7 +357,7 @@ async fn sso_login( "true" => true, "auto" => { let amr_values = CONFIG.sso_2fa_amr_vec(); - user_infos.acr.as_deref() == Some(crate::sso::SSO_2FA_ACR) + user_infos.acr.as_deref() == Some(sso::SSO_2FA_ACR) || user_infos.amr.as_ref().is_some_and(|amr| amr.iter().any(|method| amr_values.contains(method))) } _ => false, From 2a4dda3f25d5d4ddfd66a1465122190ebcc4fa48 Mon Sep 17 00:00:00 2001 From: "Kowalski Dragon (kowalski7cc)" Date: Tue, 15 Sep 2026 23:03:29 +0200 Subject: [PATCH 6/6] Replace to_string with to_owned Signed-off-by: Kowalski Dragon (kowalski7cc) --- src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index f8369083..d691e8a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -846,9 +846,9 @@ make_config! { /// 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; /// Skip 2FA for SSO/social login |> `false` keeps Vaultwarden 2FA, `true` always skips it, `auto` skips it only when the IdP returns an MFA AMR or ACR claim - sso_2fa_skip: String, true, def, "false".to_string(); + sso_2fa_skip: String, true, def, "false".to_owned(); /// SSO 2FA AMR values |> Comma-separated AMR values that satisfy `SSO_2FA_SKIP=auto` - sso_2fa_amr: String, true, def, "mfa,otp,fido2,webauthn,hwk".to_string(); + sso_2fa_amr: String, true, def, "mfa,otp,fido2,webauthn,hwk".to_owned(); /// Log all tokens |> `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` is required sso_debug_tokens: bool, true, def, false; },