Merge pull request #915 from grahamc/hydra-auth
Hydra auth: support Argon2, transparently upgrade hashes
This commit is contained in:
commit
fa924ea697
57
flake.nix
57
flake.nix
|
@ -70,6 +70,47 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
CryptArgon2 = final.perlPackages.buildPerlModule {
|
||||||
|
pname = "Crypt-Argon2";
|
||||||
|
version = "0.010";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Argon2-0.010.tar.gz";
|
||||||
|
sha256 = "3ea1c006f10ef66fd417e502a569df15c4cc1c776b084e35639751c41ce6671a";
|
||||||
|
};
|
||||||
|
nativeBuildInputs = [ pkgs.ld-is-cc-hook ];
|
||||||
|
meta = {
|
||||||
|
description = "Perl interface to the Argon2 key derivation functions";
|
||||||
|
license = final.lib.licenses.cc0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
CryptPassphrase = final.buildPerlPackage {
|
||||||
|
pname = "Crypt-Passphrase";
|
||||||
|
version = "0.003";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Passphrase-0.003.tar.gz";
|
||||||
|
sha256 = "685aa090f8179a86d6896212ccf8ccfde7a79cce857199bb14e2277a10d240ad";
|
||||||
|
};
|
||||||
|
meta = {
|
||||||
|
description = "A module for managing passwords in a cryptographically agile manner";
|
||||||
|
license = with final.lib.licenses; [ artistic1 gpl1Plus ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
CryptPassphraseArgon2 = final.buildPerlPackage {
|
||||||
|
pname = "Crypt-Passphrase-Argon2";
|
||||||
|
version = "0.002";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Passphrase-Argon2-0.002.tar.gz";
|
||||||
|
sha256 = "3906ff81697d13804ee21bd5ab78ffb1c4408b4822ce020e92ecf4737ba1f3a8";
|
||||||
|
};
|
||||||
|
propagatedBuildInputs = with final.perlPackages; [ CryptArgon2 CryptPassphrase ];
|
||||||
|
meta = {
|
||||||
|
description = "An Argon2 encoder for Crypt::Passphrase";
|
||||||
|
license = with final.lib.licenses; [ artistic1 gpl1Plus ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
DirSelf = final.buildPerlPackage {
|
DirSelf = final.buildPerlPackage {
|
||||||
pname = "Dir-Self";
|
pname = "Dir-Self";
|
||||||
version = "0.11";
|
version = "0.11";
|
||||||
|
@ -229,6 +270,19 @@
|
||||||
license = with final.stdenv.lib.licenses; [ artistic1 ];
|
license = with final.stdenv.lib.licenses; [ artistic1 ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StringCompareConstantTime = final.buildPerlPackage {
|
||||||
|
pname = "String-Compare-ConstantTime";
|
||||||
|
version = "0.321";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "mirror://cpan/authors/id/F/FR/FRACTAL/String-Compare-ConstantTime-0.321.tar.gz";
|
||||||
|
sha256 = "0b26ba2b121d8004425d4485d1d46f59001c83763aa26624dff6220d7735d7f7";
|
||||||
|
};
|
||||||
|
meta = {
|
||||||
|
description = "Timing side-channel protected string compare";
|
||||||
|
license = with final.lib.licenses; [ artistic1 gpl1Plus ];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
hydra = with final; let
|
hydra = with final; let
|
||||||
|
@ -254,6 +308,8 @@
|
||||||
CatalystViewTT
|
CatalystViewTT
|
||||||
CatalystXScriptServerStarman
|
CatalystXScriptServerStarman
|
||||||
CatalystXRoleApplicator
|
CatalystXRoleApplicator
|
||||||
|
CryptPassphrase
|
||||||
|
CryptPassphraseArgon2
|
||||||
CryptRandPasswd
|
CryptRandPasswd
|
||||||
DBDPg
|
DBDPg
|
||||||
DBDSQLite
|
DBDSQLite
|
||||||
|
@ -279,6 +335,7 @@
|
||||||
SQLSplitStatement
|
SQLSplitStatement
|
||||||
SetScalar
|
SetScalar
|
||||||
Starman
|
Starman
|
||||||
|
StringCompareConstantTime
|
||||||
SysHostnameLong
|
SysHostnameLong
|
||||||
TermSizeAny
|
TermSizeAny
|
||||||
TestMore
|
TestMore
|
||||||
|
|
|
@ -27,15 +27,14 @@ our $VERSION = '0.01';
|
||||||
__PACKAGE__->config(
|
__PACKAGE__->config(
|
||||||
name => 'Hydra',
|
name => 'Hydra',
|
||||||
default_view => "TT",
|
default_view => "TT",
|
||||||
authentication => {
|
'Plugin::Authentication' => {
|
||||||
default_realm => "dbic",
|
default_realm => "dbic",
|
||||||
realms => {
|
|
||||||
dbic => {
|
dbic => {
|
||||||
credential => {
|
credential => {
|
||||||
class => "Password",
|
class => "Password",
|
||||||
password_field => "password",
|
password_field => "password",
|
||||||
password_type => "hashed",
|
password_type => "self_check",
|
||||||
password_hash_type => "SHA-1",
|
|
||||||
},
|
},
|
||||||
store => {
|
store => {
|
||||||
class => "DBIx::Class",
|
class => "DBIx::Class",
|
||||||
|
@ -48,7 +47,6 @@ __PACKAGE__->config(
|
||||||
file($ENV{'HYDRA_LDAP_CONFIG'})
|
file($ENV{'HYDRA_LDAP_CONFIG'})
|
||||||
) : undef
|
) : undef
|
||||||
},
|
},
|
||||||
},
|
|
||||||
'Plugin::Static::Simple' => {
|
'Plugin::Static::Simple' => {
|
||||||
send_etag => 1,
|
send_etag => 1,
|
||||||
expires => 3600
|
expires => 3600
|
||||||
|
|
|
@ -6,7 +6,6 @@ use base 'Catalyst::Controller';
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
use Data::Dump qw(dump);
|
use Data::Dump qw(dump);
|
||||||
use Digest::SHA1 qw(sha1_hex);
|
|
||||||
use Config::General;
|
use Config::General;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ use base 'Hydra::Base::Controller::ListBuilds';
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
use Hydra::View::TT;
|
use Hydra::View::TT;
|
||||||
use Digest::SHA1 qw(sha1_hex);
|
|
||||||
use Nix::Store;
|
use Nix::Store;
|
||||||
use Nix::Config;
|
use Nix::Config;
|
||||||
use Encode;
|
use Encode;
|
||||||
|
|
|
@ -229,12 +229,6 @@ sub isValidPassword {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub setPassword {
|
|
||||||
my ($user, $password) = @_;
|
|
||||||
$user->update({ password => sha1_hex($password) });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub register :Local Args(0) {
|
sub register :Local Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
@ -294,7 +288,7 @@ sub updatePreferences {
|
||||||
error($c, "The passwords you specified did not match.")
|
error($c, "The passwords you specified did not match.")
|
||||||
if $password ne trim $c->stash->{params}->{password2};
|
if $password ne trim $c->stash->{params}->{password2};
|
||||||
|
|
||||||
setPassword($user, $password);
|
$user->setPassword($password);
|
||||||
}
|
}
|
||||||
|
|
||||||
my $emailAddress = trim($c->stash->{params}->{emailaddress} // "");
|
my $emailAddress = trim($c->stash->{params}->{emailaddress} // "");
|
||||||
|
@ -394,7 +388,7 @@ sub reset_password :Chained('user') :PathPart('reset-password') :Args(0) {
|
||||||
unless $user->emailaddress;
|
unless $user->emailaddress;
|
||||||
|
|
||||||
my $password = Crypt::RandPasswd->word(8,10);
|
my $password = Crypt::RandPasswd->word(8,10);
|
||||||
setPassword($user, $password);
|
$user->setPassword($password);
|
||||||
sendEmail(
|
sendEmail(
|
||||||
$c->config,
|
$c->config,
|
||||||
$user->emailaddress,
|
$user->emailaddress,
|
||||||
|
|
|
@ -195,6 +195,10 @@ __PACKAGE__->many_to_many("projects", "projectmembers", "project");
|
||||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-02-06 12:22:36
|
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-02-06 12:22:36
|
||||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4/WZ95asbnGmK+nEHb4sLQ
|
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4/WZ95asbnGmK+nEHb4sLQ
|
||||||
|
|
||||||
|
use Crypt::Passphrase;
|
||||||
|
use Digest::SHA1 qw(sha1_hex);
|
||||||
|
use String::Compare::ConstantTime;
|
||||||
|
|
||||||
my %hint = (
|
my %hint = (
|
||||||
columns => [
|
columns => [
|
||||||
"fullname",
|
"fullname",
|
||||||
|
@ -210,4 +214,42 @@ sub json_hint {
|
||||||
return \%hint;
|
return \%hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub _authenticator() {
|
||||||
|
my $authenticator = Crypt::Passphrase->new(
|
||||||
|
encoder => 'Argon2',
|
||||||
|
validators => [
|
||||||
|
(sub {
|
||||||
|
my ($password, $hash) = @_;
|
||||||
|
|
||||||
|
return String::Compare::ConstantTime::equals($hash, sha1_hex($password));
|
||||||
|
})
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub check_password {
|
||||||
|
my ($self, $password) = @_;
|
||||||
|
|
||||||
|
my $authenticator = _authenticator();
|
||||||
|
if ($authenticator->verify_password($password, $self->password)) {
|
||||||
|
if ($authenticator->needs_rehash($self->password)) {
|
||||||
|
$self->setPassword($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub setPassword {
|
||||||
|
my ($self, $password) = @_;;
|
||||||
|
|
||||||
|
$self->update({
|
||||||
|
"password" => _authenticator()->hash_password($password),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -5,17 +5,16 @@ use Hydra::Schema;
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Model::DB;
|
use Hydra::Model::DB;
|
||||||
use Getopt::Long qw(:config gnu_getopt);
|
use Getopt::Long qw(:config gnu_getopt);
|
||||||
use Digest::SHA1 qw(sha1_hex);
|
|
||||||
|
|
||||||
sub showHelp {
|
sub showHelp {
|
||||||
print <<EOF;
|
print q%
|
||||||
Usage: $0 NAME
|
Usage: hydra-create-user NAME
|
||||||
[--rename-from NAME]
|
[--rename-from NAME]
|
||||||
[--type hydra|google|github]
|
[--type hydra|google|github]
|
||||||
[--full-name FULLNAME]
|
[--full-name FULLNAME]
|
||||||
[--email-address EMAIL-ADDRESS]
|
[--email-address EMAIL-ADDRESS]
|
||||||
[--password PASSWORD]
|
[--password PASSWORD]
|
||||||
[--password-hash SHA1-HASH]
|
[--password-hash HASH]
|
||||||
[--wipe-roles]
|
[--wipe-roles]
|
||||||
[--role ROLE]...
|
[--role ROLE]...
|
||||||
|
|
||||||
|
@ -25,9 +24,31 @@ exists, roles are added to the existing roles unless --wipe-roles is
|
||||||
specified. If --rename-from is given, the specified account is
|
specified. If --rename-from is given, the specified account is
|
||||||
renamed.
|
renamed.
|
||||||
|
|
||||||
Example:
|
* PASSWORD HASH
|
||||||
\$ hydra-create-user alice --password foobar --role admin
|
The password hash should be an Argon2id hash, which can be generated
|
||||||
EOF
|
via:
|
||||||
|
|
||||||
|
$ nix-shell -p libargon2
|
||||||
|
[nix-shell]$ argon2 "$(LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | head -c16)" -id -t 3 -k 262144 -p 1 -l 16 -e
|
||||||
|
foobar
|
||||||
|
Ctrl^D
|
||||||
|
$argon2id$v=19$m=262144,t=3,p=1$NFU1QXJRNnc4V1BhQ0NJQg$6GHqjqv5cNDDwZqrqUD0zQ
|
||||||
|
|
||||||
|
SHA1 is also accepted, but SHA1 support is deprecated and the user's
|
||||||
|
password will be upgraded to Argon2id on first login.
|
||||||
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Create a user with an argon2 password:
|
||||||
|
|
||||||
|
$ hydra-create-user alice --password-hash '$argon2id$v=19$m=262144,t=3,p=1$NFU1QXJRNnc4V1BhQ0NJQg$6GHqjqv5cNDDwZqrqUD0zQ' --role admin
|
||||||
|
|
||||||
|
Create a user with a password insecurely provided on the commandline:
|
||||||
|
|
||||||
|
$ hydra-create-user alice --password foobar --role admin
|
||||||
|
|
||||||
|
%;
|
||||||
exit 0;
|
exit 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,8 +105,9 @@ $db->txn_do(sub {
|
||||||
$user->update({ emailaddress => $userName, password => "!" });
|
$user->update({ emailaddress => $userName, password => "!" });
|
||||||
} else {
|
} else {
|
||||||
$user->update({ emailaddress => $emailAddress }) if defined $emailAddress;
|
$user->update({ emailaddress => $emailAddress }) if defined $emailAddress;
|
||||||
|
|
||||||
if (defined $password && !(defined $passwordHash)) {
|
if (defined $password && !(defined $passwordHash)) {
|
||||||
$passwordHash = sha1_hex($password);
|
$user->setPassword($password);
|
||||||
}
|
}
|
||||||
$user->update({ password => $passwordHash }) if defined $passwordHash;
|
$user->update({ password => $passwordHash }) if defined $passwordHash;
|
||||||
}
|
}
|
||||||
|
|
42
t/Schema/Users.t
Normal file
42
t/Schema/Users.t
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use strict;
|
||||||
|
use Setup;
|
||||||
|
|
||||||
|
my %ctx = test_init();
|
||||||
|
|
||||||
|
require Hydra::Schema;
|
||||||
|
require Hydra::Model::DB;
|
||||||
|
|
||||||
|
use Test2::V0;
|
||||||
|
|
||||||
|
my $db = Hydra::Model::DB->new;
|
||||||
|
hydra_setup($db);
|
||||||
|
|
||||||
|
# Hydra used to store passwords, by default, as plain unsalted sha1 hashes.
|
||||||
|
# We now upgrade these badly stored passwords with much stronger algorithms
|
||||||
|
# when the user logs in. Implementing this meant reimplementing our password
|
||||||
|
# checking ourselves, so also ensure that basic password checking works.
|
||||||
|
#
|
||||||
|
# This test:
|
||||||
|
#
|
||||||
|
# 1. creates a user with the legacy password
|
||||||
|
# 2. validates that the wrong password is not considered valid
|
||||||
|
# 3. validates that the correct password is valid
|
||||||
|
# 4. checks that the checking of the correct password transparently upgraded
|
||||||
|
# the password's storage to a more secure algorithm.
|
||||||
|
|
||||||
|
# Starting the user with an unsalted sha1 password
|
||||||
|
my $user = $db->resultset('Users')->create({
|
||||||
|
"username" => "alice",
|
||||||
|
"emailaddress" => 'alice@nixos.org',
|
||||||
|
"password" => "8843d7f92416211de9ebb963ff4ce28125932878" # SHA1 of "foobar"
|
||||||
|
});
|
||||||
|
isnt($user, undef, "My user was created.");
|
||||||
|
|
||||||
|
ok(!$user->check_password("barbaz"), "Checking the password, barbaz, is not right");
|
||||||
|
|
||||||
|
is($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The unsalted sha1 is in the database.");
|
||||||
|
ok($user->check_password("foobar"), "Checking the password, foobar, is right");
|
||||||
|
isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The user has had their password rehashed.");
|
||||||
|
ok($user->check_password("foobar"), "Checking the password, foobar, is still right");
|
||||||
|
|
||||||
|
done_testing;
|
Loading…
Reference in a new issue