Merge branch 'v2.0' into 2683

This commit is contained in:
Christophe Maudoux 2022-01-22 21:18:23 +01:00
commit 3ffb7aa607
48 changed files with 1412 additions and 115 deletions

View File

@ -645,6 +645,7 @@ install_bin: install_conf_dir
${SRCPORTALDIR}/scripts/llngDeleteSession \
${SRCCOMMONDIR}/scripts/convertConfig \
${SRCCOMMONDIR}/scripts/convertSessions \
${SRCCOMMONDIR}/scripts/encryptTotpSecrets \
${SRCCOMMONDIR}/scripts/lmMigrateConfFiles2ini \
${SRCCOMMONDIR}/scripts/rotateOidcKeys \
${SRCMANAGERDIR}/scripts/lmConfigEditor \

View File

@ -2,6 +2,7 @@ etc/lemonldap-ng/lemonldap-ng.ini
etc/lemonldap-ng/for_etc_hosts
usr/share/man/man1/convertConfig.1p
usr/share/man/man1/convertSessions.1p
usr/share/man/man1/encryptTotpSecrets.1p
usr/share/man/man1/importMetadata.1p
usr/share/man/man1/lemonldap-ng-cli.1p
usr/share/man/man1/lemonldap-ng-sessions.1p
@ -11,6 +12,7 @@ usr/share/perl5/Lemonldap/NG/Common*
usr/share/lemonldap-ng/ressources
usr/share/lemonldap-ng/bin/convertConfig
usr/share/lemonldap-ng/bin/convertSessions
usr/share/lemonldap-ng/bin/encryptTotpSecrets
usr/share/lemonldap-ng/bin/importMetadata
usr/share/lemonldap-ng/bin/lemonldap-ng-sessions
usr/share/lemonldap-ng/bin/lmMigrateConfFiles2ini

View File

@ -30,6 +30,7 @@ theses :
* **OAUTH2_USERNAME_MAP**: ``sub``
* **OAUTH2_FULLNAME_MAP**: ``name``
* **OAUTH2_EMAIL_MAP**: ``email``
* **OAUTH2_REQUEST_PERMISSIONS**: ``openid profile email``
.. danger::

View File

@ -38,12 +38,8 @@ LL::NG can use two tables:
Authentication table and user table can be the same.
The password can be in plain text, or encoded with a standard SQL
method:
- SHA
- SHA1
- MD5
The password can be in plain text, or encoded with a SQL method (for example
``SHA``, ``SHA1``, ``MD5`` or any method valid on database side).
Example 1: two tables
^^^^^^^^^^^^^^^^^^^^^
@ -159,7 +155,8 @@ Password
~~~~~~~~
- **Hash schema**: SQL method for hashing password. Can be left blank
for plain text passwords.
for plain text passwords. The method will be forced to uppercase in
SQL statement.
- **Dynamic hash activation**: Activate dynamic hashing. With dynamic
hashing, the hash scheme is recovered from the user password in the
database during authentication.

View File

@ -60,6 +60,12 @@ In the manager (advanced parameters), you just have to enable it:
- **Lifetime** (Optional): Unlimited by default. Set a Time To Live in seconds.
TTL is checked at each login process if set. If TTL is expired,
relative TOTP is removed.
- **Encrypt TOTP secrets**: By default, the TOTP secret key is stored in the
persistent session database in cleartext. Set this option to encrypt all
newly-generated secrets. More details :ref:`below<totp-encryption>`
- **Logo** (Optional): logo file *(in static/<skin> directory)*
- **Label** (Optional): label that should be displayed to the user on
the choice screen
.. attention::
@ -93,6 +99,45 @@ module.// // To enable manager Second Factor Administration Module, set
[portal]
enabledModules = conf, sessions, notifications, 2ndFA
.. _totp-encryption:
Encryption of TOTP secrets
--------------------------
During registration of a TOTP device, a secret key is exchanged between the
mobile device and the server, generally through the use of a QR-Code. Once the
exchange is done, secret keys must never leave the device or the server.
Administrators may want to protect TOTP secrets by encrypting them in the
persistent session database, in order to prevent them from leaking through
backups or unauthorized database access.
Setting the *Encrypt TOTP secrets* option will automatically encrypt newly
generated secrets.
The *Encrypt TOTP secrets* options only affects *NEW* secrets, meaning that:
* A cleartext TOTP secret will be accepted even if the option is on
* An already encrypted TOTP secret will be accepted even if the option if off
The ``encryptTotpSecrets`` script can be used to encrypt previously registered TOTP
secrets so that they can be protected as well.
Encryption key
~~~~~~~~~~~~~~
By default, the key used for encryption is the global one, set in
*General Parameters* » *Advanced Parameters* » *Security* » *Key*
However, if you store your configuration and persistent sessions in the same
database, this defeats the point of encryption entirely.
It is recommended to set the TOTP encryption key in ``/etc/lemonldap-ng/lemonldap-ng.ini`` instead::
[all]
totp2fKey=changeme
Developer corner
----------------

View File

@ -153,6 +153,16 @@ If you defined the "Register page URL" or the password "Reset page URL" to an ex
</a>
Changes impacting plugin developpers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* If you are using Custom authentication modules, userDB modules or password
modules, ``$portal->loadedPlugins`` no longer contains a key with the name of
your module. You should use ``$portal->_authentication``, ``$portal->_userDB``,
or ``$portal->_passwordDB`` instead to get your module instance.
2.0.13
------

View File

@ -76,6 +76,7 @@ META.yml
README
scripts/convertConfig
scripts/convertSessions
scripts/encryptTotpSecrets
scripts/importMetadata
scripts/lemonldap-ng-cli
scripts/lemonldap-ng-sessions
@ -89,6 +90,7 @@ t/05-Common-Conf-LDAP.t
t/30-Common-Safelib.t
t/35-Common-Crypto.t
t/36-Common-Regexp.t
t/37-Common-TOTP.pm
t/40-Common-Session.t
t/50-Combination-Parser.t
t/60-Session-Cli.t

View File

