auth: Implement password reset after expiration (closes #992)

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-08-26 09:36:02 +02:00
parent 35fa91058b
commit c90aec373e
28 changed files with 69 additions and 42 deletions

View File

@@ -29,7 +29,7 @@ pub const AuthOptions = struct {
const PamAppdata = struct {
username: []const u8,
password: []const u8,
authreq_requested: bool,
new_authtok_requested: bool,
authreq_responded: bool,
new_password: []const u8,
};
@@ -52,6 +52,7 @@ pub fn authenticate(
current_environment: Environment,
login: []const u8,
password: []const u8,
maybe_new_password: ?[]const u8,
) !void {
var tty_buffer: [3]u8 = undefined;
const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty});
@@ -69,7 +70,7 @@ pub fn authenticate(
var credentials: PamAppdata = .{
.username = login,
.password = password,
.authreq_requested = false,
.new_authtok_requested = false,
.authreq_responded = false,
.new_password = "",
};
@@ -98,9 +99,11 @@ pub fn authenticate(
try log_file.info(io, "auth/pam", "validating account", .{});
status = interop.pam.pam_acct_mgmt(handle, 0);
if (status == interop.pam.PAM_NEW_AUTHTOK_REQD) {
// credentials.authreq_requested = true;
// credentials.new_password = "";
// status = interop.pam.pam_chauthtok(handle, interop.pam.PAM_CHANGE_EXPIRED_AUTHTOK);
if (maybe_new_password) |new_passwsord| {
credentials.new_authtok_requested = true;
credentials.new_password = new_passwsord;
status = interop.pam.pam_chauthtok(handle, interop.pam.PAM_CHANGE_EXPIRED_AUTHTOK);
}
}
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
@@ -334,7 +337,7 @@ fn loginConv(
},
interop.pam.PAM_PROMPT_ECHO_OFF => {
var pass = data.password;
if (data.authreq_requested) {
if (data.new_authtok_requested) {
if (data.authreq_responded) {
pass = data.new_password;
}

View File

@@ -1,5 +1,5 @@
//
// NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py`
// NOTE: After editing this file, please run `res/lang/normalize_lang_files.py`
// to update all the language files accordingly.
//
@@ -38,7 +38,6 @@ err_pam_abort: []const u8 = "pam transaction aborted",
err_pam_acct_expired: []const u8 = "account expired",
err_pam_auth: []const u8 = "authentication error",
err_pam_authinfo_unavail: []const u8 = "failed to get user info",
err_pam_authok_reqd: []const u8 = "token expired",
err_pam_buf: []const u8 = "memory buffer error",
err_pam_cred_err: []const u8 = "failed to set credentials",
err_pam_cred_expired: []const u8 = "credentials expired",
@@ -82,6 +81,7 @@ shell: [:0]const u8 = "shell",
shutdown: []const u8 = "shutdown",
sleep: []const u8 = "sleep",
toggle_password: []const u8 = "toggle password",
token_expired: []const u8 = "password expired, please reset",
wayland: []const u8 = "wayland",
x11: []const u8 = "x11",
xinitrc: [:0]const u8 = "xinitrc",

View File

@@ -113,6 +113,7 @@ const UiState = struct {
login_text: ?*Text,
password: *Text,
password_widget: *Widget,
maybe_old_password: ?[]const u8,
insert_mode: bool,
edge_margin: Position,
config: Config,
@@ -889,6 +890,9 @@ pub fn main(init: std.process.Init) !void {
);
defer state.password_label.deinit();
state.maybe_old_password = null;
defer if (state.maybe_old_password) |pass| state.allocator.free(pass);
state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert;
state.password = try Text.init(
@@ -1633,7 +1637,8 @@ fn authenticate(ptr: *anyopaque) !bool {
auth_options,
current_environment,
if (state.login_text) |box| box.text.items else state.login.?.getCurrentUsername(),
password_text,
if (state.maybe_old_password) |pass| pass else password_text,
if (state.maybe_old_password != null) password_text else null,
) catch |err| {
shared_err.writeError(err);
@@ -1645,12 +1650,19 @@ fn authenticate(ptr: *anyopaque) !bool {
std.process.exit(0);
}
var session_status: c_int = undefined;
_ = std.posix.system.waitpid(session_pid, &session_status, 0);
// HACK: It seems like the session process is not exiting immediately after the waitpid call.
// This is a workaround to ensure the session process has exited before re-initializing the TTY.
state.io.sleep(.fromSeconds(1), .real) catch {};
session_pid = -1;
if (state.maybe_old_password) |pass| {
state.allocator.free(pass);
state.maybe_old_password = null;
}
if (session_pid != -1) {
var session_status: c_int = undefined;
_ = std.posix.system.waitpid(session_pid, &session_status, 0);
// HACK: It seems like the session process is not exiting immediately after the waitpid call.
// This is a workaround to ensure the session process has exited before re-initializing the TTY.
state.io.sleep(.fromSeconds(1), .real) catch {};
session_pid = -1;
}
try state.log_file.reinit(state.io);
}
@@ -1658,7 +1670,20 @@ fn authenticate(ptr: *anyopaque) !bool {
try state.buffer.reclaim();
const auth_err = shared_err.readError();
if (auth_err) |err| {
if (auth_err) |err| handle_error: {
if (err == error.PamNewAuthTokenRequired) {
try state.info_line.addMessage(
state.lang.token_expired,
state.config.bg,
state.config.fg,
);
state.maybe_old_password = try state.allocator.dupe(u8, state.password.text.items);
state.password.clear();
state.is_autologin = false;
break :handle_error;
}
state.auth_fails += 1;
state.buffer.setActiveWidget(state.password_widget);
@@ -2611,7 +2636,6 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 {
error.PamCredentialsInsufficient => lang.err_pam_cred_insufficient,
error.PamCredentialsUnavailable => lang.err_pam_cred_unavail,
error.PamMaximumTries => lang.err_pam_maxtries,
error.PamNewAuthTokenRequired => lang.err_pam_authok_reqd,
error.PamPermissionDenied => lang.err_pam_perm_denied,
error.PamSessionError => lang.err_pam_session,
error.PamSystemError => lang.err_pam_sys,