summaryrefslogtreecommitdiff
path: root/FS/FS/Password_Mixin.pm
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2015-11-12 16:49:39 -0800
committerMark Wells <mark@freeside.biz>2015-11-13 13:00:42 -0500
commitd3f33fa4dbabb61cd94dac9f4f63cd8f249313da (patch)
treef9559ddf824ab90fcc68e1854e1219caeef5e907 /FS/FS/Password_Mixin.pm
parent75a3ac488dc908290e75edd09471e01dba2199cb (diff)
limit password reuse, core and svc_acct, #29354
Diffstat (limited to 'FS/FS/Password_Mixin.pm')
-rw-r--r--FS/FS/Password_Mixin.pm165
1 files changed, 165 insertions, 0 deletions
diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm
new file mode 100644
index 0000000..af4c5e2
--- /dev/null
+++ b/FS/FS/Password_Mixin.pm
@@ -0,0 +1,165 @@
+package FS::Password_Mixin;
+
+use FS::Record qw(qsearch);
+use FS::Conf;
+use FS::password_history;
+use Authen::Passphrase;
+# use Authen::Passphrase::BlowfishCrypt; # ha ha, no.
+# https://rt.cpan.org/Ticket/Display.html?id=72743
+
+our $DEBUG = 1;
+our $conf;
+FS::UID->install_callback( sub {
+ $conf = FS::Conf->new;
+ # this is safe
+ eval "use Authen::Passphrase::BlowfishCrypt;";
+});
+
+our $me = '[' . __PACKAGE__ . ']';
+
+our $BLOWFISH_COST = 10;
+
+=head1 NAME
+
+FS::Password_Mixin - Object methods for accounts that have passwords governed
+by the password policy.
+
+=head1 METHODS
+
+=over 4
+
+=item is_password_allowed PASSWORD
+
+Checks the password against the system password policy. Returns an error
+message on failure, an empty string on success.
+
+This MUST NOT be called from check(). It should be called by the office UI,
+self-service ClientAPI, or other I<user-interactive> code that processes a
+password change, and only if the user has taken some action with the intent
+of changing the password.
+
+=cut
+
+sub is_password_allowed {
+ my $self = shift;
+ my $password = shift;
+
+ # check length and complexity here
+
+ if ( $conf->config('password-no_reuse') =~ /^(\d+)$/ ) {
+
+ my $no_reuse = $1;
+
+ # "the last N" passwords includes the current password and the N-1
+ # passwords before that.
+ warn "$me checking password reuse limit of $no_reuse\n" if $DEBUG;
+ my @latest = qsearch({
+ 'table' => 'password_history',
+ 'hashref' => { $self->password_history_key => $self->get($self->primary_key) },
+ 'order_by' => " ORDER BY created DESC LIMIT $no_reuse",
+ });
+
+ # don't check the first one; reusing the current password is allowed.
+ shift @latest;
+
+ foreach my $history (@latest) {
+ warn "$me previous password created ".$history->created."\n" if $DEBUG;
+ if ( $history->password_equals($password) ) {
+ my $message;
+ if ( $no_reuse == 1 ) {
+ $message = "This password is the same as your previous password.";
+ } else {
+ $message = "This password was one of the last $no_reuse passwords on this account.";
+ }
+ return $message;
+ }
+ } #foreach $history
+
+ } # end of no_reuse checking
+
+ '';
+}
+
+=item password_history_key
+
+Returns the name of the field in L<FS::password_history> that's the foreign
+key to this table.
+
+=cut
+
+sub password_history_key {
+ my $self = shift;
+ $self->table . '__' . $self->primary_key;
+}
+
+=item insert_password_history
+
+Creates a L<FS::password_history> record linked to this object, with its
+current password.
+
+=cut
+
+sub insert_password_history {
+ my $self = shift;
+ my $encoding = $self->_password_encoding;
+ my $password = $self->_password;
+ my $auth;
+
+ if ( $encoding eq 'bcrypt' or $encoding eq 'crypt' ) {
+
+ # it's smart enough to figure this out
+ $auth = Authen::Passphrase->from_crypt($password);
+
+ } elsif ( $encoding eq 'ldap' ) {
+
+ $password =~ s/^{PLAIN}/{CLEARTEXT}/i; # normalize
+ $auth = Authen::Passphrase->from_rfc2307($password);
+ if ( $auth->isa('Authen::Passphrase::Clear') ) {
+ # then we've been given the password in cleartext
+ $auth = $self->_blowfishcrypt( $auth->passphrase );
+ }
+
+ } elsif ( $encoding eq 'plain' ) {
+
+ $auth = $self->_blowfishcrypt( $password );
+
+ }
+
+ my $password_history = FS::password_history->new({
+ _password => $auth->as_rfc2307,
+ created => time,
+ $self->password_history_key => $self->get($self->primary_key),
+ });
+
+ my $error = $password_history->insert;
+ return "recording password history: $error" if $error;
+ '';
+
+}
+
+=item _blowfishcrypt PASSWORD
+
+For internal use: takes PASSWORD and returns a new
+L<Authen::Passphrase::BlowfishCrypt> object representing it.
+
+=cut
+
+sub _blowfishcrypt {
+ my $class = shift;
+ my $passphrase = shift;
+ return Authen::Passphrase::BlowfishCrypt->new(
+ cost => $BLOWFISH_COST,
+ salt_random => 1,
+ passphrase => $passphrase,
+ );
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::password_history>
+
+=cut
+
+1;