From 8590579b67ff64a63a91d6c11761e95dabeede88 Mon Sep 17 00:00:00 2001 From: JT Smith Date: Tue, 25 Aug 2026 23:10:51 -0500 Subject: [PATCH] Bind secondary auth to login sessions (#1271) --- CHANGES.txt | 3 ++ author.t/secondary_auth_session.t | 70 +++++++++++++++++++++++++++++++ lib/Wing/Role/Result/User.pm | 22 ++++++++-- 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 author.t/secondary_auth_session.t diff --git a/CHANGES.txt b/CHANGES.txt index d3e6a3b..f6b8e05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,9 @@ This file tracks the changes to Wing over time. Especially with respect to new features and compatibility changes. ========================================================== +2026-08-25 + * Bind secondary authentication to the current login session so verification cannot carry into a later login. + 2026-08-20 * Remove the unused "public_key" object parameter, and rename admin_key to write_key to be clear what field we're taking from the config file. diff --git a/author.t/secondary_auth_session.t b/author.t/secondary_auth_session.t new file mode 100644 index 0000000..885f3da --- /dev/null +++ b/author.t/secondary_auth_session.t @@ -0,0 +1,70 @@ +use strict; +use warnings; +use Test::More; +use lib 'author.t/lib', 'lib'; +use Wing::Role::Result::User; + +{ + package Local::SecondaryAuthCache; + + sub new { return bless { values => {} }, shift } + + sub get { + my ($self, $key) = @_; + return $self->{values}{$key}; + } + + sub set { + my ($self, $key, $value) = @_; + $self->{values}{$key} = $value; + return 1; + } +} + +{ + package Local::SecondaryAuthSession; + + sub new { return bless { id => $_[1] }, $_[0] } + sub id { return $_[0]->{id} } +} + +{ + package Local::SecondaryAuthUser; + + sub new { + my ($class, $session_id) = @_; + return bless { + id => 'user-1', + session => Local::SecondaryAuthSession->new($session_id), + }, $class; + } + + sub id { return $_[0]->{id} } + sub has_current_session { return 1 } + sub current_session { return $_[0]->{session} } + sub secondary_auth_cache_key { return Wing::Role::Result::User::secondary_auth_cache_key(@_) } + sub has_secondary_auth_token { return Wing::Role::Result::User::has_secondary_auth_token(@_) } + sub mark_secondary_auth_verified { return Wing::Role::Result::User::mark_secondary_auth_verified(@_) } + sub verify_secondary_auth { return Wing::Role::Result::User::verify_secondary_auth(@_) } +} + +my $cache = Local::SecondaryAuthCache->new; +my $first_session = Local::SecondaryAuthUser->new('session-1'); +my $second_session = Local::SecondaryAuthUser->new('session-2'); + +{ + no warnings qw(redefine once); + local *Wing::cache = sub { return $cache }; + + ok(!$first_session->has_secondary_auth_token, 'first session starts unverified'); + ok(!$second_session->has_secondary_auth_token, 'second session starts unverified'); + + ok($first_session->mark_secondary_auth_verified, 'marks the first session verified'); + ok($first_session->has_secondary_auth_token, 'first session is verified'); + ok(!$second_session->has_secondary_auth_token, 'verification does not carry into another login'); + + $cache->set($first_session->secondary_auth_cache_key('verify'), 'email-token'); + ok(!$second_session->verify_secondary_auth('email-token'), 'email token is limited to the requesting session'); +} + +done_testing; diff --git a/lib/Wing/Role/Result/User.pm b/lib/Wing/Role/Result/User.pm index 6a06105..88e3ae4 100644 --- a/lib/Wing/Role/Result/User.pm +++ b/lib/Wing/Role/Result/User.pm @@ -437,13 +437,23 @@ sub is_chat_staff { sub has_secondary_auth_token { my $self = shift; - return Wing->cache->get('2factor-verified-'.$self->id); + my $key = $self->secondary_auth_cache_key('verified'); + return 0 unless defined $key; + return Wing->cache->get($key); +} + +sub secondary_auth_cache_key { + my ($self, $state) = @_; + return undef unless $self->has_current_session; + return join('-', '2factor', $state, $self->id, $self->current_session->id); } sub email_secondary_auth_verification { my ($self, $redirect) = @_; my $verify = random_string('ssssssss'); - Wing->cache->set('2factor-verify-'.$self->id, $verify, 60 * 30); + my $key = $self->secondary_auth_cache_key('verify'); + ouch 428, 'We could not verify this login session. Log out, log back in, and try again.' unless defined $key; + Wing->cache->set($key, $verify, 60 * 30); eval { $self->send_templated_email('secondary_auth', { token => $verify, redirect => $redirect }); }; @@ -454,7 +464,9 @@ sub email_secondary_auth_verification { sub verify_secondary_auth { my ($self, $token) = @_; - if (defined $token && $token ne "" && $token eq Wing->cache->get('2factor-verify-'.$self->id)) { + my $key = $self->secondary_auth_cache_key('verify'); + my $expected = defined $key ? Wing->cache->get($key) : undef; + if (defined $expected && defined $token && $token ne "" && $token eq $expected) { return $self->mark_secondary_auth_verified; } return 0; @@ -462,7 +474,9 @@ sub verify_secondary_auth { sub mark_secondary_auth_verified { my $self = shift; - return Wing->cache->set('2factor-verified-'.$self->id, 1, 60 * 60 * 24); + my $key = $self->secondary_auth_cache_key('verified'); + return 0 unless defined $key; + return Wing->cache->set($key, 1, 60 * 60 * 24); } sub start_session {