Bivio::Biz::Model::UserLoginBaseForm
# Copyright (c) 2011-2023 bivio Software, Inc. All Rights Reserved. package Bivio::Biz::Model::UserLoginBaseForm; use strict; use Bivio::Base 'Biz.FormModel'; my($_LAS) = b_use('Type.LoginAttemptState'); my($_R) = b_use('Biz.Random'); sub PASSWORD_FIELD { return 'p'; } sub USER_FIELD { # Returns the cookie key for the super user value. return 'u'; } sub get_basic_authorization_realm { my($self) = shift; my($ro) = $self->unsafe_get('realm_owner'); return $ro && $ro->require_otp # Extra space helps out on Mac, which puts a '.' right after realm ? 'Challenge: ' . $self->req('Model.OTP')->get_challenge . ' ' : '*'; } sub internal_initialize { my($self) = @_; # B<FOR INTERNAL USE ONLY> my($info) = $self->merge_initialize_info( shift->SUPER::internal_initialize(@_), { # Form versions are checked and mismatches causes VERSION_MISMATCH version => 1, # This form's "next" is the task which redirected to this form. # If redirect was not from a task, returns to normal "next". require_context => 1, # Fields which are shown to the user. visible => [ { name => 'login', type => 'LoginName', constraint => 'NOT_NULL', form_name => 'x1', }, { name => 'RealmOwner.password', form_name => 'x2', }, ], # Fields used internally which are computed dynamically. # They are not sent to or returned from the user. other => [ # The following fields are computed by validate { name => 'realm_owner', # PropertyModels may act as types. type => 'Bivio::Biz::Model::RealmOwner', constraint => 'NONE', }, { # Only set by validate name => 'validate_called', type => 'Boolean', constraint => 'NONE', }, { # Don't assert the cookie is valid name => 'disable_assert_cookie', type => 'Boolean', constraint => 'NONE', }, { name => 'via_mta', type => 'Boolean', constraint => 'NONE', }, { name => 'do_locked_out_task', type => 'Boolean', constraint => 'NONE', }, ], }); foreach my $field (@{$info->{visible}}) { $field = { name => $field, } unless ref($field); next if $field->{form_name}; $field->{form_name} = $field->{name}; } return $info; } sub internal_validate_login_value { my(undef, $delegator, $value) = shift->delegated_args(@_); my($owner) = $delegator->new_other('RealmOwner'); my($err) = $owner->validate_login($value); return $err ? (undef, $err) : ($owner, undef); } sub validate { my(undef, $delegator, $login, $password) = shift->delegated_args(@_); # Checks the form property values. Puts errors on the fields # if there are any. if (defined($login) && defined($password)) { $delegator->internal_put_field(login => $login); $delegator->internal_put_field('RealmOwner.password' => $password); } _validate($delegator); # don't send password back to client in error case if ($delegator->in_error) { $delegator->internal_put_field('RealmOwner.password' => undef); $delegator->internal_clear_literal('RealmOwner.password'); } return; } sub validate_and_execute_ok { my(undef, $delegator) = shift->delegated_args(@_); my($res) = $delegator->SUPER::validate_and_execute_ok(@_); if ($delegator->unsafe_get('do_locked_out_task')) { $delegator->put_on_request(1); return { method => 'server_redirect', task_id => 'locked_out_task', query => undef, }; } return $res; } sub validate_login { my(undef, $delegator, $model_or_login, $field) = shift->delegated_args(@_); $field ||= 'login'; my($model) = ref($model_or_login) ? $model_or_login : $delegator; $model->internal_put_field($field => $model_or_login) if defined($model_or_login) && !ref($model_or_login); $model->internal_put_field(validate_called => 1); my($login) = $model->get($field); return undef unless defined($login); my($realm, $err) = $delegator->internal_validate_login_value($login); $model->internal_put_error($field => $err) if $err; $model->internal_put_field(realm_owner => $realm); return $realm; } sub _password_error { my($self, $owner) = @_; my($pw_err); return undef if $owner->get_field_type('password')->is_equal( $owner->get('password'), $self->get('RealmOwner.password'), ); return 'PASSWORD_MISMATCH' unless $owner->require_otp; return 'OTP_PASSWORD_MISMATCH' unless $self->new_other('OTP')->unauth_load_or_die({ user_id => $owner->get('realm_id') })->verify($self->get('RealmOwner.password')); return undef; } sub _record_login_attempt { my($self, $owner, $success) = @_; return $self->new_other('LoginAttempt')->create({ realm_id => $owner->get('realm_id'), login_attempt_state => $success ? $_LAS->SUCCESS : $_LAS->FAILURE, }); } sub _validate { my($self) = @_; my($owner) = $self->validate_login; return if !$owner || ($self->in_error && !$owner->require_otp); return $self->internal_put_error(login => 'USER_LOCKED_OUT') if $owner->is_locked_out; return unless _validate_login_attempt($self, $owner); $owner->maybe_upgrade_password($self->get('RealmOwner.password')); $self->internal_put_field(validate_called => 1); return; } sub _validate_login_attempt { my($self, $owner) = @_; if (my $err = _password_error($self, $owner)) { # Need to stay on page or the login attempt would get rolled back $self->internal_stay_on_page; $self->internal_put_error('RealmOwner.password' => $err); if (_record_login_attempt($self, $owner, 0)->get('login_attempt_state')->eq_locked_out) { b_warn('locked out owner=', $owner); $owner->update_password($_R->password); $self->internal_put_field(do_locked_out_task => 1); } return 0; } _record_login_attempt($self, $owner, 1); return 1; } 1;