@ -104,6 +104,7 @@ WriteMakefile(
MAN1PODS => {
'scripts/convertConfig' => 'blib/man1/convertConfig.1p',
'scripts/convertSessions' => 'blib/man1/convertSessions.1p',
'scripts/encryptTotpSecrets' => 'blib/man1/encryptTotpSecrets.1p',
'scripts/lemonldap-ng-cli' => 'blib/man1/lemonldap-ng-cli.1p',
'scripts/lemonldap-ng-sessions' => 'blib/man1/lemonldap-ng-sessions.1p',
'scripts/importMetadata' => 'blib/man1/importMetadata.1p',

View File

@ -3,7 +3,7 @@ package Lemonldap::NG::Common::Conf::Backends::Local;
use strict;
use Lemonldap::NG::Common::Conf::Constants;
our $VERSION = '2.0.0';
our $VERSION = '2.0.14';
sub prereq {
return 1;
@ -26,21 +26,22 @@ sub unlock {
}
sub store {
$Lemonldap::NG::Common::Conf::msg = 'Read-only backend !';
$Lemonldap::NG::Common::Conf::msg = 'Read-only backend!';
return DATABASE_LOCKED;
}
sub load {
return {
cfgNum => 1,
cfgDate => time,
cfgAuthor => 'LLNG Team',
cfgLog =>
q"Don't edit this configuration, Null backend uses only lemonldap-ng.ini values",
q"Do not edit this configuration, Null backend uses lemonldap-ng.ini values only",
};
}
sub delete {
$Lemonldap::NG::Common::Conf::msg = 'Read-only backend !';
$Lemonldap::NG::Common::Conf::msg = 'Read-only backend!';
return 0;
}

View File

@ -31,7 +31,7 @@ use constant DEFAULTCONFBACKENDOPTIONS => (
);
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|webID)ExportedVa|exported(?:Heade|Va)|issuerDBGetParamete)r|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar|ScopeRule|Macro)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node))|penIdExportedVars)|c(?:as(?:A(?:ppMetaData(?:(?:ExportedVar|Option|Macro)s|Node)|ttributes)|S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions))|(?:ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)s)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option|Macro)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars|fExtra)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
our $arrayParameters = qr/^mySessionAuthorizedRWKeys$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|t(?:ayConnectedBypassFG|orePassword)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|heck(?:DevOps(?:D(?:isplayNormalizedHeaders|ownload)|CheckSessionAttributes)?|State|User|XSS)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|rowdsec|da)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxy(?:AuthServiceImpersonation|UseSoap))|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|n(?:o(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|ewLocationWarning)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|to(?:tp2fUserCanRemoveKey|kenUseGlobalStorage)|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|(?:wsdlServ|findUs)er)$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|t(?:ayConnectedBypassFG|orePassword)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|heck(?:DevOps(?:D(?:isplayNormalizedHeaders|ownload)|CheckSessionAttributes)?|State|User|XSS)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|rowdsec|da)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxy(?:AuthServiceImpersonation|UseSoap))|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|n(?:o(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|ewLocationWarning)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|to(?:tp2f(?:UserCanRemoveKey|EncryptSecret)|kenUseGlobalStorage)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|(?:wsdlServ|findUs)er)$/;
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );

View File

