From c0cbfd91c54c2ac9891f87737d66d29ca55a2f00 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 23 Feb 2024 08:30:04 -0600 Subject: [PATCH] Add an option to skip two factor authentication on trusted devices. A checkbox is added to the two factor verification page. If that is checked, then a signed cookie (separate from the session cookie) is set. If that cookie is set, then two factor verification is skipped for sign in attempts. --- lib/WeBWorK/Authen.pm | 42 ++++++++++++++++++- .../TwoFactorAuthentication.html.ep | 11 ++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/WeBWorK/Authen.pm b/lib/WeBWorK/Authen.pm index 9497bcc44d..ca8263fe66 100644 --- a/lib/WeBWorK/Authen.pm +++ b/lib/WeBWorK/Authen.pm @@ -156,12 +156,38 @@ sub verify { $self->{was_verified} = $self->do_verify; + my $remember_2fa = $c->signed_cookie('WeBWorK.2FA.' . $c->ce->{courseName}); + + if ($self->{was_verified} + && $self->{login_type} eq 'normal' + && !$self->{external_auth} + && (!$c->{rpc} || ($c->{rpc} && !$c->stash->{disable_cookies})) + && $remember_2fa + && !$c->db->getPassword($self->{user_id})->otp_secret) + { + # If there is not a otp secret saved in the database, and there is a cookie saved to skip two factor + # authentication, then delete it. The user needs to set up two factor authentication again. + $c->signed_cookie( + 'WeBWorK.2FA.' . $c->ce->{courseName} => 1, + { + max_age => 0, + expires => 1, + path => $c->ce->{webworkURLRoot}, + samesite => $c->ce->{CookieSameSite}, + secure => $c->ce->{CookieSecure}, + httponly => 1 + } + ); + $remember_2fa = 0; + } + if ($self->{was_verified} && $self->{login_type} eq 'normal' && !$self->{external_auth} && (!$c->{rpc} || ($c->{rpc} && !$c->stash->{disable_cookies})) && $c->ce->two_factor_authentication_enabled - && ($self->{initial_login} || $self->session->{two_factor_verification_needed})) + && ($self->{initial_login} || $self->session->{two_factor_verification_needed}) + && !$remember_2fa) { $self->{was_verified} = 0; $self->session(two_factor_verification_needed => 1); @@ -446,6 +472,20 @@ sub verify_normal_user { { delete $self->session->{two_factor_verification_needed}; + # Store a cookie that signifies this devices skips two factor + # authentication if the skip_2fa checkbox was checked. + $c->signed_cookie( + 'WeBWorK.2FA.' . $c->ce->{courseName} => 1, + { + max_age => 3600 * 24 * 365, # This cookie is valid for one year. + expires => time + 3600 * 24 * 365, + path => $c->ce->{webworkURLRoot}, + samesite => $c->ce->{CookieSameSite}, + secure => $c->ce->{CookieSecure}, + httponly => 1 + } + ) if $c->param('skip_2fa'); + # This is the case of initial setup. Save the secret from the session to the database. if ($self->session->{otp_secret}) { $password->otp_secret($self->session->{otp_secret}); diff --git a/templates/ContentGenerator/TwoFactorAuthentication.html.ep b/templates/ContentGenerator/TwoFactorAuthentication.html.ep index 6a1fbf8b77..65e582d0e4 100644 --- a/templates/ContentGenerator/TwoFactorAuthentication.html.ep +++ b/templates/ContentGenerator/TwoFactorAuthentication.html.ep @@ -36,12 +36,21 @@ <%= form_for current_route, method => 'POST', begin =%> <%= $hidden_fields =%> % -
+
<%= text_field otp_code => '', id => 'otp_code', class => 'form-control', placeholder => '', autocomplete => 'off', autocapitalize => 'none', spellcheck => 'false' =%> <%= label_for otp_code => maketext('One-Time Code') =%>
+
+ <%= check_box(skip_2fa => 1, id => 'skip_2fa', class => 'form-check-input') =%> + <%= label_for skip_2fa => maketext('Skip two factor verification on this device.') =%> +
+
+ <%= maketext('If you check the box above, then two factor verification will be skipped from now on when ' + . 'signing in with this browser. This feature is not safe for public workstations, untrusted machines, ' + . 'and machines over which you do not have direct control.') =%> +
% if ($authen_error) {
<%= $authen_error =%>
% }