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();
399 root_permission_check
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
401 return PVE
::AccessControl
::lock_tfa_config
(sub {
402 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
403 PVE
::AccessControl
::configure_u2f_and_wa
($tfa_cfg);
405 my $response = $tfa_cfg->api_add_tfa_entry(
407 $param->{description
},
414 cfs_write_file
('priv/tfa.cfg', $tfa_cfg);
420 __PACKAGE__-
>register_method ({
421 name
=> 'update_tfa_entry',
422 path
=> '{userid}/{id}',
426 ['userid-param', 'self'],
427 ['userid-group', ['User.Modify']],
430 protected
=> 1, # else we can't access shadow files
431 allowtoken
=> 0, # we don't want tokens to change the regular user's TFA settings
432 description
=> 'Add a TFA entry for a user.',
434 additionalProperties
=> 0,
436 userid
=> get_standard_option
('userid', {
437 completion
=> \
&PVE
::AccessControl
::complete_username
,
439 id
=> $TFA_ID_SCHEMA,
442 description
=> 'A description to distinguish multiple entries from one another',
448 description
=> 'Whether the entry should be enabled for login.',
451 password
=> $OPTIONAL_PASSWORD_SCHEMA,
454 returns
=> { type
=> 'null' },
458 PVE
::AccessControl
::assert_new_tfa_config_available
();
460 my $rpcenv = PVE
::RPCEnvironment
::get
();
461 my $authuser = $rpcenv->get_user();
463 root_permission_check
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
465 PVE
::AccessControl
::lock_tfa_config
(sub {
466 my $tfa_cfg = cfs_read_file
('priv/tfa.cfg');
468 $tfa_cfg->api_update_tfa_entry(
471 $param->{description
},
475 cfs_write_file
('priv/tfa.cfg', $tfa_cfg);