feat(daemon): render pam_google_authenticator.so when TOTP-required stack present
This commit is contained in:
@@ -93,27 +93,72 @@ pub(crate) fn render_profile(p: &Policy) -> String {
|
|||||||
.any(|m| matches!(m, authforge_common::types::Method::Fido2))
|
.any(|m| matches!(m, authforge_common::types::Method::Fido2))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "totp")]
|
||||||
|
let any_required_totp = p.stacks.values().any(|s| {
|
||||||
|
s.mode == Mode::Required
|
||||||
|
&& s.methods
|
||||||
|
.iter()
|
||||||
|
.any(|m| matches!(m, authforge_common::types::Method::Totp))
|
||||||
|
});
|
||||||
|
|
||||||
// Recovery line runs first on every login attempt: PAM_IGNORE when
|
// Recovery line runs first on every login attempt: PAM_IGNORE when
|
||||||
// there's no recovery file (cheap stat), PAM_SUCCESS via [success=done]
|
// there's no recovery file (cheap stat), PAM_SUCCESS via [success=done]
|
||||||
// when the user typed a valid recovery code (short-circuits the rest of
|
// when the user typed a valid recovery code (short-circuits the rest of
|
||||||
// the auth stack), PAM_IGNORE otherwise. `default=ignore` keeps a
|
// the auth stack), PAM_IGNORE otherwise. `default=ignore` keeps a
|
||||||
// missing recovery file from blocking normal auth.
|
// missing recovery file from blocking normal auth.
|
||||||
let recovery_line =
|
let mut lines: Vec<String> = Vec::new();
|
||||||
" [success=done default=ignore] pam_authforge_pending.so mode=recovery";
|
lines.push(
|
||||||
|
" [success=done default=ignore] pam_authforge_pending.so mode=recovery"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
let main_lines = if any_required_fido2 {
|
// TOTP slot (when present): runs between recovery and FIDO2. die-on-fail
|
||||||
// Per design doc § pam-auth-update profile.
|
// so a wrong code is fatal without falling through to other methods.
|
||||||
" [success=ok default=1 ignore=ignore] pam_u2f.so cue authfile=/etc/u2f_mappings\n [success=ok default=die] pam_authforge_pending.so".to_string()
|
#[cfg(feature = "totp")]
|
||||||
|
if any_required_totp {
|
||||||
|
lines.push(
|
||||||
|
" [success=ok default=die] pam_google_authenticator.so secret=/etc/google-authenticator/${USER}"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if any_required_fido2 {
|
||||||
|
lines.push(
|
||||||
|
" [success=ok default=1 ignore=ignore] pam_u2f.so cue authfile=/etc/u2f_mappings"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
" [success=ok default=die] pam_authforge_pending.so".to_string(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Policy doesn't require fido2 anywhere — only the pending-flag
|
// Policy doesn't require fido2 anywhere — only the pending-flag
|
||||||
// backstop is active. pam_u2f is left to the admin's own stack edits.
|
// backstop is active. pam_u2f is left to the admin's own stack edits.
|
||||||
" [success=ok default=die] pam_authforge_pending.so".to_string()
|
lines.push(
|
||||||
|
" [success=ok default=die] pam_authforge_pending.so".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_lines = lines.join("\n");
|
||||||
|
|
||||||
|
// `Default: yes` whenever any factor is required; auto-enable the profile
|
||||||
|
// so admins don't have to run pam-auth-update manually.
|
||||||
|
let default = if any_required_fido2 {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
#[cfg(feature = "totp")]
|
||||||
|
{
|
||||||
|
if any_required_totp {
|
||||||
|
"yes"
|
||||||
|
} else {
|
||||||
|
"no"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "totp"))]
|
||||||
|
{
|
||||||
|
"no"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth_lines = format!("{recovery_line}\n{main_lines}");
|
|
||||||
|
|
||||||
let default = if any_required_fido2 { "yes" } else { "no" };
|
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"Name: Dangerous Things authforge MFA\nDefault: {default}\nPriority: 192\nAuth-Type: Additional\nAuth:\n{auth_lines}\n"
|
"Name: Dangerous Things authforge MFA\nDefault: {default}\nPriority: 192\nAuth-Type: Additional\nAuth:\n{auth_lines}\n"
|
||||||
)
|
)
|
||||||
@@ -195,6 +240,51 @@ mod tests {
|
|||||||
assert!(body.starts_with("Name: Dangerous Things authforge MFA"));
|
assert!(body.starts_with("Name: Dangerous Things authforge MFA"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "totp")]
|
||||||
|
#[test]
|
||||||
|
fn render_required_totp_includes_pam_google_authenticator() {
|
||||||
|
let mut stacks = BTreeMap::new();
|
||||||
|
stacks.insert(
|
||||||
|
"sudo".to_string(),
|
||||||
|
StackPolicy {
|
||||||
|
mode: Mode::Required,
|
||||||
|
methods: vec![Method::Totp],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let p = Policy {
|
||||||
|
stacks,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let body = render_profile(&p);
|
||||||
|
assert!(body.contains("pam_google_authenticator.so"));
|
||||||
|
assert!(body.contains("secret=/etc/google-authenticator/${USER}"));
|
||||||
|
assert!(body.contains("Default: yes"));
|
||||||
|
// TOTP line lives after recovery, before fido2 backstop.
|
||||||
|
let recovery_idx = body.find("mode=recovery").unwrap();
|
||||||
|
let totp_idx = body.find("pam_google_authenticator.so").unwrap();
|
||||||
|
assert!(recovery_idx < totp_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "totp")]
|
||||||
|
#[test]
|
||||||
|
fn render_no_totp_when_only_fido2_required() {
|
||||||
|
let mut stacks = BTreeMap::new();
|
||||||
|
stacks.insert(
|
||||||
|
"sudo".to_string(),
|
||||||
|
StackPolicy {
|
||||||
|
mode: Mode::Required,
|
||||||
|
methods: vec![Method::Fido2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let p = Policy {
|
||||||
|
stacks,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let body = render_profile(&p);
|
||||||
|
assert!(!body.contains("pam_google_authenticator.so"));
|
||||||
|
assert!(body.contains("pam_u2f.so"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_rolls_back_on_failure() {
|
fn apply_rolls_back_on_failure() {
|
||||||
let d = tempdir().unwrap();
|
let d = tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user