From: Dietmar Maurer Date: Mon, 23 Jun 2014 09:42:44 +0000 (+0200) Subject: add basic support for two factor auth X-Git-Url: https://git.proxmox.com/?p=pve-access-control.git;a=commitdiff_plain;h=96f8ebd62506bc7126d58400004101ef6a13ca71 add basic support for two factor auth --- diff --git a/PVE/API2/AccessControl.pm b/PVE/API2/AccessControl.pm index 5e13292..f01180d 100644 --- a/PVE/API2/AccessControl.pm +++ b/PVE/API2/AccessControl.pm @@ -88,7 +88,7 @@ __PACKAGE__->register_method ({ my $verify_auth = sub { - my ($rpcenv, $username, $pw_or_ticket, $path, $privs) = @_; + my ($rpcenv, $username, $pw_or_ticket, $otp, $path, $privs) = @_; my $normpath = PVE::AccessControl::normalize_path($path); @@ -99,7 +99,7 @@ my $verify_auth = sub { } elsif (PVE::AccessControl::verify_vnc_ticket($pw_or_ticket, $username, $normpath, 1)) { # valid vnc ticket } else { - $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket); + $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp); } my $privlist = [ PVE::Tools::split_list($privs) ]; @@ -111,14 +111,14 @@ my $verify_auth = sub { }; my $create_ticket = sub { - my ($rpcenv, $username, $pw_or_ticket) = @_; + my ($rpcenv, $username, $pw_or_ticket, $otp) = @_; my $ticketuser; if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) { # valid ticket. Note: root@pam can create tickets for other users } else { - $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket); + $username = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp); } my $ticket = PVE::AccessControl::assemble_ticket($username); @@ -243,6 +243,11 @@ __PACKAGE__->register_method ({ description => "The secret password. This can also be a valid ticket.", type => 'string', }, + otp => { + description => "One-time password for Two-factor authentication.", + type => 'string', + optional => 1, + }, path => { description => "Verify ticket, and check if user have access 'privs' on 'path'", type => 'string', @@ -281,10 +286,10 @@ __PACKAGE__->register_method ({ $rpcenv->check_user_enabled($username); if ($param->{path} && $param->{privs}) { - $res = &$verify_auth($rpcenv, $username, $param->{password}, + $res = &$verify_auth($rpcenv, $username, $param->{password}, $param->{otp}, $param->{path}, $param->{privs}); } else { - $res = &$create_ticket($rpcenv, $username, $param->{password}); + $res = &$create_ticket($rpcenv, $username, $param->{password}, $param->{otp}); } }; if (my $err = $@) { diff --git a/PVE/API2/Domains.pm b/PVE/API2/Domains.pm index 10515c0..dac5660 100644 --- a/PVE/API2/Domains.pm +++ b/PVE/API2/Domains.pm @@ -34,6 +34,13 @@ __PACKAGE__->register_method ({ type => "object", properties => { realm => { type => 'string' }, + tfa => { + description => "Two-factor authentication provider.", + type => 'string', + enum => [ 'yubico' ], + optional => 1, + }, + comment => { type => 'string', optional => 1 }, comment => { type => 'string', optional => 1 }, }, }, @@ -52,6 +59,9 @@ __PACKAGE__->register_method ({ my $entry = { realm => $realm, type => $d->{type} }; $entry->{comment} = $d->{comment} if $d->{comment}; $entry->{default} = 1 if $d->{default}; + if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) { + $entry->{tfa} = $tfa_cfg->{type}; + } push @$res, $entry; } diff --git a/PVE/API2/User.pm b/PVE/API2/User.pm index 139e3b6..6208ad5 100644 --- a/PVE/API2/User.pm +++ b/PVE/API2/User.pm @@ -21,7 +21,7 @@ my $extract_user_data = sub { my $res = {}; - foreach my $prop (qw(enable expire firstname lastname email comment)) { + foreach my $prop (qw(enable expire firstname lastname email comment keys)) { $res->{$prop} = $data->{$prop} if defined($data->{$prop}); } @@ -124,6 +124,11 @@ __PACKAGE__->register_method ({ lastname => { type => 'string', optional => 1 }, email => { type => 'string', optional => 1, format => 'email-opt' }, comment => { type => 'string', optional => 1 }, + keys => { + description => "Keys for two factor auth (yubico).", + type => 'string', + optional => 1, + }, expire => { description => "Account expiration date (seconds since epoch). '0' means no expiration date.", type => 'integer', @@ -173,6 +178,7 @@ __PACKAGE__->register_method ({ $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if $param->{lastname}; $usercfg->{users}->{$username}->{email} = $param->{email} if $param->{email}; $usercfg->{users}->{$username}->{comment} = $param->{comment} if $param->{comment}; + $usercfg->{users}->{$username}->{keys} = $param->{keys} if $param->{keys}; cfs_write_file("user.cfg", $usercfg); }, "create user failed"); @@ -203,6 +209,7 @@ __PACKAGE__->register_method ({ lastname => { type => 'string', optional => 1 }, email => { type => 'string', optional => 1 }, comment => { type => 'string', optional => 1 }, + keys => { type => 'string', optional => 1 }, groups => { type => 'array' }, } }, @@ -247,6 +254,11 @@ __PACKAGE__->register_method ({ lastname => { type => 'string', optional => 1 }, email => { type => 'string', optional => 1, format => 'email-opt' }, comment => { type => 'string', optional => 1 }, + keys => { + description => "Keys for two factor auth (yubico).", + type => 'string', + optional => 1, + }, expire => { description => "Account expiration date (seconds since epoch). '0' means no expiration date.", type => 'integer', @@ -290,6 +302,7 @@ __PACKAGE__->register_method ({ $usercfg->{users}->{$username}->{lastname} = $param->{lastname} if defined($param->{lastname}); $usercfg->{users}->{$username}->{email} = $param->{email} if defined($param->{email}); $usercfg->{users}->{$username}->{comment} = $param->{comment} if defined($param->{comment}); + $usercfg->{users}->{$username}->{keys} = $param->{keys} if defined($param->{keys}); cfs_write_file("user.cfg", $usercfg); }, "update user failed"); diff --git a/PVE/AccessControl.pm b/PVE/AccessControl.pm index 3e6ed31..9e764d2 100644 --- a/PVE/AccessControl.pm +++ b/PVE/AccessControl.pm @@ -363,10 +363,30 @@ sub check_user_enabled { return undef; } +sub verify_one_time_pw { + my ($usercfg, $username, $tfa_cfg, $otp) = @_; + + my $type = $tfa_cfg->{type}; + + die "missing one time password for Factor-two authentication '$type'\n" if !$otp; + + # fixme: proxy support? + my $proxy; + + if ($type eq 'yubico') { + my $keys = $usercfg->{users}->{$username}->{keys}; + yubico_verify_otp($otp, $keys, $tfa_cfg->{url}, $tfa_cfg->{id}, $tfa_cfg->{key}, $proxy); + } else { + die "unknown tfa type '$type'\n"; + } + + die "implement me"; +} + # password should be utf8 encoded # Note: some pluging delay/sleep if auth fails sub authenticate_user { - my ($username, $password) = @_; + my ($username, $password, $otp) = @_; die "no username specified\n" if !$username; @@ -390,6 +410,11 @@ sub authenticate_user { my $plugin = PVE::Auth::Plugin->lookup($cfg->{type}); $plugin->authenticate_user($cfg, $realm, $ruid, $password); + if ($cfg->{tfa}) { + my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($cfg->{tfa}); + verify_one_time_pw($usercfg, $username, $tfa_cfg, $otp); + } + return $username; } @@ -698,7 +723,7 @@ sub parse_user_config { my $et = shift @data; if ($et eq 'user') { - my ($user, $enable, $expire, $firstname, $lastname, $email, $comment) = @data; + my ($user, $enable, $expire, $firstname, $lastname, $email, $comment, $keys) = @data; my (undef, undef, $realm) = PVE::Auth::Plugin::verify_username($user, 1); if (!$realm) { @@ -730,6 +755,7 @@ sub parse_user_config { $cfg->{users}->{$user}->{email} = $email; $cfg->{users}->{$user}->{comment} = PVE::Tools::decode_text($comment) if $comment; $cfg->{users}->{$user}->{expire} = $expire; + $cfg->{users}->{$user}->{keys} = $keys if $keys; # allowed yubico key ids #$cfg->{users}->{$user}->{groups}->{$group} = 1; #$cfg->{groups}->{$group}->{$user} = 1; @@ -875,7 +901,8 @@ sub write_user_config { my $comment = $d->{comment} ? PVE::Tools::encode_text($d->{comment}) : ''; my $expire = int($d->{expire} || 0); my $enable = $d->{enable} ? 1 : 0; - $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:\n"; + my $keys = $d->{keys} ? $d->{keys} : ''; + $data .= "user:$user:$enable:$expire:$firstname:$lastname:$email:$comment:$keys:\n"; } $data .= "\n"; @@ -1118,14 +1145,19 @@ sub yubico_compute_param_sig { } sub yubico_verify_otp { - my ($otp, $api_id, $api_key, $proxy) = @_; + my ($otp, $keys, $url, $api_id, $api_key, $proxy) = @_; + + die "yubico: missing password\n" if !defined($otp); + die "yubico: missing API ID\n" if !defined($api_id); + die "yubico: missing API KEY\n" if !defined($api_key); + die "yubico: no associated yubico keys\n" if $keys =~ m/^\s+$/; - die "yubicloud: wrong OTP lenght\n" if (length($otp) < 32) || (length($otp) > 48); + die "yubico: wrong OTP lenght\n" if (length($otp) < 32) || (length($otp) > 48); # we always use http, because https cert verification always make problem, and # some proxies does not work with https. - my $url = 'http://api2.yubico.com/wsapi/2.0/verify'; + $url = 'http://api2.yubico.com/wsapi/2.0/verify' if !defined($url); my $params = { nonce => Digest::HMAC_SHA1::hmac_sha1_hex(time(), rand()), @@ -1174,12 +1206,22 @@ sub yubico_verify_otp { if ($api_key) { my ($datastr, $vsig) = yubico_compute_param_sig($result, $api_key); $vsig = uri_unescape($vsig); - die "yubicloud: result signature verification failed\n" if $rsig ne $vsig; + die "yubico: result signature verification failed\n" if $rsig ne $vsig; } - die "yubicloud auth failed: $result->{status}\n" if $result->{status} ne 'OK'; + die "yubico auth failed: $result->{status}\n" if $result->{status} ne 'OK'; + + my $publicid = $result->{publicid} = substr(lc($result->{otp}), 0, 12); + + my $found; + foreach my $k (PVE::Tools::split_list($keys)) { + if ($k eq $publicid) { + $found = 1; + last; + } + } - $result->{publicid} = substr(lc($result->{otp}), 0, 12); + die "yubico auth failed: key does not belong to user\n" if !$found; return $result; } diff --git a/PVE/Auth/AD.pm b/PVE/Auth/AD.pm index 35396b9..d33d393 100755 --- a/PVE/Auth/AD.pm +++ b/PVE/Auth/AD.pm @@ -57,6 +57,7 @@ sub properties { optional => 1, maxLength => 256, }, + tfa => PVE::JSONSchema::get_standard_option('tfa'), }; } @@ -69,6 +70,7 @@ sub options { secure => { optional => 1 }, default => { optional => 1 },, comment => { optional => 1 }, + tfa => { optional => 1 }, }; } diff --git a/PVE/Auth/LDAP.pm b/PVE/Auth/LDAP.pm index a423ee7..3f867ec 100755 --- a/PVE/Auth/LDAP.pm +++ b/PVE/Auth/LDAP.pm @@ -40,6 +40,7 @@ sub options { secure => { optional => 1 }, default => { optional => 1 }, comment => { optional => 1 }, + tfa => { optional => 1 }, }; } diff --git a/PVE/Auth/PAM.pm b/PVE/Auth/PAM.pm index 04f0d93..d845978 100755 --- a/PVE/Auth/PAM.pm +++ b/PVE/Auth/PAM.pm @@ -17,6 +17,7 @@ sub options { return { default => { optional => 1 }, comment => { optional => 1 }, + tfa => { optional => 1 }, }; } diff --git a/PVE/Auth/PVE.pm b/PVE/Auth/PVE.pm index 5f60cf3..7f03b9e 100755 --- a/PVE/Auth/PVE.pm +++ b/PVE/Auth/PVE.pm @@ -62,10 +62,11 @@ sub type { return 'pve'; } -sub defaults { +sub options { return { default => { optional => 1 }, comment => { optional => 1 }, + tfa => { optional => 1 }, }; } diff --git a/PVE/Auth/Plugin.pm b/PVE/Auth/Plugin.pm index e9d54f0..f19a33c 100755 --- a/PVE/Auth/Plugin.pm +++ b/PVE/Auth/Plugin.pm @@ -83,6 +83,49 @@ PVE::JSONSchema::register_standard_option('userid', { maxLength => 64, }); +PVE::JSONSchema::register_format('pve-tfa-config', \&verify_tfa_config); +sub verify_tfa_config { + my ($value, $noerr) = @_; + + return $value if parse_tfa_config($value); + + return undef if $noerr; + + die "unable to parse tfa option\n"; +} + +PVE::JSONSchema::register_standard_option('tfa', { + description => "Use Two-factor authentication.", + type => 'string', format => 'pve-tfa-config', + optional => 1, + maxLength => 128, +}); + +sub parse_tfa_config { + my ($data) = @_; + + my $res = {}; + + foreach my $kvp (split(/,/, $data)) { + + if ($kvp =~ m/^type=(yubico)$/) { + $res->{type} = $1; + } elsif ($kvp =~ m/^id=(\S+)$/) { + $res->{id} = $1; + } elsif ($kvp =~ m/^key=(\S+)$/) { + $res->{key} = $1; + } elsif ($kvp =~ m/^url=(\S+)$/) { + $res->{url} = $1; + } else { + return undef; + } + } + + return undef if !$res->{type}; + + return $res; +} + sub encrypt_pw { my ($pw) = @_; @@ -140,16 +183,14 @@ sub parse_config { # add default domains - $cfg->{ids}->{pve} = { - type => 'pve', - comment => "Proxmox VE authentication server", - }; + $cfg->{ids}->{pve}->{type} = 'pve'; # force type + $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server" + if !$cfg->{ids}->{pve}->{comment}; - $cfg->{ids}->{pam} = { - type => 'pam', - plugin => 'PVE::Auth::PAM', - comment => "Linux PAM standard authentication", - }; + $cfg->{ids}->{pam}->{type} = 'pam'; # force type + $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM'; + $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication" + if !$cfg->{ids}->{pam}->{comment}; return $cfg; };