1 package PVE
::API2
::TFA
;
6 use PVE
::AccessControl
;
7 use PVE
::Cluster
qw(cfs_read_file cfs_write_file);
8 use PVE
::JSONSchema
qw(get_standard_option);
9 use PVE
::Exception
qw(raise raise_perm_exc raise_param_exc);
10 use PVE
::RPCEnvironment
;
12 use PVE
::API2
::AccessControl
; # for old login api get_u2f_instance method
16 use base
qw(PVE::RESTHandler);
18 my $OPTIONAL_PASSWORD_SCHEMA = {
19 description
=> "The current password.",
21 optional
=> 1, # Only required if not root@pam
26 my $TFA_TYPE_SCHEMA = {
28 description
=> 'TFA Entry Type.',
29 enum
=> [qw(totp u2f webauthn recovery yubico)],
32 my %TFA_INFO_PROPERTIES = (
35 description
=> 'The id used to reference this entry.',
39 description
=> 'User chosen description for this entry.',
43 description
=> 'Creation time of this entry as unix epoch.',
47 description
=> 'Whether this TFA entry is currently enabled.',
53 my $TYPED_TFA_ENTRY_SCHEMA = {
55 description
=> 'TFA Entry.',
57 type
=> $TFA_TYPE_SCHEMA,
64 description
=> 'A TFA entry id.',
67 my $TFA_UPDATE_INFO_SCHEMA = {
72 description
=> 'The id of a newly added TFA entry.',
78 'When adding u2f entries, this contains a challenge the user must respond to in order'
79 .' to finish the registration.'
85 'When adding recovery codes, this contains the list of codes to be displayed to'
89 description
=> 'A recovery entry.'
95 # Only root may modify root, regular users need to specify their password.
97 # Returns the userid returned from `verify_username`.
98 # Or ($userid, $realm) in list context.
99 my sub root_permission_check
: prototype($$$$) {
100 my ($rpcenv, $authuser, $userid, $password) = @_;
102 ($userid, my $ruid, my $realm) = PVE
::AccessControl
::verify_username
($userid);
103 $rpcenv->check_user_exist($userid);
105 raise_perm_exc
() if $userid eq 'root@pam' && $authuser ne 'root@pam';
107 # Regular users need to confirm their password to change TFA settings.
108 if ($authuser ne 'root@pam') {
109 raise_param_exc
({ 'password' => 'password is required to modify TFA data' })
110 if !defined($password);
112 my $domain_cfg = cfs_read_file
('domains.cfg');
113 my $cfg = $domain_cfg->{ids
}->{$realm};
114 die "auth domain '$realm' does not exist\n" if !$cfg;
115 my $plugin = PVE
::Auth
::Plugin-
>lookup($cfg->{type
});
116 $plugin->authenticate_user($cfg, $realm, $ruid, $password);
119 return wantarray ?
($userid, $realm) : $userid;
124 __PACKAGE__-
>register_method({
125 name
=> 'verify_tfa',
128 permissions
=> { user
=> 'all' },
129 protected
=> 1, # else we can't access shadow files
130 allowtoken
=> 0, # we don't want tokens to access TFA information
131 description
=> 'Finish a u2f challenge.',
133 additionalProperties
=> 0,
137 description
=> 'The response to the current authentication challenge.',
144 ticket
=> { type
=> 'string' },
151 my $rpcenv = PVE
::RPCEnvironment
::get
();
152 my $authuser = $rpcenv->get_user();
153 my ($username, undef, $realm) = PVE
::AccessControl
::verify_username
($authuser);
155 my ($tfa_type, $tfa_data) = PVE
::AccessControl
::user_get_tfa
($username, $realm, 0);
156 if (!defined($tfa_type)) {
157 raise
('no u2f data available');
161 if ($tfa_type eq 'u2f') {
162 my $challenge = $rpcenv->get_u2f_challenge()
163 or raise
('no active challenge');
165 my $keyHandle = $tfa_data->{keyHandle
};
166 my $publicKey = $tfa_data->{publicKey
};
167 raise
("incomplete u2f setup")
168 if !defined($keyHandle) || !defined($publicKey);
170 my $u2f = PVE
::API2
::AccessControl
::get_u2f_instance
($rpcenv, $publicKey, $keyHandle);
171 $u2f->set_challenge($challenge);
173 my ($counter, $present) = $u2f->auth_verify($param->{response
});
174 # Do we want to do anything with these?
176 # sanity check before handing off to the verification code:
177 my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
178 my $config = $tfa_data->{config
} or die "bad tfa entry\n";
179 PVE
::AccessControl
::verify_one_time_pw
($tfa_type, $authuser, $keys, $config, $param->{response
});
183 my $clientip = $rpcenv->get_client_ip() || '';
184 syslog
('err', "authentication verification failure; rhost=$clientip user=$authuser msg=$err");
185 die PVE
::Exception-
>new("authentication failure\n", code
=> 401);
189 ticket
=> PVE
::AccessControl
::assemble_ticket
($authuser),
190 cap
=> $rpcenv->compute_api_permission($authuser),
196 __PACKAGE__-
>register_method ({
197 name
=> 'list_user_tfa',
202 ['userid-param', 'self'],
203 ['userid-group', ['User.Modify', 'Sys.Audit']],
206 protected
=> 1, # else we can't access shadow files
207 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
208 description
=> 'List TFA configurations of users.',
210 additionalProperties
=> 0,
212 userid
=> get_standard_option
('userid', {
213 completion
=> \
&PVE
::AccessControl
::complete_username
,
218 description
=> "A list of the user's TFA entries.",
220 items
=> $TYPED_TFA_ENTRY_SCHEMA,
224 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
225 return $tfa_cfg->api_list_user_tfa($param->{userid
});
228 __PACKAGE__-
>register_method ({
229 name
=> 'get_tfa_entry',
230 path
=> '{userid}/{id}',
234 ['userid-param', 'self'],
235 ['userid-group', ['User.Modify', 'Sys.Audit']],
238 protected
=> 1, # else we can't access shadow files
239 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
240 description
=> 'Fetch a requested TFA entry if present.',
242 additionalProperties
=> 0,
244 userid
=> get_standard_option
('userid', {
245 completion
=> \
&PVE
::AccessControl
::complete_username
,
247 id
=> $TFA_ID_SCHEMA,
250 returns
=> $TYPED_TFA_ENTRY_SCHEMA,
253 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
254 my $id = $param->{id
};
255 my $entry = $tfa_cfg->api_get_tfa_entry($param->{userid
}, $id);
256 raise
("No such tfa entry '$id'", 404) if !$entry;
260 __PACKAGE__-
>register_method ({
261 name
=> 'delete_tfa',
262 path
=> '{userid}/{id}',
266 ['userid-param', 'self'],
267 ['userid-group', ['User.Modify']],
270 protected
=> 1, # else we can't access shadow files
271 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
272 description
=> 'Delete a TFA entry by ID.',
274 additionalProperties
=> 0,
276 userid
=> get_standard_option
('userid', {
277 completion
=> \
&PVE
::AccessControl
::complete_username
,
279 id
=> $TFA_ID_SCHEMA,
280 password
=> $OPTIONAL_PASSWORD_SCHEMA,
283 returns
=> { type
=> 'null' },
287 PVE
::AccessControl
::assert_new_tfa_config_available
();
289 my $rpcenv = PVE
::RPCEnvironment
::get
();
290 my $authuser = $rpcenv->get_user();
292 root_permission_check
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
294 return PVE
::AccessControl
::lock_tfa_config
(sub {
295 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
296 $tfa_cfg->api_delete_tfa($userid, $param->{id
});
297 cfs_write_file
('priv/tfa.cfg', $tfa_cfg);
301 __PACKAGE__-
>register_method ({
306 description
=> "Returns all or just the logged-in user, depending on privileges.",
309 protected
=> 1, # else we can't access shadow files
310 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
311 description
=> 'List TFA configurations of users.',
313 additionalProperties
=> 0,
317 description
=> "The list tuples of user and TFA entries.",
324 description
=> 'User this entry belongs to.',
328 items
=> $TYPED_TFA_ENTRY_SCHEMA,
336 my $rpcenv = PVE
::RPCEnvironment
::get
();
337 my $authuser = $rpcenv->get_user();
338 my $top_level_allowed = ($authuser eq 'root@pam');
340 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
341 return $tfa_cfg->api_list_tfa($authuser, $top_level_allowed);
344 __PACKAGE__-
>register_method ({
345 name
=> 'add_tfa_entry',
350 ['userid-param', 'self'],
351 ['userid-group', ['User.Modify']],
354 protected
=> 1, # else we can't access shadow files
355 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
356 description
=> 'Add a TFA entry for a user.',
358 additionalProperties
=> 0,
360 userid
=> get_standard_option
('userid', {
361 completion
=> \
&PVE
::AccessControl
::complete_username
,
363 type
=> $TFA_TYPE_SCHEMA,
366 description
=> 'A description to distinguish multiple entries from one another',
372 description
=> "A totp URI.",
378 'The current value for the provided totp URI, or a Webauthn/U2F'
379 .' challenge response',
384 description
=> 'When responding to a u2f challenge: the original challenge string',
387 password
=> $OPTIONAL_PASSWORD_SCHEMA,
390 returns
=> $TFA_UPDATE_INFO_SCHEMA,
394 PVE
::AccessControl
::assert_new_tfa_config_available
();
396 my $rpcenv = PVE
::RPCEnvironment
::get
();
397 my $authuser = $rpcenv->get_user();
398 my ($userid, $realm) =
399 root_permission_check
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
401 my $type = delete $param->{type
};
402 my $value = delete $param->{value
};
403 if ($type eq 'yubico') {
404 $value = validate_yubico_otp
($userid, $realm, $value);
407 return PVE
::AccessControl
::lock_tfa_config
(sub {
408 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
409 PVE
::AccessControl
::configure_u2f_and_wa
($tfa_cfg);
411 my $response = $tfa_cfg->api_add_tfa_entry(
413 $param->{description
},
420 cfs_write_file
('priv/tfa.cfg', $tfa_cfg);
426 sub validate_yubico_otp
: prototype($$) {
427 my ($userid, $realm, $value) = @_;
429 my $domain_cfg = cfs_read_file
('domains.cfg');
430 my $realm_cfg = $domain_cfg->{ids
}->{$realm};
431 die "auth domain '$realm' does not exist\n" if !$realm_cfg;
433 my $realm_tfa = $realm_cfg->{tfa
};
434 die "no yubico otp configuration available for realm $realm\n"
437 $realm_tfa = PVE
::Auth
::Plugin
::parse_tfa_config
($realm_tfa);
438 die "realm is not setup for Yubico OTP\n"
439 if !$realm_tfa || $realm_tfa->{type
} ne 'yubico';
441 my $public_key = substr($value, 0, 12);
443 PVE
::AccessControl
::authenticate_yubico_do
($value, $public_key, $realm_tfa);
448 __PACKAGE__-
>register_method ({
449 name
=> 'update_tfa_entry',
450 path
=> '{userid}/{id}',
454 ['userid-param', 'self'],
455 ['userid-group', ['User.Modify']],
458 protected
=> 1, # else we can't access shadow files
459 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
460 description
=> 'Add a TFA entry for a user.',
462 additionalProperties
=> 0,
464 userid
=> get_standard_option
('userid', {
465 completion
=> \
&PVE
::AccessControl
::complete_username
,
467 id
=> $TFA_ID_SCHEMA,
470 description
=> 'A description to distinguish multiple entries from one another',
476 description
=> 'Whether the entry should be enabled for login.',
479 password
=> $OPTIONAL_PASSWORD_SCHEMA,
482 returns
=> { type
=> 'null' },
486 PVE
::AccessControl
::assert_new_tfa_config_available
();
488 my $rpcenv = PVE
::RPCEnvironment
::get
();
489 my $authuser = $rpcenv->get_user();
491 root_permission_check
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
493 PVE
::AccessControl
::lock_tfa_config
(sub {
494 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
496 $tfa_cfg->api_update_tfa_entry(
499 $param->{description
},
503 cfs_write_file
('priv/tfa.cfg', $tfa_cfg);