@ -8,12 +8,80 @@ use Mouse;
use Convert::Base32 qw(decode_base32 encode_base32);
use Crypt::URandom;
use Digest::HMAC_SHA1 'hmac_sha1_hex';
use Lemonldap::NG::Common::Crypto;
our $VERSION = '2.0.10';
has 'key' => (
is => 'ro',
lazy => 1,
default => sub {
my ($self) = @_;
return $self->conf->{totp2fKey} || $self->conf->{key};
}
);
has encryptSecret => (
is => 'ro',
lazy => 1,
default => sub {
my ($self) = @_;
return $self->conf->{totp2fEncryptSecret};
}
);
has 'crypto' => (
is => 'ro',
lazy => 1,
default => sub {
my ($self) = @_;
Lemonldap::NG::Common::Crypto->new( $self->key );
}
);
use constant PREFIX => "{llngcrypt}";
sub is_encrypted {
my ( $self, $secret ) = @_;
return ( substr( $secret, 0, length(PREFIX) ) eq PREFIX );
}
sub get_ciphertext {
my ( $self, $secret ) = @_;
return substr( $secret, length(PREFIX) );
}
# This returns the TOTP secret from its stored form
sub get_cleartext_secret {
my ( $self, $secret ) = @_;
my $cleartext_secret = $secret;
if ( $self->is_encrypted($secret) ) {
$cleartext_secret =
$self->crypto->decrypt( $self->get_ciphertext($secret) );
}
return $cleartext_secret;
}
# This returns the cleartext or encrypted code for storage
sub get_storable_secret {
my ( $self, $secret ) = @_;
my $storable_secret = $secret;
if ( $self->encryptSecret ) {
$storable_secret = PREFIX . $self->crypto->encrypt($secret);
}
return $storable_secret;
}
# Verify that TOTP $code matches with $secret
sub verifyCode {
my ( $self, $interval, $range, $digits, $secret, $code ) = @_;
my ( $self, $interval, $range, $digits, $stored_secret, $code ) = @_;
my $secret = $self->get_cleartext_secret($stored_secret);
if ( !$secret ) {
$self->logger->error('Unable to decrypt TOTP secret');
return -1;
}
my $s = eval { decode_base32($secret) };
if ($@) {
$self->logger->error('Bad characters in TOTP secret');

View File

@ -0,0 +1,314 @@
#!/usr/bin/perl
#
use warnings;
use strict;
use POSIX;
use Lemonldap::NG::Common::Conf;
use Lemonldap::NG::Common::Apache::Session;
use Lemonldap::NG::Common::Session;
use Lemonldap::NG::Common::TOTP;
use JSON qw(from_json to_json);
use Getopt::Long qw(:config auto_help);
use Pod::Usage;
my ( $dryrun, $verbose, $force, $update, $oldkey, $newkey );
GetOptions(
"n|dry-run" => \$dryrun,
"v|verbose" => \$verbose,
"f|force" => \$force,
"u|update" => \$update,
"o|old-key=s" => \$oldkey,
"k|new-key=s" => \$newkey,
);
eval {
POSIX::setgid( scalar( getgrnam('www-data') ) );
POSIX::setuid( scalar( getpwnam('www-data') ) );
};
sub verbose {
print STDERR @_, "\n" if $verbose;
}
sub info {
print STDERR @_, "\n";
}
# Get config
my $res = Lemonldap::NG::Common::Conf->new();
die $Lemonldap::NG::Common::Conf::msg unless ($res);
my $conf = $res->getConf();
my $localconf = $res->getLocalConf()
or die "Unable to get local configuration ($!)";
if ($localconf) {
$conf->{$_} = $localconf->{$_} foreach ( keys %$localconf );
}
if ( !$conf->{totp2fEncryptSecret} ) {
if ( !$force ) {
die "Encryption of TOTP secrets is not enabled in configuration."
. " Use --force to ignore this error";
}
}
# Create TOTP object
my $decrypt_totp = Lemonldap::NG::Common::TOTP->new(
key => ( $oldkey || $conf->{totp2fKey} || $conf->{key} ),
encryptSecret => 0,
);
my $encrypt_totp = Lemonldap::NG::Common::TOTP->new(
key => ( $newkey || $conf->{totp2fKey} || $conf->{key} ),
encryptSecret => ( ( $newkey || "" ) eq "DECRYPT" ? 0 : 1 ),
);
# Search psessions
my $args;
if ( $conf->{"persistentStorage"} ) {
$args = $conf->{"persistentStorageOptions"};
$args->{backend} = $conf->{"persistentStorage"};
}
else {
$args = $conf->{"globalStorageOptions"};
$args->{backend} = $conf->{"globalStorage"};
}
verbose "Searching for persistent sessions";
$res = Lemonldap::NG::Common::Apache::Session->searchOn( $args, '_session_kind',
'Persistent', '_2fDevices', '_session_uid' );
if ( ref($res) eq "HASH" ) {
verbose "Found " . scalar( keys %{$res} ) . " persistent sessions";
# For each found psession
for my $k ( keys %{$res} ) {
my $_2fDevices = $res->{$k}->{_2fDevices};
my $uid = $res->{$k}->{_session_uid};
verbose "Processing psession $k for user $uid";
encrypt_session( $k, $uid, $_2fDevices );
}
}
else {
die "Could not find any persistent sessions";
}
sub encrypt_session {
my ( $k, $uid, $_2fDevices ) = @_;
eval {
# parse _2fDevices if found
if ($_2fDevices) {
$_2fDevices = from_json($_2fDevices);
# If the session has 2f devices
if ( ref($_2fDevices) eq "ARRAY" and @{$_2fDevices} > 0 ) {
my $changed = convert_keys_for_user( $uid, $_2fDevices );
if ( $changed and !$dryrun ) {
eval { update2fArray( $k, $_2fDevices ); };
if ($@) {
info "Error updating session for $uid: $@";
}
}
}
else {
verbose "User $uid does not have a TOTP";
}
}
else {
verbose "User $uid does not have a TOTP";
}
};
if ($@) {
verbose "Error on psession $k: $@";
}
}
sub update2fArray {
my ( $id, $_2fDevices ) = @_;
my $session = Lemonldap::NG::Common::Session->new(
storageModule => $args->{backend},
storageModuleOptions => $args,
id => $id,
);
unless ( $session->data ) {
die "Error while opening session $id";
}
unless ( $session->update( { _2fDevices => to_json($_2fDevices) } ) ) {
die "Error while updating session $id";
}
}
sub convert_device_for_user {
my ( $uid, $device ) = @_;
my $changed = 0;
# In update mode, decrypt then encrypt
if ($update) {
my $cleartext_secret =
$decrypt_totp->get_cleartext_secret( $device->{_secret} );
if ($cleartext_secret) {
my $newsecret =
$encrypt_totp->get_storable_secret($cleartext_secret);
$device->{_secret} = $newsecret;
$changed = 1;
verbose 'Updated secret for ' . $uid;
}
else {
info 'Unable to decrypt TOTP secret for ' . $uid;
}
# In normal mode, only encrypt non-encrypted secrets
}
else {
if ( !$encrypt_totp->is_encrypted( $device->{_secret} ) ) {
$device->{_secret} =
$encrypt_totp->get_storable_secret( $device->{_secret} );
$changed = 1;
info 'Encrypted TOTP secret for ' . $uid;
}
else {
verbose 'Secret is already encrypted';
}
}
return $changed;
}
sub convert_keys_for_user {
my ( $uid, $devices ) = @_;
my $has_totp = 0;
my $changed = 0;
for my $device ( @{$devices} ) {
if ( $device->{type} eq "TOTP" ) {
$has_totp = 1;
my $epoch = $device->{epoch};
verbose "Processing device with epoch $epoch for user $uid";
my $changed_current = convert_device_for_user( $uid, $device );
$changed = 1 if $changed_current;
}
}
if ( !$has_totp ) {
verbose "User $uid does not have a TOTP";
}
return ( $has_totp, $changed );
}
__END__
=encoding utf8
=head1 NAME
encryptTotpSecret - A tool to encrypt existing TOTP secrets
=head1 SYNOPSIS
encryptTotpSecret [options]
Options:
-h, --help Show help message
-n, --dry-run Do not perform any operation
-u, --update Re-encrypt or decrypt already encrypted data
-o, --old-key Specify old key when re-encrypting
-k, --new-key Specify new key when re-encrypting or decrypting
-f, --force Ignore configuration errors
-v, --verbose Print additional information
This script is a migration tool that you can use after enabling TOTP secret
encryption in the Manager. It will make sure that existing secrets are
encrypted, and not just newly registered secrets.
=head1 OPTIONS
=over 8
=item B<--help>, B<-h>
Print a brief help message and exit.
=item B<--dry-run>, B<-n>
Prevent the script from saving modifications to the session database
=item B<--update>, B<-u>
By default, secrets that are already in encrypted form are skipped by the
script. Use this option to force already encrypted secrets to be decrypted,
then re-encrypted using a different key (or decrypted)
=item B<--old-key>, B<-o>
The key used to decrypt secrets in B<--update> mode.
By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
are used.
=item B<--new-key>, B<-k>
The key used to encrypt secrets. Use B<-u -k DECRYPT> to decrypt secrets instead.
By default, the B<totp2fKey> or B<key> LemonLDAP::NG configuration parameters
are used.
=item B<--force>, B<-f>
Encrypt existing TOTP secrets even if encryption is disabled in the configuration
=item B<--verbose>, B<-v>
Increase the level of details provided by the script
=back
=head1 SEE ALSO
L<http://lemonldap-ng.org/>
=head1 AUTHORS
=over
=item Maxime Besson, E<lt>maxime.besson@worteks.comE<gt>
=back
=head1 BUG REPORT
Use OW2 system to report bug or ask for features:
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>
=head1 DOWNLOAD
Lemonldap::NG is available at
L<https://lemonldap-ng.org/download>
=head1 COPYRIGHT AND LICENSE
=over
=item Copyright (C) 2008-2016 by Xavier Guimard, E<lt>x.guimard@free.frE<gt>
=item Copyright (C) 2008-2016 by Clément Oudot, E<lt>clem.oudot@gmail.comE<gt>
=back
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see L<http://www.gnu.org/licenses/>.
=cut

View File

@ -0,0 +1,116 @@
# Before `make install' is performed this script should be runnable with
# `make test'. After `make install' it should work as `perl Lemonldap-NG-Common.t'
#########################
use Time::Fake;
# Must subclass TOTP because it uses $self->logger etc.
package TestableTotp;
use Moose;
use Test::More;
use Lemonldap::NG::Common::TOTP;
use Lemonldap::NG::Common::Logger::Null;
extends 'Lemonldap::NG::Common::TOTP';
has logger => ( is => "ro", lazy => 1, builder => '_null_logger' );
has userLogger => ( is => "ro", lazy => 1, builder => '_null_logger' );
sub _null_logger {
return Lemonldap::NG::Common::Logger::Null->new;
}
package main;
use Test::More tests => 16;
BEGIN {
use_ok('Lemonldap::NG::Common::TOTP');
}
use strict;
### WARNING FOR DEVELOPPERS ###
# These constants are not to be messed with. If this unit test breaks, do NOT
# modify them, fix the code instead.
#
# In particular, if the $stored_secret no longer decrypts to $cleartext_secret,
# it means that users will lose their encrypted TOTP secrets on the next
# upgrade.
# If you need to change the cryptographic algorithm, make sure you remain
# compatible with existing stored values
my $timestamp = 1633009395;
my $totp_for_timestamp = 766039;
my $cleartext_secret = "ggtoch5x6naorymli6nh72ku4khwd4jr";
my $key = "azert";
my $encrypted_secret =
"{llngcrypt}TdEcd2vkmn4j0D8+str3v2D8zt0Dbm3sZ8TwlzdOKcang+qUmLraTQBztSrESRHDpAh+pQCKvDozuz9va7GxhHIkaKI3EZxOCWJ0rQCun/I=";
#########################
# Insert your test code below, the Test::More module is used here so read
# its man page ( perldoc Test::More ) for help writing this test script.
my $t = TestableTotp->new( key => $key, encryptSecret => 0 );
# Verification with no offset
Time::Fake->offset($timestamp);
is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
1, "TOTP code is valid" );
Time::Fake->offset( $timestamp + 30 );
is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
0, "TOTP code is no longer valid" );
Time::Fake->offset( $timestamp - 30 );
is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
0, "TOTP code is not valid yet" );
# Verification with offset 2 allows +1m and -1m
Time::Fake->offset( $timestamp + 45 );
is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
1, "TOTP code is valid" );
Time::Fake->offset( $timestamp - 45 );
is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
1, "TOTP code is valid" );
Time::Fake->offset( $timestamp + 95 );
is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
0, "TOTP code is no longer valid" );
Time::Fake->offset( $timestamp - 95 );
is( $t->verifyCode( 30, 2, 6, $cleartext_secret, $totp_for_timestamp ),
0, "TOTP code is not valid yet" );
# TOTP encryption tests
$t = TestableTotp->new( key => $key, encryptSecret => 0 );
Time::Fake->offset($timestamp);
is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
1, "TOTP is valid with encrypted secret and encryption disabled" );
$t = TestableTotp->new( key => $key, encryptSecret => 1 );
Time::Fake->offset($timestamp);
is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
1, "TOTP is valid with encrypted secret and encryption enabled" );
Time::Fake->offset($timestamp);
is( $t->verifyCode( 30, 0, 6, $cleartext_secret, $totp_for_timestamp ),
1, "TOTP is valid with cleartext secret and encryption enabled" );
# Encryption of TOTP secret, wrong key
$t = TestableTotp->new( key => "idunno", encryptSecret => 0 );
is( $t->verifyCode( 30, 0, 6, $encrypted_secret, $totp_for_timestamp ),
-1, "TOTP code fails to verify" );
# Do not encrypt new secrets unless we configured it
$t = TestableTotp->new( key => $key, encryptSecret => 0 );
is( $t->get_storable_secret($cleartext_secret),
$cleartext_secret,
"TOTP secret is stored as-is when encryption is disabled" );
# Encrypt new secrets if we configured it
$t = TestableTotp->new( key => $key, encryptSecret => 1 );
my $new = $t->get_storable_secret($cleartext_secret);
like( $new, qr/^{llngcrypt}/, "Secret looks encrypted" );
unlike( $new, qr/$cleartext_secret/, "Secret looks encrypted" );
Time::Fake->offset($timestamp);
is( $t->verifyCode( 30, 0, 6, $new, $totp_for_timestamp ),
1, "get_storable_secret produces working secret" );

