View | Details | Raw Unified | Return to bug 40824
Collapse All | Expand All

(-)a/Koha/PatronPasswordHistories.pm (+177 lines)
Line 0 Link Here
1
package Koha::PatronPasswordHistories;
2
3
# Copyright 2023
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::PatronPasswordHistory;
24
use C4::Context;
25
use Koha::AuthUtils;
26
27
use base qw(Koha::Objects);
28
29
=head1 NAME
30
31
Koha::PatronPasswordHistories - Koha PatronPasswordHistory Object set class
32
33
=head1 API
34
35
=head2 Class Methods
36
37
=head3 cleanup_old_password_history
38
39
    Koha::PatronPasswordHistories->cleanup_old_password_history($borrowernumber, $count);
40
41
Clean up old password history for a patron, keeping only the specified number of most recent entries.
42
If count is 0 or negative, all password history for the patron will be deleted.
43
44
=cut
45
46
sub cleanup_old_password_history {
47
    my ($class, $borrowernumber, $count) = @_;
48
    
49
    return unless $borrowernumber;
50
    
51
    # If count is 0 or negative, delete all entries
52
    if ($count <= 0) {
53
        my $deleted = $class->search({
54
            borrowernumber => $borrowernumber
55
        })->delete;
56
        return $deleted;
57
    }
58
    
59
    # Get IDs of entries to keep (the most recent ones)
60
    my @keep_ids = $class->search(
61
        { borrowernumber => $borrowernumber },
62
        { 
63
            order_by => { -desc => 'created_on' },
64
            rows => $count,
65
            columns => ['id']
66
        }
67
    )->get_column('id');
68
    
69
    # Delete all entries for this borrower except the ones we're keeping
70
    my $deleted = 0;
71
    if (@keep_ids) {
72
        $deleted = $class->search({
73
            borrowernumber => $borrowernumber,
74
            id => { -not_in => \@keep_ids }
75
        })->delete;
76
    }
77
    
78
    return $deleted;
79
}
80
81
=head3 _type
82
83
Return the DBIC resultset type for this class
84
85
=cut
86
87
sub _type {
88
    # The table name in DB is borrower_password_history
89
    return 'BorrowerPasswordHistory';
90
}
91
92
=head3 object_class
93
94
Return the object class for items in this collection
95
96
=cut
97
98
sub object_class {
99
    return 'Koha::PatronPasswordHistory';
100
}
101
102
=head3 has_used_password
103
104
    my $bool = Koha::PatronPasswordHistories->has_used_password({
105
        borrowernumber => $borrowernumber,
106
        password => $plain_text_password,
107
        current_password => $hashed_current_password # Optional
108
    });
109
110
Check if the given password exists in the password history for this patron.
111
If current_password is provided, it will also check against that.
112
Returns true if the password has been used before, false otherwise.
113
114
=cut
115
116
sub has_used_password {
117
    my ($class, $params) = @_;
118
    
119
    my $borrowernumber = $params->{borrowernumber};
120
    my $password = $params->{password};
121
    my $current_password = $params->{current_password};
122
    
123
    unless ($borrowernumber && $password) {
124
        return 0;
125
    }
126
    
127
    my $history_count = C4::Context->preference('PasswordHistoryCount') || 0;
128
    
129
    # If history count is 0, don't check history at all
130
    if ($history_count == 0) {
131
        return 0;
132
    }
133
    
134
    # If history count is 1, only check current password
135
    if ($history_count == 1) {
136
        my $matches_current = $current_password && C4::Auth::checkpw_hash($password, $current_password);
137
        return $matches_current ? 1 : 0;
138
    }
139
    
140
    # First check if password matches current one, if provided
141
    if ($current_password && C4::Auth::checkpw_hash($password, $current_password)) {
142
        return 1; # Current password counts as a used password
143
    }
144
    
145
    # Adjust history count to account for current password
146
    my $history_entries_to_check = $current_password ? $history_count - 1 : $history_count;
147
    
148
    return 0 if $history_entries_to_check <= 0; # No need to check history if only checking current
149
    
150
    # Get the most recent password history entries
151
    my $password_history = $class->search(
152
        { borrowernumber => $borrowernumber },
153
        { 
154
            order_by => { -desc => 'created_on' },
155
            rows => $history_entries_to_check
156
        }
157
    );
158
    
159
    # Check each password history entry
160
    while (my $history_entry = $password_history->next) {
161
        # Use checkpw_hash to properly compare the plaintext password with stored hash
162
        my $matches = C4::Auth::checkpw_hash($password, $history_entry->password);
163
        if ($matches) {
164
            return 1; # Password found in history
165
        }
166
    }
167
    
168
    return 0; # Password not found in history
169
}
170
171
=head1 AUTHOR
172
173
Koha Development Team <http://koha-community.org/>
174
175
=cut
176
177
1; 
(-)a/Koha/PatronPasswordHistory.pm (-1 / +115 lines)
Line 0 Link Here
0
- 
1
package Koha::PatronPasswordHistory;
2
3
# Copyright 2023
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::AuthUtils;
24
use C4::Context;
25
use C4::Auth;
26
use Koha::Patrons;
27
28
use base qw(Koha::Object);
29
30
=head1 NAME
31
32
Koha::PatronPasswordHistory - Koha PatronPasswordHistory Object class
33
34
=head1 API
35
36
=head2 Class methods
37
38
=head3 _type
39
40
Return the DBIC resultset type for this object
41
42
=cut
43
44
sub _type {
45
    # The table name in DB is borrower_password_history
46
    return 'BorrowerPasswordHistory';
47
}
48
49
=head3 koha_objects_class
50
51
Define relationship with the Koha::PatronPasswordHistories class
52
53
=cut
54
55
sub koha_objects_class {
56
    return 'Koha::PatronPasswordHistories';
57
}
58
59
=head3 store
60
61
Overridden store method to ensure the password is hashed before storing.
62
Also handles cleanup of old password history entries.
63
64
=cut
65
66
sub store {
67
    my ($self) = @_;
68
    
69
    my $borrowernumber = $self->borrowernumber;
70
    my $password = $self->password;
71
    
72
    unless ($password) {
73
        return;
74
    }
75
    
76
    # Note: Don't use string comparison for password hashes - this is a common pattern
77
    # where we're intentionally storing the old password in history
78
    # The old check would fail when the patron's current hashed password was exactly
79
    # what we were trying to store (which is the expected case when changing passwords)
80
    
81
    # Hash the password if it hasn't been hashed already
82
    unless ( $self->in_storage ) {
83
        if ( $password && $password !~ /\$2a\$/ ) {
84
            $self->password( Koha::AuthUtils::hash_password($password) );
85
        }
86
    }
87
    
88
    # Store in database
89
    $self = $self->SUPER::store();
90
    
91
    # Get the history count preference
92
    my $history_count = C4::Context->preference('PasswordHistoryCount') || 0;
93
    
94
    # Clean up old history entries
95
    # Adjust history count to account for current password
96
    my $history_entries_to_keep = $history_count <= 1 
97
        ? $history_count  # If 0 or 1, keep that many
98
        : $history_count - 1;  # Otherwise keep historical entries minus current
99
    
100
    # Clean up old entries
101
    Koha::PatronPasswordHistories->cleanup_old_password_history(
102
        $borrowernumber,
103
        $history_entries_to_keep
104
    );
105
    
106
    return $self;
107
}
108
109
=head1 AUTHOR
110
111
Koha Development Team <http://koha-community.org/>
112
113
=cut
114
115
1; 

Return to bug 40824