Bivio::Biz::Model::UserLoginMFABaseForm
# Copyright (c) 2025 bivio Software, Inc. All rights reserved. package Bivio::Biz::Model::UserLoginMFABaseForm; use strict; use Bivio::Base 'Biz.FormModel'; my($_A) = b_use('Action.Acknowledgement'); my($_AAC) = b_use('Action.AccessChallenge'); my($_C) = b_use('AgentHTTP.Cookie'); my($_MM) = b_use('Type.MFAMethod'); my($_TAC) = b_use('Type.AccessCode'); my($_TACS) = b_use('Type.AccessCodeStatus'); my($_UAC) = b_use('Model.UserAccessCode'); my($_ULF) = b_use('Model.UserLoginForm'); sub MFA_RECOVERY_CODE_FIELD { return 'mrc'; } sub SENSITIVE_FIELDS { return ['mfa_recovery_code']; } sub bypass_challenge { shift->internal_put_field(bypass_challenge => 1); return; } sub delete_cookie { my($proto, $cookie) = @_; $cookie->delete( $proto->MFA_RECOVERY_CODE_FIELD, ); return; } sub execute_ok { my($self) = @_; $self->internal_set_cookie; $self->internal_login_form->set_user($self->get('realm_owner'), $self->ureq('cookie'), $self->req); return unless $self->get('realm_owner'); unless ($self->unsafe_get('bypass_challenge')) { $_AAC->assert_challenge($self->req, { type => $_TAC->ESCALATION_CHALLENGE, status => $_TACS->PENDING, })->update({status => $_TACS->PASSED}); } my($next); if (my $mrcm = $self->unsafe_get('mfa_recovery_code_model')) { $mrcm->update({status => $_TACS->ARCHIVED}); $_A->save_label(mfa_recovery_code_used => $self->req); $next = 'refill_task'; } $next ||= $_AAC->get_next($self->req); return { method => 'server_redirect', task_id => $next, no_context => 1, carry_query => 1, } if $next; return; } sub internal_clear_sensitive_fields { my($self) = @_; foreach my $f (@{$self->SENSITIVE_FIELDS}) { $self->internal_put_field($f => undef); $self->internal_clear_literal($f); } return; } sub internal_initialize { my($self) = @_; return $self->merge_initialize_info($self->SUPER::internal_initialize, { version => 1, $self->field_decl( visible => [ [qw(mfa_recovery_code Line)], [qw(disable_mfa Boolean)], ], other => [ [qw(realm_owner Model.RealmOwner)], [qw(mfa_recovery_code_model Model.UserAccessCode)], [qw(do_logout Boolean)], [qw(bypass_challenge Boolean)], ], ), }); } sub internal_login_form { return $_ULF; } sub internal_pre_execute { my($self) = @_; if ($self->unsafe_get('do_logout')) { $self->internal_put_field(realm_owner => undef); return; } return unless $self->ureq('cookie'); $self->internal_put_field( realm_owner => $self->internal_login_form->load_cookie_user($self->req, $self->req('cookie'))); b_die('FORBIDDEN') unless $self->get('realm_owner') && $self->get('realm_owner')->get_configured_mfa_methods($_MM->TOTP); return if $self->unsafe_get('bypass_challenge'); $_AAC->unauth_assert_challenge($self->req, { user_id => $self->get_nested(qw(realm_owner realm_id)), type => $_TAC->LOGIN_CHALLENGE, status => $_TACS->PASSED, }); return; } sub internal_set_cookie { my($self) = @_; my($cookie) = $self->ureq('cookie'); return undef unless $cookie; $self->delete_cookie($cookie); if ($self->get('realm_owner')) { $_C->assert_is_ok($self->req); if ($self->unsafe_get('mfa_recovery_code')) { $cookie->put($self->MFA_RECOVERY_CODE_FIELD => $self->get('mfa_recovery_code')); return $cookie; } } return undef; } sub is_valid_cookie { my($proto, $cookie, $auth_user) = @_; if (my $c = $cookie->unsafe_get($proto->MFA_RECOVERY_CODE_FIELD)) { return $_UAC->is_valid_cookie_code($auth_user->get('realm_id'), $c); } return 0; } sub validate { my($self, $realm_owner, $mfa_recovery_code) = @_; if (defined($realm_owner) && defined($mfa_recovery_code)) { $self->internal_put_field(realm_owner => $realm_owner); $self->internal_put_field(mfa_recovery_code => $mfa_recovery_code); } _validate_recovery_code($self); return; } sub _validate_recovery_code { my($self) = @_; if ($self->get('mfa_recovery_code')) { my($v, $e) = $_TAC->MFA_RECOVERY->from_literal_for_type($self->get('mfa_recovery_code')); if ($v && (my $sc = $self->new_other('UserAccessCode')->unauth_load_by_code($v, { user_id => $self->get_nested(qw(realm_owner realm_id)), type => $_TAC->MFA_RECOVERY, status => $_TACS->ACTIVE, }))) { $self->internal_put_field(mfa_recovery_code_model => $sc); return; } elsif ($e) { $self->internal_put_error(mfa_recovery_code => $e); } else { $self->internal_put_error(mfa_recovery_code => 'INVALID_MFA_RECOVERY_CODE'); } # Need to stay on page or the login attempt would get rolled back $self->internal_stay_on_page; return; } return; } 1;