View File

@ -16,12 +16,12 @@ has api => ( is => 'rw', isa => 'Str' );
sub init {
my ( $self, $args ) = @_;
eval { $self->api->init($args) };
if ( $@ and not( $self->{protection} and $self->{protection} eq 'none' ) ) {
if ( $@ and not( $args->{protection} and $args->{protection} eq 'none' ) ) {
$self->error($@);
return 0;
}
unless ( $self->api->checkConf($self)
or ( $self->{protection} and $self->{protection} eq 'none' ) )
or ( $args->{protection} and $args->{protection} eq 'none' ) )
{
$self->error(
"Unable to protect this server ($Lemonldap::NG::Common::Conf::msg)"
@ -30,7 +30,7 @@ sub init {
}
eval { $self->portal( $self->api->tsv->{portal}->() ) };
my $rule =
$self->{protection} || $self->api->localConfig->{protection} || '';
$args->{protection} || $self->api->localConfig->{protection} || '';
$self->rule(
$rule eq 'authenticate' ? 1 : $rule eq 'manager' ? '' : $rule );
return 1;

View File

@ -14,9 +14,9 @@ our $VERSION = '2.0.10';
sub init {
my ( $self, $args ) = @_;
$self->api('Lemonldap::NG::Handler::PSGI::Main') unless ( $self->api );
my $tmp = ( $self->Lemonldap::NG::Common::PSGI::init($args)
and $self->Lemonldap::NG::Handler::Lib::PSGI::init($args) );
return $tmp;
return 0 unless $self->Lemonldap::NG::Handler::Lib::PSGI::init($args);
return $self->Lemonldap::NG::Common::PSGI::init( $self->api->localConfig );
}
1;

View File

@ -4238,6 +4238,10 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
'default' => 6,
'type' => 'int'
},
'totp2fEncryptSecret' => {
'default' => 0,
'type' => 'bool'
},
'totp2fInterval' => {
'default' => 30,
'type' => 'int'

View File

@ -1956,6 +1956,11 @@ sub attributes {
type => 'int',
documentation => 'TOTP device time to live ',
},
totp2fEncryptSecret => {
type => 'bool',
default => 0,
documentation => 'Encrypt TOTP secrets in database',
},
# UTOTP 2F
utotp2fActivation => {

View File

@ -906,6 +906,7 @@ sub tree {
'totp2fInterval',
'totp2fRange',
'totp2fDigits',
'totp2fEncryptSecret',
'totp2fAuthnLevel',
'totp2fLabel',
'totp2fLogo',

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"تفعيل",
"totp2fAuthnLevel":"مستوى إثبات الهوية",
"totp2fDigits":"Number of digits",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interval",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"Authentication level",
"totp2fDigits":"Number of digits",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interval",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"Authentication level",
"totp2fDigits":"Number of digits",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interval",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Activación",
"totp2fAuthnLevel":"Nivel de autentificación",
"totp2fDigits":"Cantidad de dígitos",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Intervalo",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Etiqueta",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"Niveau d'authentification",
"totp2fDigits":"Nombre de chiffres",
"totp2fEncryptSecret":"Chiffrer le secret TOTP",
"totp2fInterval":"Intervalle",
"totp2fIssuer":"Nom du fournisseur",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Attivazione",
"totp2fAuthnLevel":"Livello di autenticazione",
"totp2fDigits":"Numero di cifre",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Intervallo",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Aktywacja",
"totp2fAuthnLevel":"Poziom uwierzytelnienia",
"totp2fDigits":"Ilość cyfr",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interwał",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Etykieta",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Aktivasyon",
"totp2fAuthnLevel":"Doğrulama seviyesi",
"totp2fDigits":"Rakam sayısı",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Süre aralığı",
"totp2fIssuer":"Düzenleyici adı",
"totp2fLabel":"Etiket",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"Kích hoạt",
"totp2fAuthnLevel":"Mức xác thực",
"totp2fDigits":"Number of digits",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interval",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"激活",
"totp2fAuthnLevel":"Authentication level",
"totp2fDigits":"Number of digits",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"Interval",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"Label",

View File

@ -1133,6 +1133,7 @@
"totp2fActivation":"啟用",
"totp2fAuthnLevel":"驗證等級",
"totp2fDigits":"位數",
"totp2fEncryptSecret":"Encrypt TOTP secrets",
"totp2fInterval":"間隔",
"totp2fIssuer":"Issuer name",
"totp2fLabel":"標籤",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,6 +59,7 @@ lib/Lemonldap/NG/Portal/Lib/_tokenRule.pm
lib/Lemonldap/NG/Portal/Lib/Captcha.pm
lib/Lemonldap/NG/Portal/Lib/CAS.pm
lib/Lemonldap/NG/Portal/Lib/Choice.pm
lib/Lemonldap/NG/Portal/Lib/CustomModule.pm
lib/Lemonldap/NG/Portal/Lib/DBI.pm
lib/Lemonldap/NG/Portal/Lib/LDAP.pm
lib/Lemonldap/NG/Portal/Lib/Net/LDAP.pm
@ -504,6 +505,7 @@ t/03-ConfTimeout.t
t/03-SessionTimeout.t
t/03-XSS-protection.t
t/04-language-selection.t
t/10-AuthCustom.t
t/19-Auth-Null.t
t/20-Auth-and-password-DBI-dynamic-hash.t
t/20-Auth-and-password-DBI.t
@ -523,6 +525,7 @@ t/26-AuthRemote.t
t/27-AuthProxy-with-choice.t
t/27-AuthProxy.t
t/28-AuthChoice-and-password.t
t/28-AuthChoice-Custom.t
t/28-AuthChoice-with-captcha.t
t/28-AuthChoice-with-info.t
t/28-AuthChoice-with-over.t
@ -612,6 +615,7 @@ t/35-REST-sessions-with-AuthBasic-handler.t
t/35-REST-sessions-with-REST-server.t
t/35-SOAP-config-backend.t
t/35-SOAP-sessions-with-SOAP-server.t
t/36-Combination-Custom.t
t/36-Combination-Kerberos-or-Demo.t
t/36-Combination-Password.t
t/36-Combination-with-Choice.t
@ -648,6 +652,7 @@ t/40-Notifications-XML-Server.t
t/41-Captcha.t
t/41-Token-with-global-storage.t
t/41-Token.t
t/42-Register-Custom.t
t/42-Register-Demo-with-captcha.t
t/42-Register-Demo-with-CustomBody.t
t/42-Register-Demo-with-token.t
@ -753,6 +758,7 @@ t/68-Impersonation-with-UnrestrictedUser.t
t/68-Impersonation.t
t/70-2F-TOTP-8-with-global-storage.t
t/70-2F-TOTP-and-U2F-with-TTL-and-JSON.t
t/70-2F-TOTP-encryption.t
t/70-2F-TOTP-with-History-and-Refresh.t
t/70-2F-TOTP-with-Range.t
t/70-2F-TOTP-with-TTL-and-JSON.t

View File

@ -157,12 +157,19 @@ sub run {
400 );
}
my $storable_secret =
$self->get_storable_secret( $token->{_totp2fSecret} );
unless ($storable_secret) {
$self->logger->error("Unable to encrypt TOTP secret");
return $self->p->sendError( $req, "serverError", 500 );
}
# Store TOTP secret
push @keep,
{
type => 'TOTP',
name => $TOTPName,
_secret => $token->{_totp2fSecret},
_secret => $storable_secret,
epoch => $epoch
};
$self->logger->debug(

View File

@ -1,35 +1,12 @@
package Lemonldap::NG::Portal::Auth::Custom;
use Lemonldap::NG::Portal::Lib::CustomModule;
use strict;
# Fake 'new' method here. Return Lemonldap::NG::Portal::Auth::Custom::{CustomAuth}->new
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customAuth} ) {
die 'Custom Auth module not defined';
}
my $res;
eval { $res = $self->{p}->loadModule( $self->{conf}->{customAuth} ) };
die 'Unable to load Auth module ' . $self->{conf}->{customAuth} if ($@);
return $res;
}
sub getDisplayType {
# Warning : $self passed here is the Portal itself
my ($self) = @_;
my $logo = ( $self->{conf}->{customAuth} =~ /::(\w+)$/ )[0];
if ( -e $self->{conf}->{templateDir}
. "/../htdocs/static/common/modules/"
. $logo
. ".png" )
{
$self->logger->debug("CustomAuth $logo.png found");
return "logo";
}
return "standardform";
}
our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
use constant {
custom_name => "Auth",
custom_config_key => "customAuth",
};
1;

View File

@ -1,23 +1,20 @@
package Lemonldap::NG::Portal::CertificateResetByMail::Custom;
use Lemonldap::NG::Portal::Lib::CustomModule;
use strict;
use Mouse;
extends 'Lemonldap::NG::Portal::Main::Plugin';
our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
use constant {
custom_name => "CertificateResetByMail",
custom_config_key => "customResetCertByMail",
};
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customRegister} ) {
die 'Custom register module not defined';
}
my $res = $self->{p}->loadModule( $self->{conf}->{customResetCertByMail} );
unless ($res) {
die 'Unable to load register module '
. $self->{conf}->{customResetCertByMail};
}
return $res;
return $class->Lemonldap::NG::Portal::Lib::CustomModule::new($self);
}
1;

