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);
} 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) ];
};
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);
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',
$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 = $@) {
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 },
},
},
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;
}
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});
}
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',
$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");
lastname => { type => 'string', optional => 1 },
email => { type => 'string', optional => 1 },
comment => { type => 'string', optional => 1 },
+ keys => { type => 'string', optional => 1 },
groups => { type => 'array' },
}
},
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',
$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");
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;
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;
}
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) {
$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;
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";
}
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()),
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;
}
optional => 1,
maxLength => 256,
},
+ tfa => PVE::JSONSchema::get_standard_option('tfa'),
};
}
secure => { optional => 1 },
default => { optional => 1 },,
comment => { optional => 1 },
+ tfa => { optional => 1 },
};
}
secure => { optional => 1 },
default => { optional => 1 },
comment => { optional => 1 },
+ tfa => { optional => 1 },
};
}
return {
default => { optional => 1 },
comment => { optional => 1 },
+ tfa => { optional => 1 },
};
}
return 'pve';
}
-sub defaults {
+sub options {
return {
default => { optional => 1 },
comment => { optional => 1 },
+ tfa => { optional => 1 },
};
}
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) = @_;
# 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;
};