Merge branch 'fix-captcha-api-2692' into 'v2.0'

New Captcha API

See merge request lemonldap-ng/lemonldap-ng!262
This commit is contained in:
Maxime Besson 2022-06-17 14:31:13 +00:00
commit 77557c246d
40 changed files with 775 additions and 264 deletions

View File

@ -31,3 +31,18 @@ Go in ``General parameters`` > ``Portal`` > ``Captcha``:
- **Activation in register form**: set to 1 to display captcha in
register form
- **Size**: length of captcha
- **Captcha module**: allows you to use a custom Captcha module, see
:ref:`below <customcaptcha>`. Leave it blank to use the default Captcha
implementation
- **Captcha module options**: options for the custom Captcha module
.. _customcaptcha:
Custom Captcha modules
----------------------
.. versionadded:: 2.0.15
If the default Captcha does not meet your requirements, you can replace it with
a different implementation. See the ``Lemonldap::NG::Portal::Captcha`` manual
page for details on how to implement a Captcha module.

View File

@ -26,6 +26,26 @@ Known regressions in the latest released version
None
2.0.15
------
New Captcha API
~~~~~~~~~~~~~~~
It is now possible to create your own Captcha modules to replace the one provided by default.
In order for custom Captcha modules to work, you need to modify your custom ``standardform.tpl``, ``mail.tpl`` and ``register.tpl`` template files:
.. code:: diff
- <TMPL_IF NAME=CAPTCHA_SRC>
- <TMPL_INCLUDE NAME="captcha.tpl">
+ <TMPL_IF NAME=CAPTCHA_HTML>
+ <TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
If you are using the default templates from the ``bootstrap`` theme, you don't need to change anything.
2.0.14
------

View File

@ -29,7 +29,7 @@ use constant DEFAULTCONFBACKEND => "File";
use constant DEFAULTCONFBACKENDOPTIONS => (
dirName => '/usr/local/lemonldap-ng/data/conf',
);
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 $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(?:a(?:s(?:A(?:ppMetaData(?:(?:ExportedVar|Option|Macro)s|Node)|ttributes)|S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions))|ptchaOptions)|(?: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)|to(?:tp2f(?:UserCanRemoveKey|EncryptSecret)|kenUseGlobalStorage)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|w(?:ebauthn2fUserCanRemoveKey|sdlServer)|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|findUser)$/;

View File

@ -22,7 +22,7 @@ our $specialNodeHash = {
};
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:c(?:as(?:StorageOption|Attribute)|ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)|l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|(?:(?:d(?:emo|bi)|webID)E|e)xportedVar|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|OPMetaDataJ(?:SON|WKS))|penIdExportedVars)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember|fExtra)|S(?:MTPTLSOpts|SLVarIf))';
our $simpleHashKeys = '(?:(?:c(?:a(?:s(?:StorageOption|Attribute)|ptchaOption)|ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)|l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|(?:(?:d(?:emo|bi)|webID)E|e)xportedVar|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|OPMetaDataJ(?:SON|WKS))|penIdExportedVars)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember|fExtra)|S(?:MTPTLSOpts|SLVarIf))';
our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaDataNode|virtualHost)s';
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:(?:UserAttribut|Servic|Rul)e|AuthnLevel)|(?:ExportedVar|Macro)s)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:Re(?:solutionRule|new)|ProxiedServices|DisplayName|SortNumber|Gateway|Icon|Url)|ExportedVars)';

View File