View File

@ -388,10 +388,10 @@ sub run {
# Check openid scope
unless ( $self->_hasScope( 'openid', $oidc_request->{'scope'} ) ) {
$self->logger->debug("No openid scope found");
$self->logger->error("No openid scope found");
#TODO manage standard OAuth request
return PE_OK;
return PE_ERROR;
}
# Check Request JWT signature

View File

@ -9,10 +9,10 @@ with 'Lemonldap::NG::Portal::Lib::OverConf';
our $VERSION = '2.0.12';
has modules => ( is => 'rw', default => sub { {} } );
has rules => ( is => 'rw', default => sub { {} } );
has type => ( is => 'rw' );
has catch => ( is => 'rw', default => sub { {} } );
has modules => ( is => 'rw', default => sub { {} } );
has rules => ( is => 'rw', default => sub { {} } );
has type => ( is => 'rw' );
has catch => ( is => 'rw', default => sub { {} } );
has sessionKey => ( is => 'ro', default => '_choice' );
my $_choiceRules;
@ -244,7 +244,7 @@ sub _buildAuthLoop {
# Get displayType for this module
no strict 'refs';
my $displayType = eval {
"Lemonldap::NG::Portal::Auth::${auth}"
$self->_authentication->modules->{$_}
->can('getDisplayType')->( $self, $req );
} || 'logo';
@ -252,10 +252,8 @@ sub _buildAuthLoop {
"Display type $displayType for module $auth");
$optionsLoop->{$displayType} = 1;
my $logo = $_;
if ( $auth eq 'Custom' ) {
$logo =
( $self->{conf}->{customAuth} =~ /::(\w+)$/ )[0];
}
my $foundLogo = 0;
# If displayType is logo, check if key.png is available
if ( -e $self->conf->{templateDir}
@ -264,11 +262,30 @@ sub _buildAuthLoop {
. ".png" )
{
$optionsLoop->{logoFile} = $logo . ".png";
$foundLogo = 1;
}
else {
$optionsLoop->{logoFile} = $auth . ".png";
}
# Compatibility, with Custom, try the module name if
# key was not found
if ( $auth eq 'Custom' and not $foundLogo ) {
$logo =
( ( $self->{conf}->{customAuth} || "" ) =~ /::(\w+)$/ )
[0];
if (
$logo
and ( -e $self->conf->{templateDir}
. "/../htdocs/static/common/modules/"
. $logo
. ".png" )
)
{
$optionsLoop->{logoFile} = $logo . ".png";
}
}
# Register item in loop
push @authLoop, $optionsLoop;

View File

@ -0,0 +1,30 @@
package Lemonldap::NG::Portal::Lib::CustomModule;
# Fake 'new' method here
sub new {
my ( $class, $self ) = @_;
my $configKey = $class->custom_config_key;
my $name = $class->custom_name;
my $module = $self->{conf}->{$configKey};
unless ($module) {
die "Custom $name module not defined";
}
$module = "Lemonldap::NG::Portal$module" if ( $module =~ /^::/ );
eval "require $module";
if ($@) {
die "Custom $name module failed to compile: $@";
}
my $obj = eval { $module->new($self); };
if ($@) {
die "Custom $name module failed to create instance: $@";
}
$self->{p}->logger->debug("Custom $name module loaded");
return $obj;
}
1;

View File

@ -12,7 +12,7 @@ use Mouse;
extends 'Lemonldap::NG::Common::Module';
our $VERSION = '2.0.0';
our $VERSION = '2.0.14';
# PROPERTIES
@ -91,16 +91,14 @@ sub init {
# @return SQL statement string
sub hash_password {
my ( $self, $password, $hash ) = @_;
if ( $hash =~ /^(md5|sha|sha1|encrypt)$/i ) {
if ($hash) {
$self->logger->debug( "Using " . uc($hash) . " to hash password" );
return uc($hash) . "($password)";
}
else {
$self->logger->notice(
"No valid password hash, using clear text for password");
$self->logger->debug("No password hash, using clear text for password");
return $password;
}
}
# Return hashed password for use in SQL SELECT statement

View File

@ -1,17 +1,12 @@
package Lemonldap::NG::Portal::Password::Custom;
use Lemonldap::NG::Portal::Lib::CustomModule;
use strict;
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customPassword} ) {
die 'Custom Password module not defined';
}
eval $self->{p}->loadModule( $self->{conf}->{customPassword} );
($@)
? return $self->{p}->loadModule( $self->{conf}->{customPassword} )
: die 'Unable to load Password module ' . $self->{conf}->{customPassword};
}
our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
use constant {
custom_name => "Password",
custom_config_key => "customPassword",
};
1;

View File

@ -1,22 +1,12 @@
package Lemonldap::NG::Portal::Register::Custom;
use Lemonldap::NG::Portal::Lib::CustomModule;
use strict;
use Mouse;
extends 'Lemonldap::NG::Portal::Register::Base';
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customRegister} ) {
die 'Custom register module not defined';
}
my $res = $self->{p}->loadModule( $self->{conf}->{customRegister} );
unless ($res) {
die 'Unable to load register module ' . $self->{conf}->{customRegister};
}
return $res;
}
our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
use constant {
custom_name => "Register",
custom_config_key => "customRegister",
};
1;

