Commit 23455138 authored by Xavier Guimard's avatar Xavier Guimard

Captcha and token in progress (#1140)

parent 81b47b79
......@@ -23,9 +23,9 @@ use constant HANDLERSECTION => "handler";
use constant MANAGERSECTION => "manager";
use constant SESSIONSEXPLORERSECTION => "sessionsExplorer";
use constant APPLYSECTION => "apply";
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|ca(?:s(?:StorageOption|Attribute)|ptchaStorageOption)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|CAS_proxiedService|macro)s|o(?:idc(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost))$/;
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|cas(?:StorageOption|Attribute)|CAS_proxiedService|macro)s|o(?:idc(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost))$/;
our @sessionTypes = ( 'captcha', 'remoteGlobal', 'cas', 'global', 'localSession', 'persistent', 'saml', 'oidc' );
our @sessionTypes = ( 'remoteGlobal', 'cas', 'global', 'localSession', 'persistent', 'saml', 'oidc' );
sub NO {qr/^(?:off|no|0)?$/i}
......
......@@ -20,25 +20,21 @@ sub defaultValues {
'browserIdAuthnLevel' => 1,
'captcha_register_enabled' => 1,
'captcha_size' => 6,
'captchaStorage' => 'Apache::Session::File',
'captchaStorageOptions' => {
'Directory' => '/var/lib/lemonldap-ng/captcha/'
},
'CAS_authnLevel' => 1,
'CAS_pgtFile' => '/tmp/pgt.txt',
'casAccessControlPolicy' => 'none',
'checkXSS' => 1,
'confirmFormMethod' => 'post',
'cookieName' => 'lemonldap',
'cspConnect' => '\'self\'',
'cspDefault' => '\'self\'',
'cspFont' => '\'self\'',
'cspImg' => '\'self\'',
'cspScript' => '\'self\'',
'cspStyle' => '\'self\'',
'dbiAuthnLevel' => 2,
'dbiExportedVars' => {},
'demoExportedVars' => {
'CAS_authnLevel' => 1,
'CAS_pgtFile' => '/tmp/pgt.txt',
'casAccessControlPolicy' => 'none',
'checkXSS' => 1,
'confirmFormMethod' => 'post',
'cookieName' => 'lemonldap',
'cspConnect' => '\'self\'',
'cspDefault' => '\'self\'',
'cspFont' => '\'self\'',
'cspImg' => '\'self\'',
'cspScript' => '\'self\'',
'cspStyle' => '\'self\'',
'dbiAuthnLevel' => 2,
'dbiExportedVars' => {},
'demoExportedVars' => {
'cn' => 'cn',
'mail' => 'mail',
'uid' => 'uid'
......@@ -50,6 +46,7 @@ sub defaultValues {
'facebookAuthnLevel' => 1,
'facebookExportedVars' => {},
'failedLoginNumber' => 5,
'formTimeout' => 120,
'globalStorage' => 'Apache::Session::File',
'globalStorageOptions' => {
'Directory' => '/var/lib/lemonldap-ng/sessions/',
......@@ -182,6 +179,7 @@ sub defaultValues {
'http://auth.example.com/Lemonldap/NG/Common/CGI/SOAPService',
'proxy' => 'http://auth.example.com/index.pl/sessions'
},
'requireToken' => 1,
'samlAttributeAuthorityDescriptorAttributeServiceSOAP' =>
'urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;',
'samlAuthnContextMapKerberos' => 4,
......
......@@ -20,7 +20,7 @@ our $specialNodeHash = {
};
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wpSslOpt)|ca(?:s(?:StorageOption|Attribute)|ptchaStorageOption)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|re(?:moteGlobalStorageOption|loadUrl)|CAS_proxiedService|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:uthChoiceModules|pplicationList))';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wpSslOpt)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|re(?:moteGlobalStorageOption|loadUrl)|cas(?:StorageOption|Attribute)|CAS_proxiedService|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:uthChoiceModules|pplicationList))';
our $specialNodeKeys = '(?:(?:saml(?:ID|S)|oidc[OR])PMetaDataNode|virtualHost)s';
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|S(?:toreIDToken|cope)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:I(?:DToken(?:Expiration|SignAlg)|con)|(?:RedirectUri|ExtraClaim)s|AccessTokenExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|UserIDAttr)|ExportedVars)';
......
......@@ -599,16 +599,6 @@ sub attributes {
'default' => 6,
'type' => 'int'
},
'captchaStorage' => {
'default' => 'Apache::Session::File',
'type' => 'PerlModule'
},
'captchaStorageOptions' => {
'default' => {
'Directory' => '/var/lib/lemonldap-ng/captcha/'
},
'type' => 'keyTextContainer'
},
'CAS_authnLevel' => {
'default' => 1,
'type' => 'int'
......@@ -883,6 +873,10 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
'default' => 5,
'type' => 'int'
},
'formTimeout' => {
'default' => 120,
'type' => 'int'
},
'globalStorage' => {
'default' => 'Apache::Session::File',
'type' => 'PerlModule'
......@@ -2097,6 +2091,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'remotePortal' => {
'type' => 'text'
},
'requireToken' => {
'default' => 1,
'type' => 'bool'
},
'restConfigServer' => {
'default' => 0,
'type' => 'bool'
......
......@@ -425,6 +425,16 @@ sub attributes {
},
# Security
formTimeout => {
default => 120,
type => 'int',
documentation => 'Token timeout for forms',
},
requireToken => {
default => 1,
type => 'bool',
documentation => 'Enable token for forms',
},
cda => {
default => 0,
type => 'bool',
......@@ -694,19 +704,6 @@ sub attributes {
documentation => 'Captcha size',
},
#captcha_data
#captcha_output
captchaStorage => {
type => 'PerlModule',
default => 'Apache::Session::File',
documentation => 'Captcha backend module',
},
captchaStorageOptions => {
type => 'keyTextContainer',
default => { 'Directory' => '/var/lib/lemonldap-ng/captcha/', },
documentation => 'Captcha backend module options',
},
# Variables
exportedVars => {
type => 'keyTextContainer',
......
......@@ -91,8 +91,6 @@ sub tree {
'captcha_mail_enabled',
'captcha_register_enabled',
'captcha_size',
'captchaStorage',
'captchaStorageOptions'
]
}
]
......@@ -615,7 +613,9 @@ sub tree {
'cspScript', 'cspStyle',
'cspConnect', 'cspFont',
]
}
},
'requireToken',
'formTimeout',
]
},
{
......
......@@ -93,8 +93,6 @@
"captcha_mail_enabled": "Activation in password reset by mail form",
"captcha_register_enabled": "Activation in register form",
"captcha_size": "Size",
"captchaStorage": "Captcha module name",
"captchaStorageOptions": "Captcha module options",
"CAS_authnLevel": "Authentication level",
"CAS_CAFile": "CA file",
"CAS_gateway": "Gateway authentication",
......@@ -202,6 +200,7 @@
"forceSave": "Force save",
"format": "Format",
"formReplay": "Form replay",
"formTimeout": "Form timeout",
"forms": "Forms",
"friendlyName": "Friendly name",
"generalParameters": "General Parameters",
......@@ -556,6 +555,7 @@
"remoteParams": "Remote parameters",
"remotePortal": "Portal URL",
"replaceByFile": "Replace by file",
"requireToken": "Require token for forms",
"restConfigServer": "REST configuration server",
"restSessionServer": "REST session server",
"restore": "Restore",
......
......@@ -93,8 +93,6 @@
"captcha_mail_enabled": "Activation dans le formulaire de réinitialisation par mail",
"captcha_register_enabled": "Activation dans le formulaire de création de compte",
"captcha_size": "Taille",
"captchaStorage": "Nom du module de stockage",
"captchaStorageOptions": "Options du module de stockage",
"CAS_authnLevel": "Niveau d'authentification",
"CAS_CAFile": "Fichier d'AC",
"CAS_gateway": "Authentification transparente",
......@@ -202,6 +200,7 @@
"forceSave": "Forcer la sauvegarde",
"format": "Format",
"formReplay": "Rejeu de formulaires",
"formTimeout": "Délai de validation pour les formulaires",
"forms": "Formulaires",
"friendlyName": "Nom alternatif",
"generalParameters": "Paramètres généraux",
......@@ -556,6 +555,7 @@
"remoteParams": "Paramètres Remote",
"remotePortal": "URL du portail",
"replaceByFile": "Remplacer par le fichier",
"requireToken": "Exige un jeton pour les formulaires",
"restConfigServer": "Serveur de configurations REST",
"restSessionServer": "Serveur de sessions REST",
"restore": "Restaurer",
......
......@@ -7,8 +7,16 @@ package Lemonldap::NG::Portal::Auth::_WebForm;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants
qw(PE_OK PE_FIRSTACCESS PE_FORMEMPTY PE_PASSWORDFORMEMPTY PE_CAPTCHAEMPTY PE_CAPTCHAERROR);
use Lemonldap::NG::Portal::Main::Constants qw(
PE_CAPTCHAEMPTY
PE_CAPTCHAERROR
PE_FIRSTACCESS
PE_FORMEMPTY
PE_NOTOKEN
PE_OK
PE_PASSWORDFORMEMPTY
PE_TOKENEXPIRED
);
our $VERSION = '2.0.0';
......@@ -22,10 +30,24 @@ has authnLevel => (
},
);
has captcha => ( is => 'rw' );
has ott => ( is => 'rw' );
# INITIALIZATION
sub init {
1;
if ( $_[0]->{conf}->{captcha_login_enabled} ) {
$_[0]->captcha(
$_[0]->p->loadModule('Lemonldap::NG::Portal::Lib::Captcha') )
or return 0;
}
elsif ( $_[0]->{conf}->{requireToken} ) {
$_[0]->ott(
$_[0]->p->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken') )
or return 0;
$_[0]->ott->timeout( $_[0]->conf->{formTimeout} );
}
return 1;
}
# RUNNING METHODS
......@@ -34,30 +56,27 @@ sub init {
sub extractFormInfo {
my ( $self, $req ) = @_;
# Init captcha
if ( $self->conf->{captcha_login_enabled} ) {
eval { $self->initCaptcha(); };
$self->lmLog( "Can't init captcha: $@", "error" ) if $@;
}
# Detect first access and empty forms
my $defUser = defined $req->param('user');
my $defPassword = defined $req->param('password');
my $defOldPassword = defined $req->param('oldpassword');
my $res = PE_OK;
# 1. No user defined at all -> first access
return PE_FIRSTACCESS unless $defUser;
unless ($defUser) {
$res = PE_FIRSTACCESS;
}
# 2. If user and password defined -> login form
if ( $defUser && $defPassword ) {
return PE_FORMEMPTY
elsif ( $defUser && $defPassword ) {
$res = PE_FORMEMPTY
unless ( ( $req->{user} = $req->param('user') )
&& ( $req->datas->{password} = $req->param('password') ) );
}
# 3. If user and oldpassword defined -> password form
if ( $defUser && $defOldPassword ) {
return PE_PASSWORDFORMEMPTY
elsif ( $defUser && $defOldPassword ) {
$res = PE_PASSWORDFORMEMPTY
unless ( ( $req->{user} = $req->param('user') )
&& ( $req->datas->{oldpassword} = $req->param('oldpassword') )
&& ( $req->datas->{newpassword} = $req->param('newpassword') )
......@@ -65,49 +84,48 @@ sub extractFormInfo {
$req->param('confirmpassword') ) );
}
# 4. Captcha for login form
if ( $self->conf->{captcha_login_enabled} && $defUser && $defPassword ) {
$req->datas->{captcha_user_code} = $req->param('captcha_user_code');
$req->datas->{captcha_check_code} = $req->param('captcha_code');
# If form seems empty
if ( $res != PE_OK ) {
unless ( $req->datas->{captcha_user_code}
&& $req->datas->{captcha_check_code} )
{
$self->lmLog( "Captcha not filled", 'warn' );
return PE_CAPTCHAEMPTY;
# If captcha is enable, prepare it
if ( $self->captcha ) {
$self->setCaptcha($req);
}
$self->lmLog(
"Captcha data received: "
. $req->datas->{captcha_user_code} . " and "
. $req->datas->{captcha_check_code},
'debug'
);
# Check captcha
my $captcha_result = $self->checkCaptcha(
$req->datas->{captcha_user_code},
$req->datas->{captcha_check_code}
);
if ( $captcha_result != 1 ) {
if ( $captcha_result == -3
or $captcha_result == -2 )
{
$self->lmLog( "Captcha failed: wrong code", 'warn' );
return PE_CAPTCHAERROR;
# Else get token
elsif ( $self->ott ) {
$self->setToken($req);
}
return $res;
}
# Security: check for captcha or token
if ( $self->captcha or $self->ott ) {
my $token;
unless ( $token = $req->param('token') ) {
$self->p->userError('Authentication tried without token');
return PE_NOTOKEN;
}
if ( $self->captcha ) {
my $code = $req->param('captcha');
unless ($code) {
$self->setCaptcha($req);
return PE_CAPTCHAEMPTY;
}
elsif ( $captcha_result == 0 ) {
$self->lmLog( "Captcha failed: code not checked (file error)",
'warn' );
unless ( $self->captcha->validateCaptcha( $token, $code ) ) {
$self->setCaptcha($req);
$self->p->userNotice("Captcha failed: wrong or expired code");
return PE_CAPTCHAERROR;
}
elsif ( $captcha_result == -1 ) {
$self->lmLog( "Captcha failed: code has expired", 'warn' );
return PE_CAPTCHAERROR;
$self->lmLog( "Captcha code verified", 'debug' );
}
elsif ( $self->ott ) {
unless ( $self->ott->getToken($token) ) {
$self->setToken($req);
$self->p->userNotice('Token expired');
return PE_TOKENEXPIRED;
}
}
$self->lmLog( "Captcha code verified", 'debug' );
}
# Other parameters
......@@ -141,4 +159,18 @@ sub getDisplayType {
return "standardform";
}
sub setCaptcha {
my ( $self, $req ) = @_;
my ( $token, $image ) = $self->captcha->getCaptcha;
$self->lmLog( 'Prepare captcha', 'debug' );
$req->token($token);
$req->captcha($image);
}
sub setToken {
my ( $self, $req ) = @_;
$self->lmLog( 'Prepare token', 'debug' );
$req->token( $self->ott->createToken );
}
1;
......@@ -83,6 +83,8 @@ use constant {
PE_REGISTERFIRSTACCESS => 78,
PE_REGISTERFORMEMPTY => 79,
PE_REGISTERALREADYEXISTS => 80,
PE_NOTOKEN => 81,
PE_TOKENEXPIRED => 82,
};
# EXPORTER PARAMETERS
......@@ -107,7 +109,7 @@ our @EXPORT_OK = qw( PE_SENDRESPONSE PE_INFO PE_REDIRECT PE_DONE PE_OK
PE_MAILNOTFOUND PE_PASSWORDFIRSTACCESS PE_MAILCONFIRMOK
PE_MUST_SUPPLY_OLD_PASSWORD PE_FORBIDDENIP PE_CAPTCHAERROR PE_CAPTCHAEMPTY
PE_REGISTERFIRSTACCESS PE_REGISTERFORMEMPTY PE_REGISTERALREADYEXISTS
HANDLER
PE_NOTOKEN PE_TOKENEXPIRED HANDLER
);
our %EXPORT_TAGS = ( 'all' => [ @EXPORT_OK, 'import' ], );
......
......@@ -56,6 +56,14 @@ has menuError => ( is => 'rw' );
# Frame flag (used by Run to not send Content-Security-Policy header)
has frame => ( is => 'rw' );
# Security
#
# Captcha
has captcha => ( is => 'rw' );
# Token
has token => ( is => 'rw' );
# Error type
sub error_type {
my $req = shift;
......
......@@ -73,6 +73,7 @@
"generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256"
},
"reloadUrls": {},
"requireToken": 0,
"userDB": "Demo",
"whatToTrace": "_whatToTrace"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment