Merge branch 'v2.0' into 2683
This commit is contained in:
commit
3ffb7aa607
1
Makefile
1
Makefile
|
@ -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 \
|
||||
|
|
2
debian/liblemonldap-ng-common-perl.install
vendored
2
debian/liblemonldap-ng-common-perl.install
vendored
|
@ -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
|
||||
|
|
|
@ -30,6 +30,7 @@ theses :
|
|||
* **OAUTH2_USERNAME_MAP**: ``sub``
|
||||
* **OAUTH2_FULLNAME_MAP**: ``name``
|
||||
* **OAUTH2_EMAIL_MAP**: ``email``
|
||||
* **OAUTH2_REQUEST_PERMISSIONS**: ``openid profile email``
|
||||
|
||||
|
||||
.. danger::
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -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
|
||||
------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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' );
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
314
lemonldap-ng-common/scripts/encryptTotpSecrets
Normal file
314
lemonldap-ng-common/scripts/encryptTotpSecrets
Normal 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
|
116
lemonldap-ng-common/t/37-Common-TOTP.pm
Normal file
116
lemonldap-ng-common/t/37-Common-TOTP.pm
Normal 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" );
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -906,6 +906,7 @@ sub tree {
|
|||
'totp2fInterval',
|
||||
'totp2fRange',
|
||||
'totp2fDigits',
|
||||
'totp2fEncryptSecret',
|
||||
'totp2fAuthnLevel',
|
||||
'totp2fLabel',
|
||||
'totp2fLogo',
|
||||
|
|
|
@ -1133,6 +1133,7 @@
|
|||
"totp2fActivation":"تفعيل",
|
||||
"totp2fAuthnLevel":"مستوى إثبات الهوية",
|
||||
"totp2fDigits":"Number of digits",
|
||||
"totp2fEncryptSecret":"Encrypt TOTP secrets",
|
||||
"totp2fInterval":"Interval",
|
||||
"totp2fIssuer":"Issuer name",
|
||||
"totp2fLabel":"Label",
|
||||
|
|
|
@ -1133,6 +1133,7 @@
|
|||
"totp2fActivation":"Activation",
|
||||
"totp2fAuthnLevel":"Authentication level",
|
||||
"totp2fDigits":"Number of digits",
|
||||
"totp2fEncryptSecret":"Encrypt TOTP secrets",
|
||||
"totp2fInterval":"Interval",
|
||||
"totp2fIssuer":"Issuer name",
|
||||
"totp2fLabel":"Label",
|
||||
|
|
|
@ -1133,6 +1133,7 @@
|
|||
"totp2fActivation":"Activation",
|
||||
"totp2fAuthnLevel":"Authentication level",
|
||||
"totp2fDigits":"Number of digits",
|
||||
"totp2fEncryptSecret":"Encrypt TOTP secrets",
|
||||
"totp2fInterval":"Interval",
|
||||
"totp2fIssuer":"Issuer name",
|
||||
"totp2fLabel":"Label",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1133,6 +1133,7 @@
|
|||
"totp2fActivation":"Aktywacja",
|
||||
"totp2fAuthnLevel":"Poziom uwierzytelnienia",
|
||||
"totp2fDigits":"Ilość cyfr",
|
||||
"totp2fEncryptSecret":"Encrypt TOTP secrets",
|
||||
"totp2fInterval":"Interwał",
|
||||
"totp2fIssuer":"Issuer name",
|
||||
"totp2fLabel":"Etykieta",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1133,6 +1133,7 @@
|
|||
"totp2fActivation":"激活",
|
||||
"totp2fAuthnLevel":"Authentication level",
|
||||
"totp2fDigits":"Number of digits",
|
||||
"totp2fEncryptSecret":"Encrypt TOTP secrets",
|
||||
"totp2fInterval":"Interval",
|
||||
"totp2fIssuer":"Issuer name",
|
||||
"totp2fLabel":"Label",
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
172
lemonldap-ng-portal/t/10-AuthCustom.t
Normal file
172
lemonldap-ng-portal/t/10-AuthCustom.t
Normal 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() );
|
72
lemonldap-ng-portal/t/28-AuthChoice-Custom.t
Normal file
72
lemonldap-ng-portal/t/28-AuthChoice-Custom.t
Normal 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();
|
129
lemonldap-ng-portal/t/36-Combination-Custom.t
Normal file
129
lemonldap-ng-portal/t/36-Combination-Custom.t
Normal 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;
|
||||
}
|
||||
}
|
102
lemonldap-ng-portal/t/42-Register-Custom.t
Normal file
102
lemonldap-ng-portal/t/42-Register-Custom.t
Normal 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() );
|
229
lemonldap-ng-portal/t/70-2F-TOTP-encryption.t
Normal file
229
lemonldap-ng-portal/t/70-2F-TOTP-encryption.t
Normal 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() );
|
||||
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user