View File

@ -1,17 +1,12 @@
package Lemonldap::NG::Portal::UserDB::Custom;
use Lemonldap::NG::Portal::Lib::CustomModule;
use strict;
sub new {
my ( $class, $self ) = @_;
unless ( $self->{conf}->{customUserDB} ) {
die 'Custom User DB module not defined';
}
eval $self->{p}->loadModule( $self->{conf}->{customUserDB} );
($@)
? return $self->{p}->loadModule( $self->{conf}->{customUserDB} )
: die 'Unable to load UserDB module ' . $self->{conf}->{customUserDB};
}
our @ISA = qw(Lemonldap::NG::Portal::Lib::CustomModule);
use constant {
custom_name => "UserDB",
custom_config_key => "customUserDB",
};
1;

View File

@ -0,0 +1,172 @@
use Test::More;
use strict;
use IO::String;
use MIME::Base64;
require 't/test-lib.pm';
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => "Custom",
customAuth => "::Auth::Demo",
customUserDB => "::UserDB::Demo"
}
}
);
# Test normal first access
# ------------------------
ok( $res = $client->_get('/'), 'Unauth JSON request' );
count(1);
expectReject($res);
# Test "first access" with an unprotected url
ok(
$res = $client->_get(
'/',
query => 'url=' . encode_base64( "http://test.example.fr/", '' ),
accept => 'text/html'
),
'Get Menu'
);
ok( $res->[2]->[0] =~ /<span trmsg="37">/, 'Rejected with PE_BADURL' )
or print STDERR Dumper( $res->[2]->[0] );
ok( $res->[2]->[0] =~ m%<span id="languages"></span>%, ' Language icons found' )
or print STDERR Dumper( $res->[2]->[0] );
count(3);
# Test "first access" with a wildcard-protected url
ok(
$res = $client->_get(
'/',
query => 'url=' . encode_base64( "http://test.example.llng/", '' ),
accept => 'text/html'
),
'Get Menu'
);
ok( $res->[2]->[0] =~ /<span trmsg="9">/, 'Rejected with PE_FIRSTACCESS' )
or print STDERR Dumper( $res->[2]->[0] );
ok( $res->[2]->[0] =~ m%<span id="languages"></span>%, ' Language icons found' )
or print STDERR Dumper( $res->[2]->[0] );
count(3);
# Test "first access" with good url
ok(
$res =
$client->_get( '/', query => 'url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==' ),
'Unauth ajax request with good url'
);
count(1);
expectReject($res);
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu' );
ok( $res->[2]->[0] =~ m%<span id="languages"></span>%, ' Language icons found' )
or print STDERR Dumper( $res->[2]->[0] );
count(2);
# Try to authenticate with unknown user
# -------------------------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=jdoe&password=jdoe'),
accept => 'text/html',
length => 23
),
'Auth query'
);
ok(
$res->[2]->[0] =~ /<span trmsg="5">/,
'jdoe rejected with PE_BADCREDENTIALS'
) or print STDERR Dumper( $res->[2]->[0] );
ok( $res->[2]->[0] =~ m%<span trspan="connect">Connect</span>%,
'Found connect button' )
or print STDERR Dumper( $res->[2]->[0] );
count(3);
# Try to authenticate with bad password
# -------------------------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=jdoe'),
accept => 'text/html',
length => 23
),
'Auth query'
);
count(1);
ok(
$res->[2]->[0] =~ /<span trmsg="5">/,
'dwho rejected with PE_BADCREDENTIALS'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
ok( $res->[2]->[0] =~ m%<span trspan="connect">Connect</span>%,
'Found connect button' )
or print STDERR Dumper( $res->[2]->[0] );
count(1);
# Try to authenticate with good password
# --------------------------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23,
),
'Auth query'
);
count(1);
expectOK($res);
my $id = expectCookie($res);
# Try to get a redirection for an auth user with a valid url
# ----------------------------------------------------------
ok(
$res = $client->_get(
'/',
query => 'url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'Auth ajax request with good url'
);
count(1);
expectRedirection( $res, 'http://test1.example.com/' );
expectAuthenticatedAs( $res, 'dwho' );
# Try to get a redirection for an auth user with a bad url (host undeclared
# in manager)
# -------------------------------------------------------------------------
ok(
$res = $client->_get(
'/',
query => 'url=aHR0cHM6Ly90LmV4YW1wbGUuY29tLw==',
cookie => "lemonldap=$id",
accept => 'text/html'
),
'Auth request with bad url'
);
count(1);
expectOK($res);
expectAuthenticatedAs( $res, 'dwho' );
require 't/test-psgi.pm';
ok( $res = mirror( cookie => "lemonldap=$id" ), 'PSGI test' );
count(1);
expectOK($res);
expectAuthenticatedAs( $res, 'dwho' );
# Test logout
$client->logout($id);
#print STDERR Dumper($res);
clean_sessions();
done_testing( count() );

View File

@ -0,0 +1,72 @@
use Test::More;
use strict;
use IO::String;
use JSON qw(from_json);
require 't/test-lib.pm';
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
portalMainLogo => 'common/logos/logo_llng_old.png',
authentication => 'Choice',
restSessionServer => 1,
nullAuthnLevel => 1,
userDB => 'Same',
authChoiceParam => 'test',
authChoiceModules => {
'1_securenull' =>
'Custom;Custom;Null;;;{"nullAuthnLevel": 3, "customAuth": "::Auth::Null", "customUserDB": "::UserDB::Null"}',
'2_null' =>
'Custom;Custom;Null;;;{"customAuth": "::Auth::Null", "customUserDB": "::UserDB::Null"}',
},
}
}
);
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu' );
ok( $res->[2]->[0] =~ /1_securenull/, '1_securenull displayed' );
ok( $res->[2]->[0] =~ /2_null/, '2_null displayed' );
# Authenticate on first choice
my $postString = 'user=dwho&password=dwho&test=1_securenull';
ok(
$res = $client->_post(
'/',
IO::String->new($postString),
length => length($postString)
),
'Auth query'
);
expectOK($res);
my $id = expectCookie($res);
ok( $res = $client->_get("/sessions/global/$id"), 'Get session' );
my $sessiondata = from_json( $res->[2]->[0] );
is( $sessiondata->{authenticationLevel}, 3, "Overriden authentication level" );
$client->logout($id);
# Authenticate on second choice
$postString = 'user=dwho&password=dwho&test=2_null';
# Try to authenticate
# -------------------
ok(
$res = $client->_post(
'/',
IO::String->new($postString),
length => length($postString)
),
'Auth query'
);
expectOK($res);
$id = expectCookie($res);
ok( $res = $client->_get("/sessions/global/$id"), 'Get session' );
$sessiondata = from_json( $res->[2]->[0] );
is( $sessiondata->{authenticationLevel}, 1, "Default authentication level" );
$client->logout($id);
clean_sessions();
done_testing();

