mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-06-10 02:57:40 +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 $create_ticket = sub {
|
||||||
my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
|
my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
|
||||||
|
|
||||||
my ($ticketuser, $u2fdata);
|
my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
|
||||||
if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
|
if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
|
||||||
($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
|
# valid ticket. Note: root@pam can create tickets for other users
|
||||||
} else {
|
} 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 %extra;
|
||||||
my $ticket_data = $username;
|
my $ticket_data = $username;
|
||||||
if (defined($u2fdata)) {
|
if (defined($tfa_info)) {
|
||||||
my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
|
$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()
|
my $challenge = $u2f->auth_challenge()
|
||||||
or die "failed to get u2f challenge\n";
|
or die "failed to get u2f challenge\n";
|
||||||
$challenge = decode_json($challenge);
|
$challenge = decode_json($challenge);
|
||||||
$extra{U2FChallenge} = $challenge;
|
$extra{U2FChallenge} = $challenge;
|
||||||
$ticket_data = "u2f!$username!$challenge->{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);
|
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
|
||||||
@ -302,7 +311,7 @@ __PACKAGE__->register_method ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
$res->{cap} = &$compute_api_permission($rpcenv, $username)
|
$res->{cap} = &$compute_api_permission($rpcenv, $username)
|
||||||
if !defined($res->{U2FChallenge});
|
if !defined($res->{NeedTFA});
|
||||||
|
|
||||||
if (PVE::Corosync::check_conf_exists(1)) {
|
if (PVE::Corosync::check_conf_exists(1)) {
|
||||||
if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
|
if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
|
||||||
@ -616,27 +625,35 @@ __PACKAGE__->register_method({
|
|||||||
my ($param) = @_;
|
my ($param) = @_;
|
||||||
|
|
||||||
my $rpcenv = PVE::RPCEnvironment::get();
|
my $rpcenv = PVE::RPCEnvironment::get();
|
||||||
my $challenge = $rpcenv->get_u2f_challenge()
|
|
||||||
or raise('no active challenge');
|
|
||||||
my $authuser = $rpcenv->get_user();
|
my $authuser = $rpcenv->get_user();
|
||||||
my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
|
my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
|
||||||
|
|
||||||
my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
|
my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
|
||||||
if (!defined($tfa_type) || $tfa_type ne 'u2f') {
|
if (!defined($tfa_type)) {
|
||||||
raise('no u2f data available');
|
raise('no u2f data available');
|
||||||
}
|
}
|
||||||
|
|
||||||
my $keyHandle = $u2fdata->{keyHandle};
|
eval {
|
||||||
my $publicKey = $u2fdata->{publicKey};
|
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")
|
raise("incomplete u2f setup")
|
||||||
if !defined($keyHandle) || !defined($publicKey);
|
if !defined($keyHandle) || !defined($publicKey);
|
||||||
|
|
||||||
my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
|
my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
|
||||||
$u2f->set_challenge($challenge);
|
$u2f->set_challenge($challenge);
|
||||||
|
|
||||||
eval {
|
|
||||||
my ($counter, $present) = $u2f->auth_verify($param->{response});
|
my ($counter, $present) = $u2f->auth_verify($param->{response});
|
||||||
# Do we want to do anything with these?
|
# 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 = $@) {
|
if (my $err = $@) {
|
||||||
my $clientip = $rpcenv->get_client_ip() || '';
|
my $clientip = $rpcenv->get_client_ip() || '';
|
||||||
@ -644,10 +661,8 @@ __PACKAGE__->register_method({
|
|||||||
die PVE::Exception->new("authentication failure\n", code => 401);
|
die PVE::Exception->new("authentication failure\n", code => 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
# create a new ticket for the user:
|
|
||||||
my $ticket_data = "u2f!$authuser!verified";
|
|
||||||
return {
|
return {
|
||||||
ticket => PVE::AccessControl::assemble_ticket($ticket_data),
|
ticket => PVE::AccessControl::assemble_ticket($authuser),
|
||||||
cap => &$compute_api_permission($rpcenv, $authuser),
|
cap => &$compute_api_permission($rpcenv, $authuser),
|
||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
|
@ -308,10 +308,10 @@ sub verify_ticket {
|
|||||||
return $auth_failure->();
|
return $auth_failure->();
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($username, $challenge);
|
my ($username, $tfa_info);
|
||||||
if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
|
if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
|
||||||
# Ticket for u2f-users:
|
# Ticket for u2f-users:
|
||||||
($username, $challenge) = ($1, $2);
|
($username, my $challenge) = ($1, $2);
|
||||||
if ($challenge eq 'verified') {
|
if ($challenge eq 'verified') {
|
||||||
# u2f challenge was completed
|
# u2f challenge was completed
|
||||||
$challenge = undef;
|
$challenge = undef;
|
||||||
@ -320,6 +320,15 @@ sub verify_ticket {
|
|||||||
# so we treat this ticket as invalid:
|
# so we treat this ticket as invalid:
|
||||||
return $auth_failure->();
|
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 {
|
} else {
|
||||||
# Regular ticket (full access)
|
# Regular ticket (full access)
|
||||||
$username = $data;
|
$username = $data;
|
||||||
@ -327,7 +336,7 @@ sub verify_ticket {
|
|||||||
|
|
||||||
return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
|
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
|
# VNC tickets
|
||||||
@ -515,22 +524,32 @@ sub authenticate_user {
|
|||||||
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
|
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
|
||||||
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
|
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
|
||||||
|
|
||||||
my $u2f;
|
|
||||||
|
|
||||||
my ($type, $tfa_data) = user_get_tfa($username, $realm);
|
my ($type, $tfa_data) = user_get_tfa($username, $realm);
|
||||||
if ($type) {
|
if ($type) {
|
||||||
if ($type eq 'u2f') {
|
if ($type eq 'u2f') {
|
||||||
# Note that if the user did not manage to complete the initial u2f registration
|
# 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:
|
# 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 {
|
} else {
|
||||||
my $keys = $tfa_data->{keys};
|
my $keys = $tfa_data->{keys};
|
||||||
my $tfa_cfg = $tfa_data->{config};
|
my $tfa_cfg = $tfa_data->{config};
|
||||||
verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
|
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 {
|
sub domain_set_password {
|
||||||
|
Loading…
Reference in New Issue
Block a user