+# Returns a tfa challenge or undef.
+sub authenticate_2nd_new : prototype($$$$) {
+ my ($username, $realm, $otp, $tfa_challenge) = @_;
+
+ my $result = lock_tfa_config(sub {
+ my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm, 1);
+
+ if (!defined($tfa_cfg)) {
+ return undef;
+ }
+
+ my $realm_type = $realm_tfa && $realm_tfa->{type};
+ $realm_type = 'totp' if $realm_type eq 'oath'; # we used to call it that
+ # verify realm type unless using recovery keys:
+ if (defined($realm_type)) {
+ if ($realm_type eq 'yubico') {
+ # Yubico auth will not be supported in rust for now...
+ if (!defined($tfa_challenge)) {
+ my $challenge = { yubico => JSON::true };
+ # Even with yubico auth we do allow recovery keys to be used:
+ if (my $recovery = $tfa_cfg->recovery_state($username)) {
+ $challenge->{recovery} = $recovery;
+ }
+ return to_json($challenge);
+ }
+
+ if ($otp =~ /^yubico:(.*)$/) {
+ $otp = $1;
+ # Defer to after unlocking the TFA config:
+ return sub {
+ authenticate_yubico_new(
+ $tfa_cfg, $username, $realm_tfa, $tfa_challenge, $otp,
+ );
+ };
+ }
+ }
+
+ my $response_type;
+ if (defined($otp)) {
+ if ($otp !~ /^([^:]+):/) {
+ die "bad otp response\n";
+ }
+ $response_type = $1;
+ }
+
+ die "realm requires $realm_type authentication\n"
+ if $response_type && $response_type ne 'recovery' && $response_type ne $realm_type;
+ }
+
+ configure_u2f_and_wa($tfa_cfg);
+
+ my $must_save = 0;
+ if (defined($tfa_challenge)) {
+ $tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
+ $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+ $tfa_challenge = undef;
+ } else {
+ $tfa_challenge = $tfa_cfg->authentication_challenge($username);
+ if (defined($otp)) {
+ if (defined($tfa_challenge)) {
+ $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+ } else {
+ die "no such challenge\n";
+ }
+ }
+ }
+
+ if ($must_save) {
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+ }
+
+ return $tfa_challenge;
+ });
+
+ # Yubico auth returns the authentication sub:
+ if (ref($result) eq 'CODE') {
+ $result = $result->();
+ }
+
+ return $result;
+}
+
+sub authenticate_yubico_new : prototype($$$) {
+ my ($tfa_cfg, $username, $realm, $tfa_challenge, $otp) = @_;
+
+ $tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
+ $tfa_challenge = from_json($tfa_challenge);
+
+ if (!$tfa_challenge->{yubico}) {
+ die "no such challenge\n";
+ }
+
+ my $keys = $tfa_cfg->get_yubico_keys($username);
+ die "no keys configured\n" if !defined($keys) || !length($keys);
+
+ authenticate_yubico_do($otp, $keys, $realm);
+
+ # return `undef` to clear the tfa challenge.
+ return undef;
+}
+
+sub authenticate_yubico_do : prototype($$$) {
+ my ($value, $keys, $realm) = @_;
+
+ # fixme: proxy support?
+ my $proxy = undef;
+
+ PVE::OTP::yubico_verify_otp($value, $keys, $realm->{url}, $realm->{id}, $realm->{key}, $proxy);
+}
+
+sub configure_u2f_and_wa : prototype($) {
+ my ($tfa_cfg) = @_;
+
+ my $dc = cfs_read_file('datacenter.cfg');
+ if (my $u2f = $dc->{u2f}) {
+ my $origin = $u2f->{origin};
+ if (!defined($origin)) {
+ my $rpcenv = PVE::RPCEnvironment::get();
+ $origin = $rpcenv->get_request_host(1);
+ if ($origin) {
+ $origin = "https://$origin";
+ } else {
+ die "failed to figure out u2f origin\n";
+ }
+ }
+ $tfa_cfg->set_u2f_config({
+ origin => $origin,
+ appid => $u2f->{appid},
+ });
+ }
+ if (my $wa = $dc->{webauthn}) {
+ $tfa_cfg->set_webauthn_config($wa);
+ }
+}
+