View File

@ -0,0 +1,129 @@
use Test::More;
use strict;
use IO::String;
require 't/test-lib.pm';
my $res;
my $maintests = 3;
my $client;
my $userdb = tempdb();
SKIP: {
eval { require DBI; require DBD::SQLite; };
if ($@) {
skip 'DBD::SQLite not found', $maintests;
}
my $dbh = DBI->connect("dbi:SQLite:dbname=$userdb");
$dbh->do('CREATE TABLE users (user text,password text,name text)');
$dbh->do("INSERT INTO users VALUES ('dvador','dvador','Test user 1')");
$dbh->do("INSERT INTO users VALUES ('rtyler','rtyler','Test user 1')");
$client = iniCmb('[Dm] or [DB]');
$client->logout( expectCookie( try('dwho') ) );
expectCookie( try('dvador') );
$client = iniCmb('[Dm] and [DB]');
$client->logout( expectCookie( try('rtyler') ) );
expectReject( try('dwho') );
$client = iniCmb('if($env->{HTTP_X} eq "dwho") then [Dm] else [DB]');
$client->logout( expectCookie( try('dwho') ) );
$client->logout( expectCookie( try('dvador') ) );
$client = iniCmb(
'if($env->{HTTP_X} eq "rtyler") then [Dm] and [DB] else if($env->{HTTP_X} eq "dvador") then [DB] else [DB]'
);
my $id = expectCookie( try('rtyler') );
my $res;
ok( $res = $client->_get("/sessions/global/$id"), 'Get session content' );
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
or print STDERR $@;
ok(
( $res->{demo} eq 'rtyler' and $res->{dbi} eq 'rtyler' ),
' Demo and DBI exported variables exist in session'
);
expectCookie( try('dvador') );
expectReject( try('dwho') );
$client = iniCmb(
'if($env->{REMOTE_ADDR} =~ /^(127\.)/) then [Dm] or [DB] else [DB]');
expectCookie( try('rtyler') );
expectCookie( try('dwho') );
$client = iniCmb(
'if($env->{REMOTE_ADDR} =~ /^(128\.)/) then [Dm,Dm] or [DB,DB] else [DB,DB]'
);
expectCookie( try('rtyler') );
expectReject( try('dwho') );
}
count($maintests);
clean_sessions();
done_testing( count() );
sub try {
my $user = shift;
my $s = "user=$user&password=$user";
my $res;
ok(
$res = $client->_post(
'/', IO::String->new($s),
length => length($s),
custom => { HTTP_X => $user }
),
" Try to connect with login $user"
);
count(1);
return $res;
}
sub iniCmb {
my $expr = shift;
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
if (
my $res = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
authentication => 'Combination',
userDB => 'Same',
restSessionServer => 1,
combination => $expr,
combModules => {
DB => {
for => 0,
type => 'Custom',
over => {
customAuth => "::Auth::DBI",
customUserDB => "::UserDB::DBI",
},
},
Dm => {
for => 0,
type => 'Custom',
over => {
customAuth => "::Auth::Demo",
customUserDB => "::UserDB::Demo",
},
},
},
dbiAuthChain => "dbi:SQLite:dbname=$userdb",
dbiAuthUser => '',
dbiAuthPassword => '',
dbiAuthTable => 'users',
dbiAuthLoginCol => 'user',
dbiAuthPasswordCol => 'password',
dbiAuthPasswordHash => '',
dbiExportedVars => { dbi => 'user' },
demoExportedVars => { demo => 'uid' },
}
}
)
)
{
pass(qq'Expression loaded: "$expr"');
count(1);
return $res;
}
}

View File

