mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-06-09 19:58:53 +00:00
verify_ticket: allow general non-challenge tfa to be run as two step call
This allows for doing OTP TFA in two steps, first login with normal credentials and get the half-logged-in ticket, then send the OTP verification for full login, same as with u2f was already possible. This allows for a nicer UI, as OTP fields can be shown on demand, and do not need to be visible by default. The old way of sending the OTP code immediately with the initial credentials request still works for backward compatibility and as some API user may prefer it. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com> Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
parent
a270d4e167
commit
f25628d3ef
@ -125,23 +125,32 @@ my $verify_auth = sub {
|
||||
my $create_ticket = sub {
|
||||
my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
|
||||
|
||||
my ($ticketuser, $u2fdata);
|
||||
if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
|
||||
($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
|
||||
my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
|
||||
if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
|
||||
if (defined($tfa_info)) {
|
||||
die "incomplete ticket\n";
|
||||
}
|
||||
# valid ticket. Note: root@pam can create tickets for other users
|
||||
} else {
|
||||
($username, $u2fdata) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
|
||||
($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
|
||||
}
|
||||
|
||||
my %extra;
|
||||
my $ticket_data = $username;
|
||||
if (defined($u2fdata)) {
|
||||
my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
|
||||
my $challenge = $u2f->auth_challenge()
|
||||
or die "failed to get u2f challenge\n";
|
||||
$challenge = decode_json($challenge);
|
||||
$extra{U2FChallenge} = $challenge;
|
||||
$ticket_data = "u2f!$username!$challenge->{challenge}";
|
||||
if (defined($tfa_info)) {
|
||||
$extra{NeedTFA} = 1;
|
||||
if ($tfa_info->{type} eq 'u2f') {
|
||||
my $u2finfo = $tfa_info->{data};
|
||||
my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
|
||||
my $challenge = $u2f->auth_challenge()
|
||||
or die "failed to get u2f challenge\n";
|
||||
$challenge = decode_json($challenge);
|
||||
$extra{U2FChallenge} = $challenge;
|
||||
$ticket_data = "u2f!$username!$challenge->{challenge}";
|
||||
} else {
|
||||
# General half-login / 'missing 2nd factor' ticket:
|
||||
$ticket_data = "tfa!$username";
|
||||
}
|
||||
}
|
||||
|
||||
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
|
||||
@ -302,7 +311,7 @@ __PACKAGE__->register_method ({
|
||||
}
|
||||
|
||||
$res->{cap} = &$compute_api_permission($rpcenv, $username)
|
||||
if !defined($res->{U2FChallenge});
|
||||
if !defined($res->{NeedTFA});
|
||||
|
||||
if (PVE::Corosync::check_conf_exists(1)) {
|
||||
if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
|
||||
@ -616,27 +625,35 @@ __PACKAGE__->register_method({
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $challenge = $rpcenv->get_u2f_challenge()
|
||||
or raise('no active challenge');
|
||||
my $authuser = $rpcenv->get_user();
|
||||
my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
|
||||
|
||||
my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
|
||||
if (!defined($tfa_type) || $tfa_type ne 'u2f') {
|
||||
my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
|
||||
if (!defined($tfa_type)) {
|
||||
raise('no u2f data available');
|
||||
}
|
||||
|
||||
my $keyHandle = $u2fdata->{keyHandle};
|
||||
my $publicKey = $u2fdata->{publicKey};
|
||||
raise("incomplete u2f setup")
|
||||
if !defined($keyHandle) || !defined($publicKey);
|
||||
|
||||
my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
|
||||
$u2f->set_challenge($challenge);
|
||||
|
||||
eval {
|
||||
my ($counter, $present) = $u2f->auth_verify($param->{response});
|
||||
# Do we want to do anything with these?
|
||||
if ($tfa_type eq 'u2f') {
|
||||
my $challenge = $rpcenv->get_u2f_challenge()
|
||||
or raise('no active challenge');
|
||||
|
||||
my $keyHandle = $tfa_data->{keyHandle};
|
||||
my $publicKey = $tfa_data->{publicKey};
|
||||
raise("incomplete u2f setup")
|
||||
if !defined($keyHandle) || !defined($publicKey);
|
||||
|
||||
my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
|
||||
$u2f->set_challenge($challenge);
|
||||
|
||||
my ($counter, $present) = $u2f->auth_verify($param->{response});
|
||||
# Do we want to do anything with these?
|
||||
} else {
|
||||
# sanity check before handing off to the verification code:
|
||||
my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
|
||||
my $config = $tfa_data->{config} or die "bad tfa entry\n";
|
||||
PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
|
||||
}
|
||||
};
|
||||
if (my $err = $@) {
|
||||
my $clientip = $rpcenv->get_client_ip() || '';
|
||||
@ -644,10 +661,8 @@ __PACKAGE__->register_method({
|
||||
die PVE::Exception->new("authentication failure\n", code => 401);
|
||||
}
|
||||
|
||||
# create a new ticket for the user:
|
||||
my $ticket_data = "u2f!$authuser!verified";
|
||||
return {
|
||||
ticket => PVE::AccessControl::assemble_ticket($ticket_data),
|
||||
ticket => PVE::AccessControl::assemble_ticket($authuser),
|
||||
cap => &$compute_api_permission($rpcenv, $authuser),
|
||||
}
|
||||
}});
|
||||
|
@ -308,10 +308,10 @@ sub verify_ticket {
|
||||
return $auth_failure->();
|
||||
}
|
||||
|
||||
my ($username, $challenge);
|
||||
my ($username, $tfa_info);
|
||||
if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
|
||||
# Ticket for u2f-users:
|
||||
($username, $challenge) = ($1, $2);
|
||||
($username, my $challenge) = ($1, $2);
|
||||
if ($challenge eq 'verified') {
|
||||
# u2f challenge was completed
|
||||
$challenge = undef;
|
||||
@ -320,6 +320,15 @@ sub verify_ticket {
|
||||
# so we treat this ticket as invalid:
|
||||
return $auth_failure->();
|
||||
}
|
||||
$tfa_info = {
|
||||
type => 'u2f',
|
||||
challenge => $challenge,
|
||||
};
|
||||
} elsif ($data =~ /^tfa!(.*)$/) {
|
||||
# TOTP and Yubico don't require a challenge so this is the generic
|
||||
# 'missing 2nd factor ticket'
|
||||
$username = $1;
|
||||
$tfa_info = { type => 'tfa' };
|
||||
} else {
|
||||
# Regular ticket (full access)
|
||||
$username = $data;
|
||||
@ -327,7 +336,7 @@ sub verify_ticket {
|
||||
|
||||
return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
|
||||
|
||||
return wantarray ? ($username, $age, $challenge) : $username;
|
||||
return wantarray ? ($username, $age, $tfa_info) : $username;
|
||||
}
|
||||
|
||||
# VNC tickets
|
||||
@ -515,22 +524,32 @@ sub authenticate_user {
|
||||
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
|
||||
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
|
||||
|
||||
my $u2f;
|
||||
|
||||
my ($type, $tfa_data) = user_get_tfa($username, $realm);
|
||||
if ($type) {
|
||||
if ($type eq 'u2f') {
|
||||
# Note that if the user did not manage to complete the initial u2f registration
|
||||
# challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
|
||||
$u2f = $tfa_data if !exists $tfa_data->{challenge};
|
||||
$tfa_data = undef if exists $tfa_data->{challenge};
|
||||
} elsif (!defined($otp)) {
|
||||
# The user requires a 2nd factor but has not provided one. Return success but
|
||||
# don't clear $tfa_data.
|
||||
} else {
|
||||
my $keys = $tfa_data->{keys};
|
||||
my $tfa_cfg = $tfa_data->{config};
|
||||
verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
|
||||
$tfa_data = undef;
|
||||
}
|
||||
|
||||
# Return the type along with the rest:
|
||||
if ($tfa_data) {
|
||||
$tfa_data = {
|
||||
type => $type,
|
||||
data => $tfa_data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return wantarray ? ($username, $u2f) : $username;
|
||||
return wantarray ? ($username, $tfa_data) : $username;
|
||||
}
|
||||
|
||||
sub domain_set_password {
|
||||
|
Loading…
Reference in New Issue
Block a user