Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / Password_Mixin.pm
1 package FS::Password_Mixin;
2
3 use FS::Record qw(qsearch);
4 use FS::Conf;
5 use FS::password_history;
6 use Authen::Passphrase;
7 use Authen::Passphrase::BlowfishCrypt;
8 # https://rt.cpan.org/Ticket/Display.html?id=72743
9
10 our $DEBUG = 1;
11 our $conf;
12 FS::UID->install_callback( sub {
13     $conf = FS::Conf->new;
14     # this is safe
15     #eval "use Authen::Passphrase::BlowfishCrypt;";
16 });
17
18 our $me = '[' . __PACKAGE__ . ']';
19
20 our $BLOWFISH_COST = 10;
21
22 =head1 NAME
23
24 FS::Password_Mixin - Object methods for accounts that have passwords governed
25 by the password policy.
26
27 =head1 METHODS
28
29 =over 4
30
31 =item is_password_allowed PASSWORD
32
33 Checks the password against the system password policy. Returns an error
34 message on failure, an empty string on success.
35
36 This MUST NOT be called from check(). It should be called by the office UI,
37 self-service ClientAPI, or other I<user-interactive> code that processes a
38 password change, and only if the user has taken some action with the intent
39 of changing the password.
40
41 =cut
42
43 sub is_password_allowed {
44   my $self = shift;
45   my $password = shift;
46
47   # check length and complexity here
48
49   if ( $conf->config('password-no_reuse') =~ /^(\d+)$/ ) {
50
51     my $no_reuse = $1;
52
53     # "the last N" passwords includes the current password and the N-1
54     # passwords before that.
55     warn "$me checking password reuse limit of $no_reuse\n" if $DEBUG;
56     my @latest = qsearch({
57         'table'     => 'password_history',
58         'hashref'   => { $self->password_history_key => $self->get($self->primary_key) },
59         'order_by'  => " ORDER BY created DESC LIMIT $no_reuse",
60     });
61
62     # don't check the first one; reusing the current password is allowed.
63     shift @latest;
64
65     foreach my $history (@latest) {
66       warn "$me previous password created ".$history->created."\n" if $DEBUG;
67       if ( $history->password_equals($password) ) {
68         my $message;
69         if ( $no_reuse == 1 ) {
70           $message = "This password is the same as your previous password.";
71         } else {
72           $message = "This password was one of the last $no_reuse passwords on this account.";
73         }
74         return $message;
75       }
76     } #foreach $history
77
78   } # end of no_reuse checking
79
80   '';
81 }
82
83 =item password_history_key
84
85 Returns the name of the field in L<FS::password_history> that's the foreign
86 key to this table.
87
88 =cut
89
90 sub password_history_key {
91   my $self = shift;
92   $self->table . '__' . $self->primary_key;
93 }
94
95 =item insert_password_history
96
97 Creates a L<FS::password_history> record linked to this object, with its
98 current password.
99
100 =cut
101
102 sub insert_password_history {
103   my $self = shift;
104   my $encoding = $self->_password_encoding;
105   my $password = $self->_password;
106   my $auth;
107
108   if ( $encoding eq 'bcrypt' ) {
109     # our format, used for contact and access_user passwords
110     my ($cost, $salt, $hash) = split(',', $password);
111     $auth = Authen::Passphrase::BlowfishCrypt->new(
112       cost        => $cost,
113       salt_base64 => $salt,
114       hash_base64 => $hash,
115     );
116
117   } elsif ( $encoding eq 'crypt' ) {
118
119     # it's smart enough to figure this out
120     $auth = Authen::Passphrase->from_crypt($password);
121
122   } elsif ( $encoding eq 'ldap' ) {
123
124     $password =~ s/^{PLAIN}/{CLEARTEXT}/i; # normalize
125     $auth = Authen::Passphrase->from_rfc2307($password);
126     if ( $auth->isa('Authen::Passphrase::Clear') ) {
127       # then we've been given the password in cleartext
128       $auth = $self->_blowfishcrypt( $auth->passphrase );
129     }
130   
131   } elsif ( $encoding eq 'plain' ) {
132
133     $auth = $self->_blowfishcrypt( $password );
134
135   }
136
137   my $password_history = FS::password_history->new({
138       _password => $auth->as_rfc2307,
139       created   => time,
140       $self->password_history_key => $self->get($self->primary_key),
141   });
142
143   my $error = $password_history->insert;
144   return "recording password history: $error" if $error;
145   '';
146
147 }
148
149 =item _blowfishcrypt PASSWORD
150
151 For internal use: takes PASSWORD and returns a new
152 L<Authen::Passphrase::BlowfishCrypt> object representing it.
153
154 =cut
155
156 sub _blowfishcrypt {
157   my $class = shift;
158   my $passphrase = shift;
159   return Authen::Passphrase::BlowfishCrypt->new(
160     cost => $BLOWFISH_COST,
161     salt_random => 1,
162     passphrase => $passphrase,
163   );
164 }
165
166 =back
167
168 =head1 SEE ALSO
169
170 L<FS::password_history>
171
172 =cut
173
174 1;