@ -0,0 +1,102 @@
use Test::More;
use strict;
use IO::String;
BEGIN {
eval {
require 't/test-lib.pm';
require 't/smtp.pm';
};
}
my $maintests = 11;
my ( $res, $user, $pwd, $mail, $subject );
SKIP: {
eval 'require Email::Sender::Simple; use Text::Unidecode';
if ($@) {
skip 'Missing dependencies', $maintests;
}
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
portalDisplayRegister => 1,
authentication => 'Demo',
userDB => 'Same',
registerDB => 'Custom',
customRegister => '::Register::Demo',
registerConfirmSubject => 'Demonstration',
captcha_register_enabled => 0,
}
}
);
# Test normal first access
# ------------------------
ok(
$res = $client->_get( '/register', accept => 'text/html' ),
'Unauth request',
);
my ( $host, $url, $query ) =
expectForm( $res, '#', undef, 'firstname', 'lastname', 'mail' );
ok(
$res = $client->_post(
'/register',
IO::String->new(
'firstname=Fôo&lastname=Bà Bar&mail=foobar%40badwolf.org'),
length => 53,
accept => 'text/html'
),
'Ask to create account'
);
expectOK($res);
$mail = mail();
$subject = subject();
ok( $subject eq 'Demonstration', 'Found subject' )
or explain( $subject, 'Custom subject' );
ok( $mail =~ m#a href="http://auth.example.com/register\?(.+?)"#,
'Found register token' )
or explain( $mail, 'Confirm body' );
$query = $1;
ok( $query =~ /register_token=/, 'Found register_token' );
ok( $mail =~ /Fôo/, 'UTF-8 works' ) or explain( $mail, 'Fôo' );
ok(
$res =
$client->_get( '/register', query => $query, accept => 'text/html' ),
'Push register_token'
);
expectOK($res);
$mail = mail();
$subject = subject();
ok( $subject eq '[LemonLDAP::NG] Your new account', 'Found subject' )
or explain( $subject, 'Default subject' );
ok(
$mail =~
m#Your login is.+?<b>(\w+)</b>.*?Your password is.+?<b>(.*?)</b>#s,
'Found user and password'
) or explain( $mail, 'Done body' );
$user = $1;
$pwd = $2;
ok( $user eq 'fbabar', 'Get good login' );
ok(
$res = $client->_post(
'/', IO::String->new("user=fbabar&password=fbabar"),
length => 27,
accept => 'text/html'
),
'Try to authenticate'
);
expectCookie($res);
}
count($maintests);
clean_sessions();
done_testing( count() );

View File

@ -0,0 +1,229 @@
use Test::More;
use strict;
use IO::String;
use JSON qw/from_json to_json/;
require 't/test-lib.pm';
my $maintests = 30;
SKIP: {
eval { require Convert::Base32 };
if ($@) {
skip 'Convert::Base32 is missing', $maintests;
}
eval { require Authen::OATH };
if ($@) {
skip 'Authen::OATH is missing', $maintests;
}
require Lemonldap::NG::Common::TOTP;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
totp2fSelfRegistration => 1,
totp2fActivation => 1,
totp2fDigits => 6,
totp2fTTL => -1,
totp2fEncryptSecret => 1,
formTimeout => 120,
requireToken => 1,
}
}
);
my $res;
# Try to authenticate
# -------------------
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
my ( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
my $id = expectCookie($res);
expectRedirection( $res, 'http://auth.example.com/' );
# TOTP form
ok(
$res = $client->_get(
'/2fregisters',
cookie => "lemonldap=$id",
accept => 'text/html',
),
'Form registration'
);
expectRedirection( $res, qr#/2fregisters/totp$# );
ok(
$res = $client->_get(
'/2fregisters/totp',
cookie => "lemonldap=$id",
accept => 'text/html',
),
'Form registration'
);
ok( $res->[2]->[0] =~ /totpregistration\.(?:min\.)?js/, 'Found TOTP js' );
# JS query
ok(
$res = $client->_post(
'/2fregisters/totp/getkey', IO::String->new(''),
cookie => "lemonldap=$id",
length => 0,
),
'Get new key'
);
eval { $res = JSON::from_json( $res->[2]->[0] ) };
ok( not($@), 'Content is JSON' )
or explain( $res->[2]->[0], 'JSON content' );
my ( $key, $token );
ok( $key = $res->{secret}, 'Found secret' );
ok( $token = $res->{token}, 'Found token' );
$key = Convert::Base32::decode_base32($key);
# Post code
my $code;
ok( $code = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ),
'Code' );
ok( $code =~ /^\d{6}$/, 'Code contains 6 digits' );
my $s = "code=$code&token=$token";
ok(
$res = $client->_post(
'/2fregisters/totp/verify',
IO::String->new($s),
length => length($s),
cookie => "lemonldap=$id",
),
'Post code'
);
eval { $res = JSON::from_json( $res->[2]->[0] ) };
ok( not($@), 'Content is JSON' )
or explain( $res->[2]->[0], 'JSON content' );
ok( $res->{result} == 1, 'Key is registered' );
# Try to sign-in
$client->logout($id);
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
( $host, $url, $query ) =
expectForm( $res, undef, '/totp2fcheck', 'token' );
# Generate TOTP with LLNG
my $totp;
ok( $totp = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ),
'LLNG Code' );
# Generate TOTP with an external application to validate LLNG TOTP formula
my $oath = Authen::OATH->new( digits => 6 );
ok( $code = $oath->totp($key), 'Ext. App Code' );
ok( $code == $totp, 'Both TOTP match' )
or explain( [ $code, $totp ], 'LLNG and Ext. App TOTP mismatch' );
$query =~ s/code=/code=$code/;
ok(
$res = $client->_post(
'/totp2fcheck', IO::String->new($query),
length => length($query),
),
'Post code'
);
$id = expectCookie($res);
$client->logout($id);
# Try to sign-in with an expired OTT
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
( $host, $url, $query ) =
expectForm( $res, undef, '/totp2fcheck', 'token' );
# Generate TOTP with LLNG
ok( $totp = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ),
'LLNG Code' );
$query =~ s/code=/code=$code/;
# Skipping time until form token expiration
Time::Fake->offset("+5m");
ok(
$res = $client->_post(
'/totp2fcheck', IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Post code'
);
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
ok( $res->[2]->[0] =~ /<span trmsg="82"><\/span>/, 'Token expired' )
or print STDERR Dumper( $res->[2]->[0] );
# Try to sign-in
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(
'/',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Auth query'
);
( $host, $url, $query ) =
expectForm( $res, undef, '/totp2fcheck', 'token' );
# Verify that TOTP secret is encrypted
my $session = getPSession( "dwho" );
my $_2fDevices = $session->data->{_2fDevices};
ok( $_2fDevices, "TOTP persistent data found" );
$_2fDevices = from_json($_2fDevices);
is( @{$_2fDevices}, 1, "Only one device found" );
my $secret = $_2fDevices->[0]->{_secret};
like( $secret, qr/^\{llngcrypt\}/, "TOTP secret is encrypted" );
}
count($maintests);
clean_sessions();
done_testing( count() );

View File

@ -633,12 +633,14 @@ fi
%config(noreplace) %{apache_confdir}/z-lemonldap-ng-portal.conf
%{_mandir}/man1/convertConfig*
%{_mandir}/man1/convertSessions*
%{_mandir}/man1/encryptTotpSecrets*
%{_mandir}/man1/lemonldap-ng-sessions*
%dir %{_libexecdir}/%{name}
%dir %{lm_sbindir}
%dir %{lm_bindir}
%{lm_bindir}/convertConfig
%{lm_bindir}/convertSessions
%{lm_bindir}/encryptTotpSecrets
%{lm_bindir}/lemonldap-ng-sessions
%{lm_bindir}/importMetadata
%{lm_bindir}/lmMigrateConfFiles2ini