+sub oath_verify_otp {
+ my ($otp, $keys, $step, $digits) = @_;
+
+ die "oath: missing password\n" if !defined($otp);
+ die "oath: no associated oath keys\n" if $keys =~ m/^\s+$/;
+
+ $step = 30 if !$step;
+ $digits = 6 if !$digits;
+
+ my $found;
+
+ my $parser = sub {
+ my $line = shift;
+
+ if ($line =~ m/^\d{6}$/) {
+ $found = 1 if $otp eq $line;
+ }
+ };
+
+ foreach my $k (PVE::Tools::split_list($keys)) {
+ # Note: we generate 3 values to allow small time drift
+ my $now = localtime(time() - $step);
+ my $cmd = ['oathtool', '--totp', '--digits', $digits, '-N', $now, '-s', $step, '-w', '2', '-b', $k];
+ eval { run_command($cmd, outfunc => $parser, errfunc => sub {}); };
+ last if $found;
+ }
+
+ die "oath auth failed\n" if !$found;
+}
+
+# bash completion helpers
+
+sub complete_username {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{users}} ];
+}
+
+sub complete_group {
+
+ my $user_cfg = cfs_read_file('user.cfg');
+
+ return [ keys %{$user_cfg->{groups}} ];
+}
+
+sub complete_realm {
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+
+ return [ keys %{$domain_cfg->{ids}} ];
+}
+