@ -681,6 +681,9 @@ sub attributes {
'default' => 30,
'type' => 'int'
},
'captcha' => {
'type' => 'PerlModule'
},
'captcha_login_enabled' => {
'default' => 0,
'type' => 'bool'
@ -697,6 +700,9 @@ sub attributes {
'default' => 6,
'type' => 'int'
},
'captchaOptions' => {
'type' => 'keyTextContainer'
},
'casAccessControlPolicy' => {
'default' => 'none',
'select' => [ {

View File

@ -1479,6 +1479,16 @@ sub attributes {
default => 6,
documentation => 'Captcha size',
},
captcha => {
type => 'PerlModule',
documentation => 'Captcha backend module',
flags => 'hp',
},
captchaOptions => {
type => 'keyTextContainer',
documentation => 'Captcha module options',
flags => 'hp',
},
# Variables
exportedVars => {

View File

@ -118,13 +118,20 @@ sub tree {
},
{
title => 'portalCaptcha',
help => 'captcha.html',
form => 'simpleInputContainer',
help => 'captcha.html#configuration',
nodes => [
'captcha_login_enabled',
'captcha_mail_enabled',
'captcha_register_enabled',
'captcha_size',
{
title => 'captchaCustom',
help => 'captcha.html#configuration',
nodes => [
'captcha',
'captchaOptions',
]
},
]
}
]
@ -132,7 +139,7 @@ sub tree {
{
title => 'authParams',
help =>
'start.html#authentication-users-and-password-databases',
'start.html#authentication-users-and-password-databases',
form => 'authParams',
nodes => [
'authentication', 'userDB', 'passwordDB', 'registerDB'
@ -217,8 +224,8 @@ sub tree {
nodes => [
'dbiDynamicHashEnabled',
'dbiDynamicHashValidSchemes',
'dbiDynamicHashValidSaltedSchemes',
'dbiDynamicHashNewPasswordScheme'
'dbiDynamicHashValidSaltedSchemes',
'dbiDynamicHashNewPasswordScheme'
]
}
]
@ -556,7 +563,7 @@ sub tree {
help => 'logs.html',
form => 'simpleInputContainer',
nodes =>
[ 'whatToTrace', 'customToTrace', 'hiddenAttributes' ]
[ 'whatToTrace', 'customToTrace', 'hiddenAttributes' ]
},
{
title => 'cookieParams',
@ -653,7 +660,7 @@ sub tree {
{
title => 'soapServices',
help =>
'portalservers.html#SOAP_(deprecated)',
'portalservers.html#SOAP_(deprecated)',
form => 'simpleInputContainer',
nodes => [
'soapSessionServer',
@ -687,14 +694,14 @@ sub tree {
{
title => 'serverNotification',
help =>
'notifications.html#notification-server',
'notifications.html#notification-server',
nodes => [
'notificationServer',
'notificationDefaultCond',
'notificationServerSentAttributes',
{
title =>
'notificationServerMethods',
'notificationServerMethods',
form => 'simpleInputContainer',
nodes => [
'notificationServerPOST',

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Lock time",
"cancel":"إلغاء",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"التفعيل في استمارة تسجيل الدخول",
"captcha_mail_enabled":"التفعيل في إعادة تعيين كلمة المرور بواسطة استمارة البريد",
"captcha_register_enabled":"التفعيل في استمارة التسجيل",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Lock time",
"cancel":"Cancel",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Activation in login form",
"captcha_mail_enabled":"Activation in password reset by mail form",
"captcha_register_enabled":"Activation in register form",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Lock time",
"cancel":"Cancelar",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Activación en formulario de acceso",
"captcha_mail_enabled":"Activación en formulario de restauración por correo",
"captcha_register_enabled":"Activación en formulario de registro",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Temps maximum de verrouillage",
"bruteForceProtectionTempo":"Temps de verrouillage",
"cancel":"Annuler",
"captcha":"Module Captcha",
"captchaCustom":"Module Captcha personnalisé",
"captchaOptions":"Options du module Captcha",
"captcha_login_enabled":"Activation dans le formulaire d'authentification",
"captcha_mail_enabled":"Activation dans le formulaire de réinitialisation par mail",
"captcha_register_enabled":"Activation dans le formulaire de création de compte",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"זמן הנעילה המרבי",
"bruteForceProtectionTempo":"זמן נעילה",
"cancel":"ביטול",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"הפעלה בטופס הכניסה",
"captcha_mail_enabled":"הפעלה באיפוס סיסמה בטופס בדוא״ל",
"captcha_register_enabled":"הפעלה בטופס הרשמה",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Lock time",
"cancel":"Cancella",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Attivazione nel modulo di login",
"captcha_mail_enabled":"Attivazione della reimpostazione della password tramite modulo di posta",
"captcha_register_enabled":"Attivazione nel formulario di registro",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Czas blokady",
"cancel":"Anuluj",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Aktywacja w formularzu logowania",
"captcha_mail_enabled":"Aktywacja przy resetowaniu hasła za pomocą formularza pocztowego",
"captcha_register_enabled":"Aktywacja w formularzu rejestracji",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maksimum kilit süresi",
"bruteForceProtectionTempo":"Kilit süresi",
"cancel":"İptal Et",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Giriş formunda aktivasyon",
"captcha_mail_enabled":"E-posta formu tarafından parola sıfırlamada aktivasyon",
"captcha_register_enabled":"Kayıt formunda aktivasyon",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"Lock time",
"cancel":"Hủy",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"Kích hoạt ở dạng đăng nhập",
"captcha_mail_enabled":"Kích hoạt đặt lại mật khẩu bằng biểu mẫu thư",
"captcha_register_enabled":"Kích hoạt trong biểu mẫu đăng ký",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"鎖時間",
"cancel":"取消",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":" 登录激活",
"captcha_mail_enabled":"通过邮件进行密码重置 激活",
"captcha_register_enabled":"注册 激活",

View File

@ -131,6 +131,9 @@
"bruteForceProtectionMaxLockTime":"Maximum lock time",
"bruteForceProtectionTempo":"鎖時間",
"cancel":"取消",
"captcha":"Captcha module",
"captchaCustom":"Custom Captcha module",
"captchaOptions":"Captcha module options",
"captcha_login_enabled":"在登入表單中啟用",
"captcha_mail_enabled":"透過郵件表單啟用密碼重設",
"captcha_register_enabled":"在註冊表單中啟用",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,8 @@ lib/Lemonldap/NG/Portal/Auth/Slave.pm
lib/Lemonldap/NG/Portal/Auth/SSL.pm
lib/Lemonldap/NG/Portal/Auth/Twitter.pm
lib/Lemonldap/NG/Portal/Auth/WebID.pm
lib/Lemonldap/NG/Portal/Captcha.pod
lib/Lemonldap/NG/Portal/Captcha/SecurityImage.pm
lib/Lemonldap/NG/Portal/CDC.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/Custom.pm
lib/Lemonldap/NG/Portal/CertificateResetByMail/Demo.pm

View File

@ -44,7 +44,7 @@ sub init {
my $self = shift;
if ( $self->{conf}->{captcha_login_enabled} ) {
$self->captcha( $self->p->loadModule('::Lib::Captcha') ) or return 0;
$self->captcha(1);
}
else {
$self->ott( $self->p->loadModule('::Lib::OneTimeToken') ) or return 0;
@ -117,18 +117,15 @@ sub extractFormInfo {
}
if ( $self->captcha ) {
my $code = $req->param('captcha');
unless ($code) {
$self->captcha->setCaptcha($req);
return PE_CAPTCHAEMPTY;
my $result = $self->p->_captcha->check_captcha($req);
if ($result) {
$self->logger->debug("Captcha code verified");
}
unless ( $self->captcha->validateCaptcha( $token, $code ) ) {
$self->captcha->setCaptcha($req);
$self->userLogger->warn(
"Captcha failed: wrong or expired code");
else {
$self->p->_captcha->init_captcha($req);
$self->userLogger->warn("Captcha failed");
return PE_CAPTCHAERROR;
}
$self->logger->debug("Captcha code verified");
}
elsif ( $self->ottRule->( $req, {} ) ) {
unless ( $req->data->{tokenVerified}
@ -179,7 +176,7 @@ sub setSecurity {
# If captcha is enable, prepare it
if ( $self->captcha ) {
$self->captcha->setCaptcha($req);
$self->p->_captcha->init_captcha($req);
}
# Else get token

View File

@ -0,0 +1,153 @@
=pod
=encoding utf8
=head1 NAME
Lemonldap:NG::Portal::Captcha - Writing CAPTCHA modules for LemonLDAP::NG.
=head1 SYNOPSIS
package Lemonldap::NG::Portal::Captcha::My;
use strict;
use Mouse;
# Add constants used by this module
our $VERSION = '0.1';
extends 'Lemonldap::NG::Portal::Main::Plugin';
sub init {
my $self = shift;
return 1;
}
sub init_captcha {
my ( $self, $req ) = @_;
# Read option from the manager configuration
my $option = $self->conf->{captchaOptions}->{option1};
# This can be used to inject custom JS code at the beginning
# of the page
my $script = <your html code>;
$req->data->{customScript} .= $script;
# This will add your custom HTML code to the protected form
my $html = <your html code>;
$req->captchaHtml($html);
}
sub check_captcha {
my ( $self, $req ) = @_;
my $captcha_input = $req->param('some_post_param');
my $is_captcha_valid = <your code here>;
if($is_captcha_valid) {
return 1;
} else {
return 0;
}
}
1;
=head1 DESCRIPTION
Captcha modules only need to implement two methods: one for initializing the
challenge, before the form is displayed, and the other to verify that the
submitted response is correct.
=head1 METHODS
=head2 Accessors and methods provided by Lemonldap::NG::Portal::Main::Plugin
=over
=item p: portal object
=item conf: configuration hash (as reference)
=item logger alias for p->logger accessor
=item userLogger alias for p->userLogger accessor
=item error: alias for p->error method
=back
=head2 "Routes" management
Like each module that inherits from Lemonldap::NG::Portal::Plugin,
you can define dedicated routes in a Captcha plugin.
=over
=item addAuthRoute: wrapper to L<Lemonldap::NG::Handler::PSGI::Try>
addAuthRoute() method
=item addUnauthRoute: wrapper to L<Lemonldap::NG::Handler::PSGI::Try>
addUnauthRoute() method
=back
=head2 Methods that must be provided by a Captcha module
=head3 init_captcha($req)
This method is called when the protected form is built by LemonLDAP::NG.
Your responsibility is to do any preparatory step, and provide LemonLDAP::NG
with the HTML code that it has to display in the form to enable the Captcha.
This is done by setting C<$req-E<gt>captchaHtml>
=head3 check_captcha($req)
This method is called after the user submitted the protected form. Your
responibility is to check the user's response (usually provided as a POST
field), and return 0 if the response is incorrect, 1 if the response is
correct.
=head1 LOGGING
Logging is provided by C<$self-E<gt>logger> and C<$self-E<gt>userLogger>. See
L<Lemonldap::NG::Portal::Main::Plugin> for a detailed description of logging levels.
=head1 AUTHORS
=over
=item LemonLDAP::NG team L<http://lemonldap-ng.org/team>
=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
See COPYING file for details.
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,189 @@
package Lemonldap::NG::Portal::Captcha::SecurityImage;
use strict;
use Mouse;
use MIME::Base64;
use GD::SecurityImage use_magick => 1;
our $VERSION = '2.0.12';
extends 'Lemonldap::NG::Portal::Main::Plugin';
has width => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaWidth} || 220 }
);
has height => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaHeight} || 40 }
);
has lines => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaLines} || 5 }
);
has scramble => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaScramble} || 1 }
);
has fgColor => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaFg} || '#403030' }
);
has bgColor => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaBg} || '#FF644B' }
);
has rndmax => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captcha_size} || 6 }
);
has timeout => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{formTimeout} }
);
has ott => (
is => 'rw',
lazy => 1,
default => sub {
my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
$ott->timeout( $_[0]->timeout );
return $ott;
}
);
sub init {
my ($self) = @_;
if ( $self->conf->{captcha_mail_enabled}
|| $self->conf->{captcha_login_enabled}
|| $self->conf->{captcha_register_enabled} )
{
$self->addUnauthRoute( renewcaptcha => '_sendCaptcha', ['GET'] );
}
return 1;
}
# Internal methods
sub _getCaptcha {
my ($self) = @_;
my $image = GD::SecurityImage->new(
width => $self->width,
height => $self->height,
lines => $self->lines,
gd_font => 'Giant',
scramble => $self->scramble,
rndmax => $self->rndmax,
);
$image->random;
$image->create( 'normal', 'default', $self->fgColor, $self->bgColor );
my ( $imageData, $mimeType, $rdm ) = $image->out( force => 'png' );
my $img = 'data:image/png;base64,' . encode_base64( $imageData, '' );
my $token = $self->ott->createToken( { captcha => $rdm } );
return ( $token, $img );
}
sub _sendCaptcha {
my ( $self, $req ) = @_;
$self->logger->info("User request for captcha renew");
my ( $token, $image ) = $self->_getCaptcha($req);
return $self->p->sendJSONresponse( $req,
{ newtoken => $token, newimage => $image } );
}
sub _validate_captcha_token {
my ( $self, $token, $value ) = @_;
my $s = $self->ott->getToken($token);
unless ($s) {
$self->logger->warn("Captcha token $token isn't valid");
return 0;
}
unless ( $s->{captcha} eq $value ) {
$self->logger->notice('Bad captcha response');
return 0;
}
$self->logger->debug('Good captcha response');
return 1;
}
sub _get_captcha_html {
my ( $self, $req, $src ) = @_;
my $sp = $self->p->staticPrefix;
$sp =~ s/\/*$/\//;
return $self->loadTemplate(
$req,
'captcha',
params => {
STATIC_PREFIX => $sp,
CAPTCHA_SRC => $src,
CAPTCHA_SIZE => $self->rndmax,
}
);
}
# New API
sub check_captcha {
my ( $self, $req ) = @_;
my $token = $req->param('token');
unless ($token) {
$self->logger->warn("No token provided for Captcha::SecurityImage");
return 0;
}
my $value = $req->param('captcha');
unless ($value) {
$self->logger->warn("No response provided for Captcha::SecurityImage");
return 0;
}
return $self->_validate_captcha_token( $token, $value );
}
sub init_captcha {
my ( $self, $req ) = @_;
my ( $token, $image ) = $self->_getCaptcha;
$self->logger->debug('Prepare captcha');
$req->token($token);
$req->captchaHtml( $self->_get_captcha_html( $req, $image ) );
# DEPRECATED: Compatibility with old templates
$req->captcha($image);
}
# #######
# Old API
# TODO: Remove this in 3.0
# #######
sub validateCaptcha {
my ( $self, $token, $value ) = @_;
return $self->_validate_captcha_token( $token, $value );
}
sub setCaptcha {
my ( $self, $req ) = @_;
$self->init_captcha($req);
}
sub sendCaptcha {
my ( $self, $req ) = @_;
return $self->_sendCaptcha($req);
}
sub getCaptcha {
my ( $self, $req ) = @_;
return $self->_getCaptcha($req);
}
1;

View File

@ -1,109 +1,40 @@
package Lemonldap::NG::Portal::Lib::Captcha;
# Old Captcha API, this is only a wrapper around Captcha::SecurityImage
use strict;
use Mouse;
use MIME::Base64;
use GD::SecurityImage use_magick => 1;
our $VERSION = '2.0.12';
extends 'Lemonldap::NG::Common::Module';
has width => (
has module => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaWidth} || 220 }
);
has height => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaHeight} || 40 }
);
has lines => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaLines} || 5 }
);
has scramble => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaScramble} || 1 }
);
has fgColor => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaFg} || '#403030' }
);
has bgColor => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captchaBg} || '#FF644B' }
);
has rndmax => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{captcha_size} || 6 }
);
has timeout => (
is => 'rw',
lazy => 1,
default => sub { $_[0]->{conf}->{formTimeout} }
);
has ott => (
is => 'rw',
lazy => 1,
default => sub {
my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
$ott->timeout( $_[0]->timeout );
return $ott;
}
handles => [
qw(setCaptcha validateCaptcha getCaptcha ott width height lines scramble fgColor bgColor rndmax timeout )
]
);
sub init {
return 1;
}
# Returns secret + a HTML image src content
sub getCaptcha {
my ($self) = @_;
my $image = GD::SecurityImage->new(
width => $self->width,
height => $self->height,
lines => $self->lines,
gd_font => 'Giant',
scramble => $self->scramble,
rndmax => $self->rndmax,
);
$image->random;
$image->create( 'normal', 'default', $self->fgColor, $self->bgColor );
my ( $imageData, $mimeType, $rdm ) = $image->out( force => 'png' );
my $img = 'data:image/png;base64,' . encode_base64( $imageData, '' );
my $token = $self->ott->createToken( { captcha => $rdm } );
return ( $token, $img );
}
sub validateCaptcha {
my ( $self, $token, $value ) = @_;
my $s = $self->ott->getToken($token);
unless ($s) {
$self->logger->warn("Captcha token $token isn't valid");
if ( $self->conf->{captcha} ) {
$self->logger->error( "The Lib::Captcha API is not compatible"
. " with custom Captcha module" );
return 0;
}
unless ( $s->{captcha} eq $value ) {
$self->logger->notice('Bad captcha response');
return 0;
else {
my $module = $self->p->loadModule("::Captcha::SecurityImage");
if ($module) {
$self->module($module);
return 1;
}
else {
return 0;
}
}
$self->logger->debug('Good captcha response');
return 1;
}
sub setCaptcha {
my ( $self, $req ) = @_;
my ( $token, $image ) = $self->getCaptcha;
$self->logger->debug('Prepare captcha');
$req->token($token);
$req->captcha($image);
}
1;

View File

@ -446,6 +446,15 @@ sub display {
}
# Display captcha if it's enabled
if ( $req->captchaHtml ) {
%templateParams =
( %templateParams, CAPTCHA_HTML => $req->captchaHtml, );
}
if ( $req->token ) {
%templateParams = ( %templateParams, TOKEN => $req->token, );
}
# DEPRECATED: This is only used for compatibility with existing templates
if ( $req->captcha ) {
%templateParams = (
%templateParams,
@ -453,9 +462,6 @@ sub display {
CAPTCHA_SIZE => $self->{conf}->{captcha_size} || 6
);
}
if ( $req->token ) {
%templateParams = ( %templateParams, TOKEN => $req->token, );
}
# Show password form if password policy error
if (

View File

@ -30,6 +30,7 @@ has _authentication => ( is => 'rw' );
has _userDB => ( is => 'rw' );
has _passwordDB => ( is => 'rw' );
has _sfEngine => ( is => 'rw' );
has _captcha => ( is => 'rw' );
has loadedModules => ( is => 'rw' );
@ -336,6 +337,14 @@ sub reloadConf {
unless $self->{_sfEngine} =
$self->loadPlugin( $self->conf->{'sfEngine'} );
# Load Captcha module
return $self->fail
unless $self->_captcha(
$self->loadPlugin(
$self->conf->{'captcha'} || '::Captcha::SecurityImage'
)
);
# Compile macros in _macros, groups in _groups
foreach my $type (qw(macros groups)) {
$self->{"_$type"} = {};

View File

@ -74,8 +74,10 @@ has lockTime => ( is => 'rw' );
# Security
#
# Captcha
has captcha => ( is => 'rw' );
# Captcha HTML code to display in forms
has captchaHtml => ( is => 'rw' );
# DEPRECATED: 2.0 captcha compatibility
has captcha => ( is => 'rw' );
# Token
has token => ( is => 'rw' );

View File

@ -98,7 +98,7 @@ sub init {
# Initialize Captcha if needed
if ( $self->conf->{captcha_mail_enabled} ) {
$self->captcha( $self->p->loadModule('::Lib::Captcha') ) or return 0;
$self->captcha(1);
}
# Load registered module
@ -179,46 +179,32 @@ sub _certificateReset {
# Use submitted value
$req->{user} = $req->param('mail');
# Check if token exists
my $token;
if ( $self->ottRule->( $req, {} ) or $self->captcha ) {
$token = $req->param('token');
# Captcha for register form
if ( $self->captcha ) {
my $result = $self->p->_captcha->check_captcha($req);
if ($result) {
$self->logger->debug("Captcha code verified");
}
else {
$self->setSecurity($req);
$self->userLogger->warn("Captcha failed");
return PE_CAPTCHAERROR;
}
}
elsif ( $self->ottRule->( $req, {} ) ) {
my $token = $req->param('token');
unless ($token) {
$self->setSecurity($req);
$self->userLogger->warn('Reset try without token');
return PE_NOTOKEN;
}
}
# Captcha for register form
if ( $self->captcha ) {
my $captcha = $req->param('captcha');
unless ($captcha) {
$self->userLogger->notice('Reset try with captcha not filled');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAEMPTY;
}
# Check captcha
unless ( $self->captcha->validateCaptcha( $token, $captcha ) ) {
$self->userLogger->info('Captcha failed: wrong code');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAERROR;
}
$self->logger->debug('Captcha code verified');
}
elsif ( $self->ottRule->( $req, {} ) ) {
unless ( $self->ott->getToken($token) ) {
$self->setSecurity($req);
$self->userLogger->warn('Reset try with expired/bad token');
return PE_TOKENEXPIRED;
}
}
unless ( $req->{user} =~ /$self->{conf}->{userControl}/o ) {
$self->setSecurity($req);
return PE_MALFORMEDUSER;
@ -581,9 +567,11 @@ sub modifyCertificate {
sub setSecurity {
my ( $self, $req ) = @_;
if ( $self->captcha ) {
$self->captcha->setCaptcha($req);
$self->p->_captcha->init_captcha($req);
}
elsif ( $self->ottRule->( $req, {} ) ) {
$self->ott->setToken($req);
}
@ -608,9 +596,13 @@ sub display {
STARTMAILDATE => $req->data->{startMailDate},
STARTMAILTIME => $req->data->{startMailTime},
MAILALREADYSENT => $req->data->{mailAlreadySent},
MAIL => (
$self->p->checkXSSAttack( 'mail', $req->{user} )
? ''
(
$req->data->{customScript}
? ( CUSTOM_SCRIPT => $req->data->{customScript} )
: ()
),
MAIL => (
$self->p->checkXSSAttack( 'mail', $req->{user} ) ? ''
: $req->{user}
),
DISPLAY_FORM => 0,
@ -627,14 +619,19 @@ sub display {
}
# Display captcha if enabled
if ( $req->captcha ) {
$tplPrm{CAPTCHA_SRC} = $req->captcha;
$tplPrm{CAPTCHA_SIZE} = $self->conf->{captcha_size};
if ( $req->captchaHtml ) {
$tplPrm{CAPTCHA_HTML} = $req->captchaHtml;
}
if ( $req->token ) {
$tplPrm{TOKEN} = $req->token;
}
# DEPRECATED: This is only used for compatibility with existing templates
if ( $req->captcha ) {
$tplPrm{CAPTCHA_SRC} = $req->captcha;
$tplPrm{CAPTCHA_SIZE} = $self->conf->{captcha_size};
}
# Display form the first time
if ( (
$req->error == PE_MAILFORMEMPTY

View File

@ -71,7 +71,7 @@ sub init {
# Initialize Captcha if needed
if ( $self->conf->{captcha_mail_enabled} ) {
$self->captcha( $self->p->loadModule('::Lib::Captcha') ) or return 0;
$self->captcha(1);
}
# Parse password policy activation rule
@ -154,46 +154,32 @@ sub _reset {
# Use submitted value
$req->{user} = $req->param('mail');
# Check if token exists
my $token;
if ( $self->ottRule->( $req, {} ) or $self->captcha ) {
$token = $req->param('token');
# Captcha for register form
if ( $self->captcha ) {
my $result = $self->p->_captcha->check_captcha($req);
if ($result) {
$self->logger->debug("Captcha code verified");
}
else {
$self->setSecurity($req);
$self->userLogger->warn("Captcha failed");
return PE_CAPTCHAERROR;
}
}
elsif ( $self->ottRule->( $req, {} ) ) {
my $token = $req->param('token');
unless ($token) {
$self->setSecurity($req);
$self->userLogger->warn('Reset try without token');
return PE_NOTOKEN;
}
}
# Captcha for register form
if ( $self->captcha ) {
my $captcha = $req->param('captcha');
unless ($captcha) {
$self->userLogger->notice('Reset try with captcha not filled');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAEMPTY;
}
# Check captcha
unless ( $self->captcha->validateCaptcha( $token, $captcha ) ) {
$self->userLogger->info('Captcha failed: wrong code');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAERROR;
}
$self->logger->debug('Captcha code verified');
}
elsif ( $self->ottRule->( $req, {} ) ) {
unless ( $self->ott->getToken($token) ) {
$self->setSecurity($req);
$self->userLogger->warn('Reset try with expired/bad token');
return PE_TOKENEXPIRED;
}
}
unless ( $req->{user} =~ /$self->{conf}->{userControl}/o ) {
$self->setSecurity($req);
return PE_MALFORMEDUSER;
@ -583,7 +569,7 @@ sub changePwd {
sub setSecurity {
my ( $self, $req ) = @_;
if ( $self->captcha ) {
$self->captcha->setCaptcha($req);
$self->p->_captcha->init_captcha($req);
}
elsif ( $self->ottRule->( $req, {} ) ) {
$self->ott->setToken($req);
@ -623,6 +609,11 @@ sub display {
STARTMAILDATE => $req->data->{startMailDate},
STARTMAILTIME => $req->data->{startMailTime},
MAILALREADYSENT => $req->data->{mailAlreadySent},
(
$req->data->{customScript}
? ( CUSTOM_SCRIPT => $req->data->{customScript} )
: ()
),
MAIL => (
$self->p->checkXSSAttack( 'mail', $req->{user} )
? ''
@ -653,14 +644,19 @@ sub display {
}
# Display captcha if it's enabled
if ( $req->captcha ) {
$tplPrm{CAPTCHA_SRC} = $req->captcha;
$tplPrm{CAPTCHA_SIZE} = $self->conf->{captcha_size};
if ( $req->captchaHtml ) {
$tplPrm{CAPTCHA_HTML} = $req->captchaHtml;
}
if ( $req->token ) {
$tplPrm{TOKEN} = $req->token;
}
# DEPRECATED: This is only used for compatibility with existing templates
if ( $req->captcha ) {
$tplPrm{CAPTCHA_SRC} = $req->captcha;
$tplPrm{CAPTCHA_SIZE} = $self->conf->{captcha_size};
}
# Display form the first time
if ( (
$req->error == PE_MAILFORMEMPTY

View File

@ -18,7 +18,6 @@
# * DELETE /sessions/my : ask for global logout (if GlobalLogout plugin is on)
#
# - Authentication
# * GET /renewcaptcha : get token and captcha image
# * POST /sessions/<type>/<session-id>?auth : authenticate with a fixed
# sessionId
# * Note that the "getCookie" method (authentification via SOAP) exists for
@ -71,7 +70,6 @@ our $VERSION = '2.0.14';
extends qw(
Lemonldap::NG::Portal::Main::Plugin
Lemonldap::NG::Portal::Lib::Captcha
);
has configStorage => (
@ -116,7 +114,6 @@ has exportedAttr => (
}
}
);
has captcha => ( is => 'rw' );
has ott => (
is => 'rw',
lazy => 1,
@ -134,13 +131,6 @@ sub init {
my ($self) = @_;
my @parents = ('Lemonldap::NG::Portal::Main::Plugin');
my $add = 0;
if ( $self->conf->{captcha_mail_enabled}
|| $self->conf->{captcha_login_enabled}
|| $self->conf->{captcha_register_enabled} )
{
$self->captcha( $self->p->loadModule('::Lib::Captcha') ) or return 0;
$self->addUnauthRoute( renewcaptcha => 'sendCaptcha', ['GET'] );
}
if ( $self->conf->{restConfigServer} ) {
push @parents, 'Lemonldap::NG::Common::Conf::RESTServer';
$add++;
@ -641,15 +631,6 @@ sub removeSessions {
return $self->p->sendJSONresponse( $req, { result => $nbr } );
}
sub sendCaptcha {
my ( $self, $req ) = @_;
$self->logger->info("User request for captcha renew");
my ( $token, $image ) = $self->captcha->getCaptcha($req);
return $self->p->sendJSONresponse( $req,
{ newtoken => $token, newimage => $image } );
}
sub pwdReset {
my ( $self, $req ) = @_;

View File

@ -76,7 +76,7 @@ sub init {
# Initialize Captcha if needed
if ( $self->conf->{captcha_register_enabled} ) {
$self->captcha( $self->p->loadModule('::Lib::Captcha') ) or return 0;
$self->captcha(1);
}
# Initialize form token if needed (captcha provides also a token)
@ -168,44 +168,28 @@ sub _register {
!$self->getRegisterSession( $req->data->{registerInfo}->{mail} ) )
{
# Check if token exists
my $token;
if ( $self->ottRule->( $req, {} ) or $self->captcha ) {
$token = $req->param('token');
# Captcha for register form
if ( $self->captcha ) {
my $result = $self->p->_captcha->check_captcha($req);
if ($result) {
$self->logger->debug("Captcha code verified");
}
else {
$self->setSecurity($req);
$self->userLogger->warn("Captcha failed");
return PE_CAPTCHAERROR;
}
}
elsif ( $self->ottRule->( $req, {} ) ) {
my $token = $req->param('token');
unless ($token) {
$self->setSecurity($req);
$self->userLogger->warn('Register try without token');
return PE_NOTOKEN;
}
}
# Captcha for register form
if ( $self->captcha ) {
my $captcha = $req->param('captcha');
unless ($captcha) {
$self->userLogger->warn(
'Register try with captcha not filled');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAEMPTY;
}
# Check captcha
unless ( $self->captcha->validateCaptcha( $token, $captcha ) ) {
$self->userLogger->info('Captcha failed: wrong code');
# Set captcha or token
$self->setSecurity($req);
return PE_CAPTCHAERROR;
}
$self->logger->debug("Captcha code verified");
}
elsif ( $self->ottRule->( $req, {} ) ) {
unless ( $self->ott->getToken($token) ) {
$self->setSecurity($req);
$self->userLogger->notice(
$self->userLogger->warn(
'Register try with expired/bad token');
return PE_TOKENEXPIRED;
}
@ -445,7 +429,12 @@ sub display {
STARTMAILDATE => $req->data->{startMailDate},
STARTMAILTIME => $req->data->{startMailTime},
MAILALREADYSENT => $req->data->{mail_already_sent},
MAIL => $self->p->checkXSSAttack(
(
$req->data->{customScript}
? ( CUSTOM_SCRIPT => $req->data->{customScript} )
: ()
),
MAIL => $self->p->checkXSSAttack(
'mail', $req->data->{registerInfo}->{mail}
) ? ""
: $req->data->{registerInfo}->{mail},
@ -483,14 +472,19 @@ sub display {
}
# Display captcha if it's enabled
if ( $req->captcha ) {
$templateParams{CAPTCHA_SRC} = $req->captcha;
$templateParams{CAPTCHA_SIZE} = $self->conf->{captcha_size} || 6;
if ( $req->captchaHtml ) {
$templateParams{CAPTCHA_HTML} = $req->captchaHtml;
}
if ( $req->token ) {
$templateParams{TOKEN} = $req->token;
}
# DEPRECATED: This is only used for compatibility with existing templates
if ( $req->captcha ) {
$templateParams{CAPTCHA_SRC} = $req->captcha;
$templateParams{CAPTCHA_SIZE} = $self->conf->{captcha_size};
}
if ( $req->error == PE_REGISTERALREADYEXISTS ) {
%templateParams = (
%templateParams,
@ -560,7 +554,7 @@ sub display {
sub setSecurity {
my ( $self, $req ) = @_;
if ( $self->captcha ) {
$self->captcha->setCaptcha($req);
$self->p->_captcha->init_captcha($req);
}
elsif ( $self->ottRule->( $req, {} ) ) {
$self->ott->setToken($req);

View File

@ -28,16 +28,8 @@
<input id="mailfield" name="mail" type="text" value="<TMPL_VAR NAME="MAIL">" class="form-control" trplaceholder="mail" required />
</div>
<TMPL_IF NAME=CAPTCHA_SRC>
<div class="form-group">
<img src="<TMPL_VAR NAME=CAPTCHA_SRC>" class="img-thumbnail" />
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text"><label for="captchafield" class="mb-0"><i class="fa fa-eye"></i></label></span>
</div>
<input id="captchafield" type="text" name="captcha" size="<TMPL_VAR NAME=CAPTCHA_SIZE>" class="form-control" trplaceholder="captcha" required autocomplete="off" />
</div>
<TMPL_IF NAME=CAPTCHA_HTML>
<TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
<TMPL_IF NAME="TOKEN">
<input type="hidden" name="token" value="<TMPL_VAR NAME="TOKEN">" />

View File

@ -15,8 +15,8 @@
<textarea id="passwordfield" name="password" class="form-control" trplaceholder="Signed text" required aria-required="true"></textarea>
</div>
<TMPL_IF NAME=CAPTCHA_SRC>
<TMPL_INCLUDE NAME="captcha.tpl">
<TMPL_IF NAME=CAPTCHA_HTML>
<TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
<input id="token" type="hidden" name="token" value="<TMPL_VAR NAME="TOKEN">" />

View File

@ -71,8 +71,8 @@
<span trspan="resentConfirm">Do you want the confirmation mail to be resent?</span>
</p>
<TMPL_IF NAME=CAPTCHA_SRC>
<TMPL_INCLUDE NAME="captcha.tpl">
<TMPL_IF NAME=CAPTCHA_HTML>
<TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
<div class="input-group mb-3">

View File

@ -40,8 +40,8 @@
<input id="mailfield" name="mail" type="text" value="<TMPL_VAR NAME="MAIL">" class="form-control" trplaceholder="mail" required aria-required="true" autocomplete="email" />
</div>
<TMPL_IF NAME=CAPTCHA_SRC>
<TMPL_INCLUDE NAME="captcha.tpl">
<TMPL_IF NAME=CAPTCHA_HTML>
<TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
<TMPL_IF NAME="TOKEN">
<input id="token" type="hidden" name="token" value="<TMPL_VAR NAME="TOKEN">" />

View File

@ -33,8 +33,8 @@
</TMPL_IF>
</div>
<TMPL_IF NAME=CAPTCHA_SRC>
<TMPL_INCLUDE NAME="captcha.tpl">
<TMPL_IF NAME=CAPTCHA_HTML>
<TMPL_VAR NAME=CAPTCHA_HTML>
</TMPL_IF>
<TMPL_INCLUDE NAME="impersonation.tpl">

View File

@ -0,0 +1,114 @@
use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants 'PE_CAPTCHAEMPTY';
require 't/test-lib.pm';
my $res;
my $maintests = 0;
SKIP: {
eval 'use GD::SecurityImage; use Image::Magick;';
if ($@) {
skip 'Image::Magick not found', $maintests;
}
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
portalMainLogo => 'common/logos/logo_llng_old.png',
customPlugins => 't::CaptchaOldApi',
}
}
);
my ( $data, $json );
# check setCaptcha
$data = '';
$json = expectJSON(
$client->_post(
'/setCaptcha',
IO::String->new($data),
length => length($data),
)
);
like( $json->{token}, qr/.+/ );
like( $json->{img}, qr#^data:image/png;base64,.{10}# );
like( $json->{answer}, qr#^\d{6}$# );
count(3);
# check getCaptcha
$data = '';
$json = expectJSON(
$client->_post(
'/getCaptcha',
IO::String->new($data),
length => length($data),
)
);
like( $json->{token}, qr/.+/ );
like( $json->{img}, qr#^data:image/png;base64,.{10}# );
like( $json->{answer}, qr#^\d{6}$# );
count(3);
my $token = $json->{token};
my $answer = $json->{answer};
# validate: wrong token
$data = buildForm( { token => 111, answer => $answer } );
$json = expectJSON(
$client->_post(
'/validateCaptcha', IO::String->new($data),
length => length($data),
)
);
is( $json->{result}, 0, 'Wrong token failed' );
count(1);
# validate: wrong answer
$data = buildForm( { token => $token, answer => 999 } );
$json = expectJSON(
$client->_post(
'/validateCaptcha', IO::String->new($data),
length => length($data),
)
);
is( $json->{result}, 0, 'Wrong captcha failed' );
count(1);
# Get Fresh token/answer pair
$data = '';
$json = expectJSON(
$client->_post(
'/getCaptcha',
IO::String->new($data),
length => length($data),
)
);
like( $json->{token}, qr/.+/ );
like( $json->{img}, qr#^data:image/png;base64,.{10}# );
like( $json->{answer}, qr#^\d{6}$# );
count(3);
$token = $json->{token};
$answer = $json->{answer};
# validate: correct values
$data = buildForm( { token => $token, answer => $answer } );
$json = expectJSON(
$client->_post(
'/validateCaptcha', IO::String->new($data),
length => length($data),
)
);
is( $json->{result}, 1, 'Captcha successfully verified' );
count(1);
}
count($maintests);
clean_sessions();
done_testing( count() );

View File

@ -2,7 +2,7 @@ use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants 'PE_CAPTCHAEMPTY';
use Lemonldap::NG::Portal::Main::Constants 'PE_CAPTCHAERROR';
require 't/test-lib.pm';
@ -42,8 +42,8 @@ SKIP: {
my $json;
ok( $json = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' )
or print STDERR "$@\n" . Dumper($res);
ok( $json->{error} == PE_CAPTCHAEMPTY, 'Response is PE_CAPTCHAEMPTY' )
or explain( $json, "error => 77" );
ok( $json->{error} == PE_CAPTCHAERROR, 'Response is PE_CAPTCHAERROR' )
or explain( $json, "error => 76" );
# Test normal first access
# ------------------------

View File

@ -0,0 +1,57 @@
package t::CaptchaOldApi;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants;
extends 'Lemonldap::NG::Portal::Main::Plugin';
has 'captcha' => ( is => 'rw' );
sub init {
my $self = shift;
$self->addUnauthRoute( validateCaptcha => 'validateCaptcha', ['POST'] );
$self->addUnauthRoute( setCaptcha => 'setCaptcha', ['POST'] );
$self->addUnauthRoute( getCaptcha => 'getCaptcha', ['POST'] );
$self->captcha( $self->p->loadModule('::Lib::Captcha') );
return 1;
}
sub setCaptcha {
my ( $self, $req ) = @_;
$self->captcha->setCaptcha($req);
my $info = $self->captcha->ott->getToken( $req->token, 1 );
return $self->sendJSONresponse(
$req,
{
token => $req->token,
img => $req->captcha,
answer => $info->{captcha}
}
);
}
sub getCaptcha {
my ( $self, $req ) = @_;
my ( $token, $image ) = $self->captcha->getCaptcha;
my $info = $self->captcha->ott->getToken( $token, 1 );
return $self->sendJSONresponse( $req,
{ token => $token, img => $image, answer => $info->{captcha} } );
}
sub validateCaptcha {
my ( $self, $req ) = @_;
my $token = $req->param('token');
my $answer = $req->param('answer');
my $result = $self->captcha->validateCaptcha( $token, $answer );
return $self->sendJSONresponse( $req, { result => $result } );
}
1;