Merge branch 'v2.0'

This commit is contained in:
Yadd 2021-06-19 18:47:01 +02:00
commit 051a8e4331
53 changed files with 918 additions and 148 deletions

View File

@ -64,7 +64,7 @@ build_centos_7:
script:
- rm -f /etc/yum.repos.d/CentOS-Sources.repo
- yum -y install epel-release
- make rpm-dist
- make dist
- ci-build-pkg
build_centos_8:
@ -74,7 +74,7 @@ build_centos_8:
- yum-config-manager --enable PowerTools
- yum-config-manager --enable AppStream
- yum -y install epel-release
- make rpm-dist
- make dist
- ci-build-pkg
sign:
@ -91,7 +91,7 @@ sign:
- build_buster
- build_bionic
- build_centos_7
# - build_centos_8
- build_centos_8
artifacts:
expire_in: 1 day
paths:

View File

@ -1085,23 +1085,13 @@ manager_uninstall: manager
dist: clean
@mkdir -p lemonldap-ng-$(VERSION)
@cp -pRH $$(find * -maxdepth 0|grep -v -e "\(lemonldap-ng-$(VERSION)\|debian\|rpm\)") lemonldap-ng-$(VERSION)
@cp -pRH $$(find * -maxdepth 0|grep -v -e "lemonldap-ng-$(VERSION)") lemonldap-ng-$(VERSION)
@find $$dir -name '*.bak' -delete
@rm -rf lemonldap-ng-$(VERSION)/lemonldap-ng-$(VERSION)
@rm -rf lemonldap-ng-$(VERSION)/node_modules
@$(COMPRESS) lemonldap-ng-$(VERSION).$(COMPRESSSUFFIX) lemonldap-ng-$(VERSION)
@rm -rf lemonldap-ng-$(VERSION)
rpm-dist: clean
@mkdir -p lemonldap-ng-$(VERSION)
@cp -pRH $$(find * -maxdepth 0|grep -v -e "\(lemonldap-ng-$(VERSION)\|debian\)") lemonldap-ng-$(VERSION)
@find $$dir -name '*.bak' -delete
@rm -rf lemonldap-ng-$(VERSION)/lemonldap-ng-$(VERSION)
@rm -rf lemonldap-ng-$(VERSION)/node_modules
@$(COMPRESS) lemonldap-ng-$(VERSION).$(COMPRESSSUFFIX) lemonldap-ng-$(VERSION)
@rm -rf lemonldap-ng-$(VERSION)
debian-dist: clean
@mkdir -p lemonldap-ng-$(VERSION)
@cp -pRH $$(find * -maxdepth 0|grep -v -e "\(lemonldap-ng-$(VERSION)\|rpm\)") lemonldap-ng-$(VERSION)

View File

@ -60,10 +60,7 @@ $ make clean && make dist
- RedHat packaging:
Create the RPM specific tarball:
$ make clean && make rpm-dist
Next steps: see rpm/README
See rpm/README
- Debian packaging:
@ -87,7 +84,7 @@ Upload modules tarballs (generated by make cpan)
- OW2 Release:
Upload dist and bundles on sftp://release-up.ow2.org/projects/lemonldap
- RPM: see rpm/REDAME
- RPM: see rpm/README
- DEB:
The DEB repository is hosted on https://lemonldap-ng.org/deb

View File

@ -895,6 +895,11 @@
"default" : "HS512",
"enum" : [ "none", "RS256", "RS384", "RS512" ]
},
"userInfoSignAlg" : {
"type" : "string",
"default" : "",
"enum" : [ "", "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" ]
},
"accessTokenJWT" : {
"type" : "bool"
},

View File

@ -34,6 +34,7 @@ Applications
applications/nextcloud
applications/obm
applications/office365
applications/publik
applications/phpldapadmin
applications/roundcube
applications/salesforce
@ -113,6 +114,7 @@ Application Configuration
.. image:: applications/nextcloud-logo.png :doc:`NextCloud<applications/nextcloud>`
.. image:: applications/obm_logo.png :doc:`OBM<applications/obm>`
.. image:: applications/logo_office_365.png :doc:`Office 365<applications/office365>`
.. image:: applications/logo-publik.png :doc:`Publik<applications/publik>`
.. image:: applications/phpldapadmin_logo.png :doc:`phpLDAPAdmin<applications/phpldapadmin>`
.. image:: applications/roundcube_logo.png :doc:`Roundcube<applications/roundcube>`
.. image:: applications/salesforce-logo.jpg :doc:`SalesForce<applications/salesforce>`

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,53 @@
Publik
=======
|image0|
Presentation
------------
Publik is an open-source citizen relationship management tool.
See `the official Publik website <https://publik.entrouvert.com/>`__ for a
complete presentation.
It feature an OpenID Connect login that work with LemonLDAP::NG.
Configuring Publik
-------------------
Connect to your publik instance authentic2 webui with an Admin user, in the admin panel, go to "Authentic2_Auth_Oidc" "Oidc providers".
Click on "Add Oidc Provider".
* Name : LemonLDAP SSO
* Short id : lemonldap
* Provider : https://auth.example.com/
* Client id : clientid
* Client secret : secret
* Authorization endpoint : https://auth.example.com/oauth2/authorize
* Token endpoint : https://auth.example.com/oauth2/token
* Userinfo endpoint : https://auth.example.com/oauth2/userinfo
* End session endpont : https://auth.example.com/oauth2/logout
* WebKey JSON : Copy/Paste the content of https://auth.example.com/oauth2/jwks
* Claims Enabled : yes
* Show on connection page : yes
Strategy and Collectivity can be configured based to your needs.
OIDC Claim mappings can be configured based on your needs.
Configuring LemonLDAP
~~~~~~~~~~~~~~~~~~~~~
We now have to configure LemonLDAP::NG to recognize publik as a valid OIDC relying party.
Add a :doc:`new OpenID Connect relying party<..//idpopenidconnect>`
with the following parameters (Options -> Basic) :
* **Client ID**: the same you set in Publik configuration.
* **Client Secret**: the same you set in Publik configuration.
* **Allowed redirection addresses for login**: The "Callback URL" for authentic2 : https://authentic2-instance/accounts/oidc/callback/
.. |image0| image:: /applications/logo-publik.png
:class: align-center

View File

@ -11,6 +11,13 @@ repeatedly trying to guess the password of an user. If disabled,
automated tools may submit thousands of password attempts in a matter of
seconds.
.. attention::
This plugin relies on the Login History, stored in users' persistent sessions.
This means that the authentication and persistent session backends will be
accessed for every login attempt, even fraudulent ones. This plugin is not
meant to protect against denial of service attacks.
Configuration
-------------

View File

@ -314,6 +314,11 @@ Options
(RSXXX) or HMAC (HSXXX) based signature algorithms
- **Access Token signature algorithm**: Select one of the available public
key signature algorithms
- **Userinfo signature algorithm** (since version ``2.0.12``): Select one
of the available signature algorithms to release user information as a JWT
on the ``/userinfo`` endpoint. If this option is left empty, user
information will be released as a plain JSON object. The ``None`` value
will release user information as an unsigned JWT.
- **Require PKCE** (since version ``2.0.4``): a code challenge is
required at token endpoint (see
`RFC7636 <https://tools.ietf.org/html/rfc7636>`__)

View File

@ -51,17 +51,19 @@ protected from being impersonated.
.. attention::
Both spoofed and real session attributes can be used to
set access rules, groups or macros.
By example : ``$real_uid eq 'dwho'`` or ``$real_groups =~ /\bsu\b/``
By example : ``$real_uid && $real_uid eq 'dwho'`` or ``$real_groups && $real_groups =~ /\bsu\b/``
Keep in mind that real session is computed first. Afterward, if access
is granted, impersonated session is computed with real and spoofed
session attributes if Impersonation is allowed.
So, ``real_`` attributes are computed by second authentication process.
To avoid Perl warnings, you have to prefix regex with ``$real_var &&``.
.. attention::

View File

@ -89,7 +89,7 @@ configuration.
|image0|
To set your own background, copy your file in
``/usr/share/lemonldap-ng/portal/htdocs/skins/common/backgrounds/`` and
``/usr/share/lemonldap-ng/portal/htdocs/static/common/backgrounds/`` and
register it in ``/etc/lemonldap-ng/lemonldap-ng.ini``:
.. code-block:: ini

View File

@ -354,7 +354,7 @@ Go in Manager, ``General parameters`` » ``Advanced parameters`` »
to disable CSRF token by setting a special rule based on callers IP
address like this :
requireToken => $env->{REMOTE_ADDR} !~ /^127\.0\.[1-3]\.1$/
requireToken => $env->{REMOTE_ADDR} && $env->{REMOTE_ADDR} !~ /^127\.0\.[1-3]\.1$/
.. danger::

View File

@ -54,6 +54,15 @@ You can then remove them with ::
lemonldap-ng-sessions delete <session_id> <session_id> <etc.>
Brute-force protection plugin may cause duplicate persistent sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Because of `bug #2482 <https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2482>`__ , some users may notice that the persistent session database is filling with duplicate sessions. Some examples include:
* An uppercase version of the regular persistent session (dwho vs DWHO)
* An unqualified version (dwho vs dwho@idp.com)
This bug was fixed in 2.0.12, but administrators are advised to clean up their persistent session database to remove any duplicate persistent sessions remaining after the upgrade.
2.0.11
------

View File

@ -1213,6 +1213,18 @@ components:
- RS384
- RS512
default: HS512
userInfoSignAlg:
type: string
enum:
- ""
- none
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
default: ""
accessTokenJWT:
type: bool
accessTokenClaims:

View File

@ -27,7 +27,7 @@ our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaData
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:(?:UserAttribut|Servic|Rul)e|AuthnLevel)|(?:ExportedVar|Macro)s)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|S(?:toreIDToken|ortNumber|cope)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Expiration|SignAlg|Claims|JWT)|uth(?:orizationCodeExpiration|nLevel)|dditionalAudiences)|I(?:DToken(?:ForceClaims|Expiration|SignAlg)|con)|R(?:e(?:directUris|freshToken|quirePKCE)|ule)|Logout(?:SessionRequired|Type|Url)|P(?:ostLogoutRedirectUris|ublic)|OfflineSessionExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|ExtraClaims|UserIDAttr)|(?:ExportedVar|ScopeRule|Macro)s)';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Expiration|SignAlg|Claims|JWT)|uth(?:orizationCodeExpiration|nLevel)|dditionalAudiences)|I(?:DToken(?:ForceClaims|Expiration|SignAlg)|con)|R(?:e(?:directUris|freshToken|quirePKCE)|ule)|Logout(?:SessionRequired|Type|Url)|P(?:ostLogoutRedirectUris|ublic)|UserI(?:nfoSignAlg|DAttr)|OfflineSessionExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|ExtraClaims)|(?:ExportedVar|ScopeRule|Macro)s)';
our $samlIDPMetaDataNodeKeys = 'samlIDPMetaData(?:Options(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|EncryptionMod|UserAttribut|DisplayNam)e|S(?:ign(?:S[LS]OMessage|atureMethod)|toreSAMLToken|[LS]OBinding|ortNumber)|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Re(?:questedAuthnContext|solutionRule|layStateURL)|Force(?:Authn|UTF8)|I(?:sPassive|con)|NameIDFormat)|ExportedAttributes|XML)';
our $samlSPMetaDataNodeKeys = 'samlSPMetaData(?:Options(?:S(?:ign(?:S[LS]OMessage|atureMethod)|essionNotOnOrAfterTimeout)|N(?:ameID(?:SessionKey|Format)|otOnOrAfterTimeout)|(?:CheckS[LS]OMessageSignatur|OneTimeUs|Rul)e|En(?:ableIDPInitiatedURL|cryptionMode)|AuthnLevel|ForceUTF8)|(?:ExportedAttribute|Macro)s|XML)';
our $virtualHostKeys = '(?:vhost(?:A(?:ccessToTrace|uthnLevel|liases)|(?:Maintenanc|Typ)e|ServiceTokenTTL|Https|Port)|(?:exportedHeader|locationRule)s|post)';

View File

@ -122,7 +122,7 @@ sub encrypt {
}
sub token {
return encrypt( join( ':', time, @_ ) );
return $_[0] ? encrypt( join( ':', time, @_ ) ) : encrypt(time);
}
## @method reval

View File

@ -139,7 +139,9 @@ sub run {
}
# Try to recover cookie and user session
if ( $id = $class->fetchId($req)
$id = $class->fetchId($req);
$class->data( {} ) unless($id);
if ( $id
and $session = $class->retrieveSession( $req, $id ) )
{

View File

@ -2453,6 +2453,43 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'oidcRPMetaDataOptionsUserIDAttr' => {
'type' => 'text'
},
'oidcRPMetaDataOptionsUserInfoSignAlg' => {
'default' => '',
'select' => [ {
'k' => '',
'v' => ''
},
{
'k' => 'none',
'v' => 'None'
},
{
'k' => 'HS256',
'v' => 'HS256'
},
{
'k' => 'HS384',
'v' => 'HS384'
},
{
'k' => 'HS512',
'v' => 'HS512'
},
{
'k' => 'RS256',
'v' => 'RS256'
},
{
'k' => 'RS384',
'v' => 'RS384'
},
{
'k' => 'RS512',
'v' => 'RS512'
}
],
'type' => 'select'
},
'oidcRPMetaDataScopeRules' => {
'default' => {},
'test' => {

View File

@ -4262,6 +4262,20 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
],
default => 'RS256',
},
oidcRPMetaDataOptionsUserInfoSignAlg => {
type => 'select',
select => [
{ k => '', v => '' },
{ k => 'none', v => 'None' },
{ k => 'HS256', v => 'HS256' },
{ k => 'HS384', v => 'HS384' },
{ k => 'HS512', v => 'HS512' },
{ k => 'RS256', v => 'RS256' },
{ k => 'RS384', v => 'RS384' },
{ k => 'RS512', v => 'RS512' },
],
default => '',
},
oidcRPMetaDataOptionsAccessTokenJWT => { type => 'bool', default => 0 },
oidcRPMetaDataOptionsAccessTokenClaims =>
{ type => 'bool', default => 0 },

View File

@ -224,6 +224,7 @@ sub cTrees {
nodes => [
'oidcRPMetaDataOptionsIDTokenSignAlg',
'oidcRPMetaDataOptionsAccessTokenSignAlg',
'oidcRPMetaDataOptionsUserInfoSignAlg',
'oidcRPMetaDataOptionsRequirePKCE',
'oidcRPMetaDataOptionsAllowOffline',
'oidcRPMetaDataOptionsAllowPasswordGrant',

View File

@ -567,6 +567,47 @@ function templates(tpl,key) {
"title" : "oidcRPMetaDataOptionsAccessTokenSignAlg",
"type" : "select"
},
{
"default" : "",
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserInfoSignAlg",
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsUserInfoSignAlg",
"select" : [
{
"k" : "",
"v" : ""
},
{
"k" : "none",
"v" : "None"
},
{
"k" : "HS256",
"v" : "HS256"
},
{
"k" : "HS384",
"v" : "HS384"
},
{
"k" : "HS512",
"v" : "HS512"
},
{
"k" : "RS256",
"v" : "RS256"
},
{
"k" : "RS384",
"v" : "RS384"
},
{
"k" : "RS512",
"v" : "RS512"
}
],
"title" : "oidcRPMetaDataOptionsUserInfoSignAlg",
"type" : "select"
},
{
"default" : 0,
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRequirePKCE",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"قاعدة الدخول",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"خاصّيّة المستخدم",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"اسم أوبين أيدي كونيكت RP",
"oidcRPStateTimeout":"حالة مهلة الجلسة",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"خدمة أل يو أر ل",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"لا يحتوي الخادم على إعدادات. استخدام قالب لحفظ الأول"
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Regla de acceso",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"Atributo de usuario",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"Caducidad de estado de sesión",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Règle d'accès",
"oidcRPMetaDataOptionsTimeouts":"Expiration",
"oidcRPMetaDataOptionsUserIDAttr":"Attribut de l'utilisateur",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Algorithme de signature des informations utilisateur",
"oidcRPMetaDataScopeRules":"Règles de scope",
"oidcRPName":"Nom du client OpenID Connect",
"oidcRPStateTimeout":"Durée d'une session state",

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Regola di accesso",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"Attributo utente",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"Nome di OpenID Connect RP",
"oidcRPStateTimeout":"Durata della sessione stato",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"URL del servizio",
"yubikey2fUserCanRemoveKey":"Autorizza l'utente a rimuovere la Yubikey",
"zeroConfExplanations":"Il server non ha alcuna configurazione. Utilizza il modello per salvare il primo."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Reguła dostępu",
"oidcRPMetaDataOptionsTimeouts":"Limit czasu",
"oidcRPMetaDataOptionsUserIDAttr":"Atrybut użytkownika",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Zasady dotyczące zakresu",
"oidcRPName":"Nazwa RP OpenID Connect",
"oidcRPStateTimeout":"Limit czasu sesji stanowej",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"URL usługi",
"yubikey2fUserCanRemoveKey":"Pozwól użytkownikowi usunąć Yubikey",
"zeroConfExplanations":"Serwer nie ma konfiguracji. Użyj szablonu, aby zapisać pierwszy."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Erişim kuralı",
"oidcRPMetaDataOptionsTimeouts":"Zaman aşımları",
"oidcRPMetaDataOptionsUserIDAttr":"Kullanıcı niteliği",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Kapsam kuralları",
"oidcRPName":"OpenID Connect RP Adı",
"oidcRPStateTimeout":"Oturum zaman aşımını belirle",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"Servis URL'si",
"yubikey2fUserCanRemoveKey":"Yubikey'i kaldırmak için kullanıcıya izin ver",
"zeroConfExplanations":"Sunucunun yapılandırması yok. Şimdi bir tane kaydetmek için şablonu kullanın."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Quy tắc truy cập",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"thuộc tính người dùng",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"Thời gian chờ của trạng thái phiên làm việc",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"Dịch vụ URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Máy chủ không có cấu hình. Sử dụng mẫu để lưu đầu tiên. "
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcRPMetaDataOptionsTimeouts":"Timeouts",
"oidcRPMetaDataOptionsUserIDAttr":"User attribute",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID Connect RP Name",
"oidcRPStateTimeout":"State session timeout",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -689,6 +689,7 @@
"oidcRPMetaDataOptionsRule":"存取規則",
"oidcRPMetaDataOptionsTimeouts":"逾時",
"oidcRPMetaDataOptionsUserIDAttr":"使用者屬性",
"oidcRPMetaDataOptionsUserInfoSignAlg":"Userinfo signature algorithm",
"oidcRPMetaDataScopeRules":"Scope rules",
"oidcRPName":"OpenID 連線 RP 名稱",
"oidcRPStateTimeout":"狀態工作階段逾時",
@ -1210,4 +1211,4 @@
"yubikey2fUrl":"服務 URL",
"yubikey2fUserCanRemoveKey":"允許使用者移除 Yubikey",
"zeroConfExplanations":"伺服器未設定。使用飯本來儲存第一個。"
}
}

View File

@ -485,6 +485,7 @@ t/01-Handler-redirection-and-URL-check-by-portal.t
t/01-pdata.t
t/01-Reject-Hashes-in-URL.t
t/01-Unauth-Logout.t
t/02-Password-Demo-Hook.t
t/02-Password-Demo-Local-noPpolicy.t
t/02-Password-Demo-Local-Ppolicy.t
t/02-Password-Demo-Local-SpeChars-Ppolicy.t
@ -552,6 +553,7 @@ t/31-Auth-and-issuer-CAS-proxied.t
t/31-Auth-and-issuer-CAS-with-choice-and-cancel.t
t/31-Auth-and-issuer-CAS-with-choice.t
t/31-Auth-and-issuer-CAS-XSS-on-logout.t
t/32-Auth-and-issuer-OIDC-authorization_code-jwt-userinfo.t
t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
@ -563,6 +565,7 @@ t/32-Auth-and-issuer-OIDC-implicit-no-token.t
t/32-Auth-and-issuer-OIDC-implicit.t
t/32-Auth-and-issuer-OIDC-sorted.t
t/32-CAS-10.t
t/32-CAS-Hooks.t
t/32-CAS-Macros.t
t/32-CAS-Prefix.t
t/32-OIDC-ClaimTypes.t
@ -631,6 +634,7 @@ t/43-MailPasswordReset-Choice.t
t/43-MailPasswordReset-Combination-LDAP.t
t/43-MailPasswordReset-Combination.t
t/43-MailPasswordReset-DBI.t
t/43-MailPasswordReset-Hook.t
t/43-MailPasswordReset-LDAP.t
t/43-MailPasswordReset-with-captcha.t
t/43-MailPasswordReset-with-token.t
@ -748,9 +752,11 @@ t/78-2F-UpgradeOnly.t
t/79-2F-Yubikey-from-Session.t
t/79-2F-Yubikey.t
t/90-Translations.t
t/91-handler-cache-cleaned.t
t/91-Memory-Leak.t
t/99-Dont-load-Dumper.t
t/99-pod.t
t/CasHookPlugin.pm
t/gpghome/key.asc
t/gpghome/openpgp-revocs.d/9482CEFB055809CBAFE6D71AAB2D5542891D1677.rev
t/gpghome/private-keys-v1.d/A076B0E7DB141A919271EE8B581CDFA8DA42F333.key
@ -767,6 +773,7 @@ t/lib/Lemonldap/NG/Portal/Custom.pm
t/lmConf-1.json
t/oidc-lib.pm
t/OidcHookPlugin.pm
t/PasswordHookPlugin.pm
t/pdata.pm
t/README.md
t/saml-lib.pm

View File

@ -368,6 +368,7 @@ sub run {
->{$idp_initiated_spConfKey}
->{samlSPMetaDataOptionsNameIDFormat} || "email";
eval {
$login->request()->NameIDPolicy()
->Format( $self->getNameIDFormat($nameIDFormatKey) );
};
@ -458,7 +459,9 @@ sub run {
}
unless ($result) {
$self->logger->error("Could not verify signature of incoming SSO request from $spConfKey");
$self->logger->error(
"Could not verify signature of incoming SSO request from $spConfKey"
);
return PE_SAML_SIGNATURE_ERROR;
}
else {
@ -469,11 +472,35 @@ sub run {
$self->logger->debug("Message signature will not be checked");
}
my $nameIDFormat;
# Check NameID Policy in request
if ( $login->request()->NameIDPolicy ) {
$nameIDFormat = $login->request()->NameIDPolicy->Format();
$self->logger->debug(
"Get NameID format $nameIDFormat from request");
}
# Get default NameID Format from configuration
# Set to "email" if no value in configuration
my $nameIDFormatKey =
$self->conf->{samlSPMetaDataOptions}->{$spConfKey}
->{samlSPMetaDataOptionsNameIDFormat} || "email";
# NameID unspecified is forced to default NameID format
if ( !$nameIDFormat
or $nameIDFormat eq $self->getNameIDFormat("unspecified") )
{
$nameIDFormat = $self->getNameIDFormat($nameIDFormatKey);
eval {
$login->request()->NameIDPolicy()->Format($nameIDFormat);
};
}
# Force AllowCreate to TRUE for transient/persistent NameIDPolicy
if ( $login->request()->NameIDPolicy ) {
my $nif = $login->request()->NameIDPolicy->Format();
if ( $nif eq $self->getNameIDFormat("transient")
or $nif eq $self->getNameIDFormat("persistent") )
if ( $nameIDFormat eq $self->getNameIDFormat("transient")
or $nameIDFormat eq $self->getNameIDFormat("persistent") )
{
$self->logger->debug(
"Force AllowCreate flag in NameIDPolicy");
@ -566,27 +593,6 @@ sub run {
$self->logger->debug("SSO: assertion is built");
# Get default NameID Format from configuration
# Set to "email" if no value in configuration
my $nameIDFormatKey =
$self->conf->{samlSPMetaDataOptions}->{$spConfKey}
->{samlSPMetaDataOptionsNameIDFormat} || "email";
my $nameIDFormat;
# Check NameID Policy in request
if ( $login->request()->NameIDPolicy ) {
$nameIDFormat = $login->request()->NameIDPolicy->Format();
$self->logger->debug(
"Get NameID format $nameIDFormat from request");
}
# NameID unspecified is forced to default NameID format
if ( !$nameIDFormat
or $nameIDFormat eq $self->getNameIDFormat("unspecified") )
{
$nameIDFormat = $self->getNameIDFormat($nameIDFormatKey);
}
# Get session key associated with NameIDFormat
# Not for unspecified, transient, persistent, entity, encrypted
my $nameIDFormatConfiguration = {
@ -599,7 +605,9 @@ sub run {
};
my $nameIDSessionKey =
$self->conf->{ $nameIDFormatConfiguration->{$nameIDFormat} };
$nameIDFormatConfiguration->{$nameIDFormat}
? $self->conf->{ $nameIDFormatConfiguration->{$nameIDFormat} }
: '';
# Override default NameID Mapping
if ( $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
@ -611,12 +619,16 @@ sub run {
}
my $nameIDContent;
if ( $self->spMacros->{$sp}->{$nameIDSessionKey} ) {
if ( $nameIDSessionKey
and $self->spMacros->{$sp}->{$nameIDSessionKey} )
{
$nameIDContent =
$self->spMacros->{$sp}->{$nameIDSessionKey}
->( $req, $req->{sessionInfo} );
}
elsif ( defined $req->{sessionInfo}->{$nameIDSessionKey} ) {
elsif ( $nameIDSessionKey
and ( defined $req->{sessionInfo}->{$nameIDSessionKey} ) )
{
$nameIDContent =
$self->p->getFirstValue(
$req->{sessionInfo}->{$nameIDSessionKey} );
@ -650,8 +662,8 @@ sub run {
$self->logger->debug(
"NameID Format is " . $login->nameIdentifier->Format );
$self->logger->debug(
"NameID Content is " . $login->nameIdentifier->content );
$self->logger->debug( "NameID Content is "
. ( $login->nameIdentifier->content || "" ) );
# Push attributes
my @attributes;
@ -700,7 +712,8 @@ sub run {
}
$self->logger->debug(
"SAML2 attribute $name will be set with $_ session key ($sp)");
"SAML2 attribute $name will be set with $_ session key ($sp)"
);
# SAML2 attribute
my $attribute =
@ -1317,8 +1330,7 @@ sub soapSloServer {
"SLO response signature according to metadata");
}
$h =
$self->p->processHook( $req, 'samlBuildLogoutResponse', $logout );
$h = $self->p->processHook( $req, 'samlBuildLogoutResponse', $logout );
if ( $h != PE_OK ) {
return $self->p->sendError( $req,
"SLO: samlBuildLogoutResponse hook returned error", 400 );
@ -1976,7 +1988,9 @@ sub sloServer {
if ($checkSLOMessageSignature) {
unless ( $self->checkSignatureStatus($logout) ) {
$self->logger->error("Could not verify signature of incoming SLO request from $spConfKey");
$self->logger->error(
"Could not verify signature of incoming SLO request from $spConfKey"
);
$self->imgnok($req);
}
else {

View File

@ -434,6 +434,7 @@ sub display {
),
( $req->token ? ( TOKEN => $req->token ) : () ),
( $req->data->{waitingMessage} ? ( WAITING_MESSAGE => 1 ) : () ),
ENABLE_PASSWORD_DISPLAY => $self->conf->{portalEnablePasswordDisplay},
);
# Disable all forms on:

View File

@ -396,7 +396,7 @@ sub authenticate {
$req->steps( [
'setSessionInfo', 'setMacros',
'setPersistentSessionInfo', 'storeHistory',
@{ $self->afterData }, sub { PE_BADCREDENTIALS }
@{ $self->afterData }, sub { PE_BADCREDENTIALS }
]
);
@ -475,13 +475,12 @@ sub setGroups {
}
sub setPersistentSessionInfo {
# $user passed by BruteForceProtection plugin
my ( $self, $req, $user ) = @_;
my ( $self, $req ) = @_;
# Do not restore infos if session already opened
unless ( $req->id ) {
my $key = $req->{sessionInfo}->{ $self->conf->{whatToTrace} } || $user;
my $key = $req->{sessionInfo}->{ $self->conf->{whatToTrace} };
return PE_OK unless ( $key and length($key) );
my $persistentSession = $self->getPersistentSession($key);
@ -620,9 +619,9 @@ sub secondFactor {
}
sub storeHistory {
my ( $self, $req, $uid ) = @_; # $uid passed by BruteForceProtection plugin
my ( $self, $req ) = @_;
if ( $self->conf->{loginHistoryEnabled} ) {
$self->registerLogin( $req, $uid );
$self->registerLogin($req);
}
PE_OK;
}

View File

@ -1047,9 +1047,7 @@ sub tplParams {
}
sub registerLogin {
# $user passed by BruteForceProtection plugin
my ( $self, $req, $uid ) = @_;
my ( $self, $req ) = @_;
return
unless ( $self->conf->{loginHistoryEnabled}
and defined $req->authResult );
@ -1079,8 +1077,7 @@ sub registerLogin {
}
}
}
$self->updatePersistentSession( $req, { 'loginHistory' => undef },
$uid );
$self->updatePersistentSession( $req, { 'loginHistory' => undef } );
delete $req->sessionInfo->{loginHistory};
}
@ -1105,7 +1102,7 @@ sub registerLogin {
if ( scalar @{ $history->{$type} } > $self->conf->{ $type . "Number" } );
# Save into persistent session
$self->updatePersistentSession( $req, { _loginHistory => $history }, $uid );
$self->updatePersistentSession( $req, { _loginHistory => $history, } );
PE_OK;
}

View File

@ -12,7 +12,7 @@ our $VERSION = '2.1.0';
extends 'Lemonldap::NG::Portal::Main::Plugin';
# INITIALIZATION
use constant aroundSub => { authenticate => 'check' };
use constant afterSub => { setPersistentSessionInfo => 'run' };
has lockTimes => (
is => 'rw',
@ -61,7 +61,9 @@ sub init {
sort { $a <=> $b }
map {
$_ =~ s/\D//;
abs $_ < $self->conf->{bruteForceProtectionMaxLockTime} ? abs $_ : ()
abs $_ < $self->conf->{bruteForceProtectionMaxLockTime}
? abs $_
: ()
}
grep { /\d+/ }
split /\s*,\s*/, $self->conf->{bruteForceProtectionLockTimes};
@ -99,13 +101,9 @@ sub init {
}
# RUNNING METHOD
sub check {
my ( $self, $sub, $req ) = @_;
my $now = time;
$self->p->setSessionInfo($req);
$self->logger->debug("Retrieve $req->{user} logins history");
$self->p->setPersistentSessionInfo( $req, $req->{user} );
sub run {
my ( $self, $req ) = @_;
my $now = time;
my $countFailed = my @failedLogins =
map { ( $now - $_->{_utime} ) <= $self->maxAge ? $_ : () }
@{ $req->sessionInfo->{_loginHistory}->{failedLogin} };
@ -115,7 +113,7 @@ sub check {
my $lastFailedLoginEpoch = $failedLogins[0]->{_utime} || undef;
if ( $self->conf->{bruteForceProtectionIncrementalTempo} ) {
return $sub->($req) unless $lastFailedLoginEpoch;
return PE_OK unless $lastFailedLoginEpoch;
# Delta between current attempt and last failed login
my $delta = $now - $lastFailedLoginEpoch;
@ -148,10 +146,10 @@ sub check {
$req->lockTime( $waitingTime - $delta );
return PE_WAIT;
}
return $sub->($req);
return PE_OK;
}
return $sub->($req)
return PE_OK
if ( $countFailed < $self->maxFailed );
# Delta between current attempt and last failed login
@ -159,7 +157,7 @@ sub check {
$self->logger->debug(" -> Delta = $delta");
# Delta < Tempo => wait
return $sub->($req)
return PE_OK
unless ( $delta < $self->conf->{bruteForceProtectionTempo}
&& $countFailed );

View File

@ -172,50 +172,52 @@ sub activeSessions {
my $customParam = $self->conf->{globalLogoutCustomParam} || '';
# Try to retrieve sessions from sessions DB
$self->logger->debug('Try to retrieve sessions from DB');
my $moduleOptions = $self->conf->{globalStorageOptions} || {};
$moduleOptions->{backend} = $self->conf->{globalStorage};
$self->logger->debug("Looking for \"$user\" sessions...");
$sessions =
$self->module->searchOn( $moduleOptions, $self->conf->{whatToTrace},
$user );
if ($user) {
$self->logger->debug('Try to retrieve sessions from DB');
my $moduleOptions = $self->conf->{globalStorageOptions} || {};
$moduleOptions->{backend} = $self->conf->{globalStorage};
$self->logger->debug("Looking for \"$user\" sessions...");
$sessions =
$self->module->searchOn( $moduleOptions, $self->conf->{whatToTrace},
$user );
$self->logger->debug('Remove non-SSO session(s)...');
my $other = 0;
foreach ( keys %$sessions ) {
unless ( $sessions->{$_}->{_session_kind} eq 'SSO' ) {
delete $sessions->{$_};
$other++;
$self->logger->debug('Skip non-SSO session(s)...');
my $other = 0;
foreach ( keys %$sessions ) {
unless ( $sessions->{$_}->{_session_kind} eq 'SSO' ) {
delete $sessions->{$_};
$other++;
}
}
}
$self->logger->info("$other non-SSO session(s) removed")
if $other;
$self->logger->info("$other non-SSO session(s) skipped")
if $other;
$self->logger->debug('Build an array ref with sessions info...');
@$activeSessions =
map {
my $epoch;
my $regex = '^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$';
$_->{startTime} =~ /$regex/;
$epoch = timelocal( $6, $5, $4, $3, $2 - 1, $1 );
$_->{startTime} = $epoch;
if ( $_->{updateTime} ) {
$_->{updateTime} =~ /$regex/;
$self->logger->debug('Build an array ref with sessions info...');
@$activeSessions =
map {
my $epoch;
my $regex = '^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$';
$_->{startTime} =~ /$regex/;
$epoch = timelocal( $6, $5, $4, $3, $2 - 1, $1 );
$_->{updateTime} = $epoch;
}
$_;
}
sort { $b->{startTime} cmp $a->{startTime} } map { {
id => $_,
customParam => $sessions->{$_}->{$customParam},
ipAddr => $sessions->{$_}->{ipAddr},
authLevel => $sessions->{$_}->{authenticationLevel},
startTime => $sessions->{$_}->{_startTime},
updateTime => $sessions->{$_}->{_updateTime}
};
} keys %$sessions;
$_->{startTime} = $epoch;
if ( $_->{updateTime} ) {
$_->{updateTime} =~ /$regex/;
$epoch = timelocal( $6, $5, $4, $3, $2 - 1, $1 );
$_->{updateTime} = $epoch;
}
$_;
}
sort { $b->{startTime} cmp $a->{startTime} } map { {
id => $_,
customParam => $sessions->{$_}->{$customParam},
ipAddr => $sessions->{$_}->{ipAddr},
authLevel => $sessions->{$_}->{authenticationLevel},
startTime => $sessions->{$_}->{_startTime},
updateTime => $sessions->{$_}->{_updateTime}
};
} keys %$sessions;
}
return $activeSessions;
}

View File

@ -515,6 +515,14 @@ $(window).on 'load', () ->
if window.datas.ppolicy? and $('#newpassword').length
$('#reset').change togglecheckpassword
if datas['enablePasswordDisplay']
$(".toggle-password").mousedown (e) ->
$(this).toggleClass("fa-eye fa-eye-slash");
$("input[name=password]").attr("type", "text");
$(".toggle-password").mouseup (e) ->
$(this).toggleClass("fa-eye fa-eye-slash");
$("input[name=password]").attr("type", "password");
# Ping if asked
if datas['pingInterval'] and datas['pingInterval'] > 0
window.setTimeout ping, datas['pingInterval']
@ -577,4 +585,4 @@ $(window).on 'load', () ->
console.log 'Error', err if err
res = JSON.parse j.responseText if j
if res and res.error
console.log 'Returned error', res
console.log 'Returned error', res

View File

@ -561,6 +561,16 @@ LemonLDAP::NG Portal jQuery scripts
if ((window.datas.ppolicy != null) && $('#newpassword').length) {
$('#reset').change(togglecheckpassword);
}
if (datas['enablePasswordDisplay']) {
$(".toggle-password").mousedown(function(e) {
$(this).toggleClass("fa-eye fa-eye-slash");
return $("input[name=password]").attr("type", "text");
});
$(".toggle-password").mouseup(function(e) {
$(this).toggleClass("fa-eye fa-eye-slash");
return $("input[name=password]").attr("type", "password");
});
}
if (datas['pingInterval'] && datas['pingInterval'] > 0) {
window.setTimeout(ping, datas['pingInterval']);
}

View File

@ -1 +1 @@
!function(){var o=function(e,r){return $("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning message-danger alert-success alert-warning alert-danger"),$("#color").addClass("message-"+r),"positive"===r&&(r="success"),$("#color").addClass("alert-"+r)},r=function(e,r,t){if(console.log("Error",t),(e=JSON.parse(e.responseText))&&e.error)return e=e.error.replace(/.* /,""),console.log("Returned error",e),o(e,"warning")},t="",e=function(e){return o("yourTotpKey","warning"),$.ajax({type:"POST",url:portal+"/2fregisters/totp/getkey",dataType:"json",data:{newkey:e},error:r,success:function(e){var r;return e.error?(e.error.match(/totpExistingKey/)&&$("#divToHide").hide(),o(e.error,"warning")):e.portal&&e.user&&e.secret?($("#divToHide").show(),r="otpauth://totp/"+escape(e.portal)+":"+escape(e.user)+"?secret="+e.secret+"&issuer="+escape(e.portal),6!==e.digits&&(r+="&digits="+e.digits),30!==e.interval&&(r+="&period="+e.interval),new QRious({element:document.getElementById("qr"),value:r,size:150}),$("#serialized").text(r),e.newkey?o("yourNewTotpKey","warning"):o("yourTotpKey","success"),t=e.token):o("PE24","danger")}})},n=function(){var e=$("#code").val();return e?$.ajax({type:"POST",url:portal+"/2fregisters/totp/verify",dataType:"json",data:{token:t,code:e,TOTPName:$("#TOTPName").val()},error:r,success:function(e){return e.error?e.error.match(/bad(Code|Name)/)?o(e.error,"warning"):o(e.error,"danger"):o("yourKeyIsRegistered","success")}}):o("fillTheForm","warning")};$(document).ready(function(){return e(0),$("#changekey").on("click",function(){return e(1)}),$("#verify").on("click",n)})}.call(this);
(function(){var r,e,n,t,o;n=function(e,r){return $("#msg").html(window.translate(e)),$("#color").removeClass("message-positive message-warning message-danger alert-success alert-warning alert-danger"),$("#color").addClass("message-"+r),"positive"===r&&(r="success"),$("#color").addClass("alert-"+r)},r=function(e,r,t){var o;if(console.log("Error",t),(o=JSON.parse(e.responseText))&&o.error)return o=o.error.replace(/.* /,""),console.log("Returned error",o),n(o,"warning")},t="",e=function(e){return n("yourTotpKey","warning"),$.ajax({type:"POST",url:portal+"/2fregisters/totp/getkey",dataType:"json",data:{newkey:e},error:r,success:function(e){var r;return e.error?(e.error.match(/totpExistingKey/)&&$("#divToHide").hide(),n(e.error,"warning")):e.portal&&e.user&&e.secret?($("#divToHide").show(),r="otpauth://totp/"+escape(e.portal)+":"+escape(e.user)+"?secret="+e.secret+"&issuer="+escape(e.portal),6!==e.digits&&(r+="&digits="+e.digits),30!==e.interval&&(r+="&period="+e.interval),new QRious({element:document.getElementById("qr"),value:r,size:150}),$("#serialized").text(r),e.newkey?n("yourNewTotpKey","warning"):n("yourTotpKey","success"),t=e.token):n("PE24","danger")}})},o=function(){var e;return(e=$("#code").val())?$.ajax({type:"POST",url:portal+"/2fregisters/totp/verify",dataType:"json",data:{token:t,code:e,TOTPName:$("#TOTPName").val()},error:r,success:function(e){return e.error?e.error.match(/bad(Code|Name)/)?n(e.error,"warning"):n(e.error,"danger"):n("yourKeyIsRegistered","success")}}):n("fillTheForm","warning")},$(document).ready(function(){return e(0),$("#changekey").on("click",function(){return e(1)}),$("#verify").on("click",function(){return o()})})}).call(this);

View File

@ -1 +1 @@
{"version":3,"sources":["totpregistration.js"],"names":["setMsg","msg","level","$","html","window","translate","removeClass","addClass","displayError","j","status","err","console","log","res","JSON","parse","responseText","error","replace","token","getKey","reset","ajax","type","url","portal","dataType","data","newkey","success","s","match","hide","user","secret","show","escape","digits","interval","QRious","element","document","getElementById","value","size","text","verify","val","code","TOTPName","ready","on","call","this"],"mappings":"CAMA,WACE,IAEAA,EAAS,SAASC,EAAKC,GAOrB,OANAC,EAAE,QAAQC,KAAKC,OAAOC,UAAUL,IAChCE,EAAE,UAAUI,YAAY,4FACxBJ,EAAE,UAAUK,SAAS,WAAaN,GACpB,aAAVA,IACFA,EAAQ,WAEHC,EAAE,UAAUK,SAAS,SAAWN,IAGzCO,EAAe,SAASC,EAAGC,EAAQC,GAIjC,GAFAC,QAAQC,IAAI,QAASF,IACrBG,EAAMC,KAAKC,MAAMP,EAAEQ,gBACRH,EAAII,MAGb,OAFAJ,EAAMA,EAAII,MAAMC,QAAQ,MAAO,IAC/BP,QAAQC,IAAI,iBAAkBC,GACvBf,EAAOe,EAAK,YAIvBM,EAAQ,GAERC,EAAS,SAASC,GAEhB,OADAvB,EAAO,cAAe,WACfG,EAAEqB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,2BACdC,SAAU,OACVC,KAAM,CACJC,OAAQP,GAEVJ,MAAOV,EACPsB,QAAS,SAASF,GAChB,IAAQG,EACR,OAAIH,EAAKV,OACHU,EAAKV,MAAMc,MAAM,oBACnB9B,EAAE,cAAc+B,OAEXlC,EAAO6B,EAAKV,MAAO,YAEtBU,EAAKF,QAAUE,EAAKM,MAAQN,EAAKO,QAGvCjC,EAAE,cAAckC,OAChBL,EAAI,kBAAqBM,OAAOT,EAAKF,QAAW,IAAOW,OAAOT,EAAKM,MAAS,WAAaN,EAAKO,OAAS,WAAcE,OAAOT,EAAKF,QAC7G,IAAhBE,EAAKU,SACPP,GAAK,WAAaH,EAAKU,QAEH,KAAlBV,EAAKW,WACPR,GAAK,WAAaH,EAAKW,UAEpB,IAAIC,OAAO,CACdC,QAASC,SAASC,eAAe,MACjCC,MAAOb,EACPc,KAAM,MAER3C,EAAE,eAAe4C,KAAKf,GAClBH,EAAKC,OACP9B,EAAO,iBAAkB,WAEzBA,EAAO,cAAe,WAEjBqB,EAAQQ,EAAKR,OArBXrB,EAAO,OAAQ,cA0B9BgD,EAAS,WACP,IACAC,EAAM9C,EAAE,SAAS8C,MACjB,OAAKA,EAGI9C,EAAEqB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,2BACdC,SAAU,OACVC,KAAM,CACJR,MAAOA,EACP6B,KAAMD,EACNE,SAAUhD,EAAE,aAAa8C,OAE3B9B,MAAOV,EACPsB,QAAS,SAASF,GAChB,OAAIA,EAAKV,MACHU,EAAKV,MAAMc,MAAM,kBACZjC,EAAO6B,EAAKV,MAAO,WAEnBnB,EAAO6B,EAAKV,MAAO,UAGrBnB,EAAO,sBAAuB,cApBpCA,EAAO,cAAe,YA2BjCG,EAAEwC,UAAUS,MAAM,WAKhB,OAJA9B,EAAO,GACPnB,EAAE,cAAckD,GAAG,QAAS,WAC1B,OAAO/B,EAAO,KAETnB,EAAE,WAAWkD,GAAG,QACdL,MAIVM,KAAKC"}
{"version":3,"sources":["totpregistration.js"],"names":["displayError","getKey","setMsg","token","verify","msg","level","$","html","window","translate","removeClass","addClass","j","status","err","res","console","log","JSON","parse","responseText","error","replace","reset","ajax","type","url","portal","dataType","data","newkey","success","s","match","hide","user","secret","show","escape","digits","interval","QRious","element","document","getElementById","value","size","text","val","code","TOTPName","ready","on","call","this"],"mappings":"CAMA,WACE,IAAIA,EAAcC,EAAQC,EAAQC,EAAOC,EAEzCF,EAAS,SAASG,EAAKC,GAOrB,OANAC,EAAE,QAAQC,KAAKC,OAAOC,UAAUL,IAChCE,EAAE,UAAUI,YAAY,4FACxBJ,EAAE,UAAUK,SAAS,WAAaN,GACpB,aAAVA,IACFA,EAAQ,WAEHC,EAAE,UAAUK,SAAS,SAAWN,IAGzCN,EAAe,SAASa,EAAGC,EAAQC,GACjC,IAAIC,EAGJ,GAFAC,QAAQC,IAAI,QAASH,IACrBC,EAAMG,KAAKC,MAAMP,EAAEQ,gBACRL,EAAIM,MAGb,OAFAN,EAAMA,EAAIM,MAAMC,QAAQ,MAAO,IAC/BN,QAAQC,IAAI,iBAAkBF,GACvBd,EAAOc,EAAK,YAIvBb,EAAQ,GAERF,EAAS,SAASuB,GAEhB,OADAtB,EAAO,cAAe,WACfK,EAAEkB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,2BACdC,SAAU,OACVC,KAAM,CACJC,OAAQP,GAEVF,MAAOtB,EACPgC,QAAS,SAASF,GAChB,IAAQG,EACR,OAAIH,EAAKR,OACHQ,EAAKR,MAAMY,MAAM,oBACnB3B,EAAE,cAAc4B,OAEXjC,EAAO4B,EAAKR,MAAO,YAEtBQ,EAAKF,QAAUE,EAAKM,MAAQN,EAAKO,QAGvC9B,EAAE,cAAc+B,OAChBL,EAAI,kBAAqBM,OAAOT,EAAKF,QAAW,IAAOW,OAAOT,EAAKM,MAAS,WAAaN,EAAKO,OAAS,WAAcE,OAAOT,EAAKF,QAC7G,IAAhBE,EAAKU,SACPP,GAAK,WAAaH,EAAKU,QAEH,KAAlBV,EAAKW,WACPR,GAAK,WAAaH,EAAKW,UAEpB,IAAIC,OAAO,CACdC,QAASC,SAASC,eAAe,MACjCC,MAAOb,EACPc,KAAM,MAERxC,EAAE,eAAeyC,KAAKf,GAClBH,EAAKC,OACP7B,EAAO,iBAAkB,WAEzBA,EAAO,cAAe,WAEjBC,EAAQ2B,EAAK3B,OArBXD,EAAO,OAAQ,cA0B9BE,EAAS,WACP,IAAI6C,EAEJ,OADAA,EAAM1C,EAAE,SAAS0C,OAIR1C,EAAEkB,KAAK,CACZC,KAAM,OACNC,IAAKC,OAAS,2BACdC,SAAU,OACVC,KAAM,CACJ3B,MAAOA,EACP+C,KAAMD,EACNE,SAAU5C,EAAE,aAAa0C,OAE3B3B,MAAOtB,EACPgC,QAAS,SAASF,GAChB,OAAIA,EAAKR,MACHQ,EAAKR,MAAMY,MAAM,kBACZhC,EAAO4B,EAAKR,MAAO,WAEnBpB,EAAO4B,EAAKR,MAAO,UAGrBpB,EAAO,sBAAuB,cApBpCA,EAAO,cAAe,YA2BjCK,EAAEqC,UAAUQ,MAAM,WAKhB,OAJAnD,EAAO,GACPM,EAAE,cAAc8C,GAAG,QAAS,WAC1B,OAAOpD,EAAO,KAETM,EAAE,WAAW8C,GAAG,QAAS,WAC9B,OAAOjD,UAIVkD,KAAKC"}

View File

@ -20,6 +20,11 @@
<input id="passwordfield" name="password" type="text" class="form-control key" autocomplete="off" required aria-required="true" aria-hidden="true"/>
<TMPL_ELSE>
<input id="passwordfield" name="password" type="password" class="form-control" trplaceholder="password" required aria-required="true"/>
<TMPL_IF NAME="ENABLE_PASSWORD_DISPLAY">
<div class="input-group-append">
<span class="input-group-text"><i class="fa fa-eye-slash toggle-password"></i></span>
</div>
</TMPL_IF>
</TMPL_IF>
</div>

View File

@ -26,7 +26,7 @@
"scriptname":"<TMPL_VAR NAME="SCRIPT_NAME">",
"activeTimer":<TMPL_VAR NAME="ACTIVE_TIMER" DEFAULT="0">,
"pingInterval":<TMPL_VAR NAME="PING" DEFAULT="0">,
"trOver":<TMPL_VAR NAME="TROVER" DEFAULT="[]"><TMPL_IF NAME="DISPLAY_PPOLICY">,
"trOver":<TMPL_VAR NAME="TROVER" DEFAULT="[]">,<TMPL_IF NAME="DISPLAY_PPOLICY">
"ppolicy": {
"display": "<TMPL_VAR NAME="DISPLAY_PPOLICY" DEFAULT="0">",
"minsize": "<TMPL_VAR NAME="PPOLICY_MINSIZE" DEFAULT="0">",
@ -36,7 +36,8 @@
"nopolicy": "<TMPL_VAR NAME="PPOLICY_NOPOLICY" DEFAULT="0">",
"allowedspechar": "<TMPL_VAR NAME="PPOLICY_ALLOWEDSPECHAR" ESCAPE="js" DEFAULT="">",
"minspechar": "<TMPL_VAR NAME="PPOLICY_MINSPECHAR" DEFAULT="0">"
}</TMPL_IF>
},</TMPL_IF>
"enablePasswordDisplay":<TMPL_VAR NAME="ENABLE_PASSWORD_DISPLAY" DEFAULT="0">
}
</script>

View File

@ -0,0 +1,345 @@
use lib 'inc';
use Test::More;
use strict;
use IO::String;
use LWP::UserAgent;
use LWP::Protocol::PSGI;
use MIME::Base64;
BEGIN {
require 't/test-lib.pm';
require 't/saml-lib.pm';
}
my $debug = 'error';
my ( $issuer, $sp, $res );
# Redefine LWP methods for tests
LWP::Protocol::PSGI->register(
sub {
my $req = Plack::Request->new(@_);
fail('POST should not launch SOAP requests');
count(1);
return [ 500, [], [] ];
}
);
sub runTest {
my ( $req_nif, $res_nif, $force_attr, $expect_req_nif, $expect_res_nif,
$expect_nameid )
= @_;
# Initialization
$issuer = register( 'issuer', sub { issuer( $res_nif, $force_attr ) } );
$sp = register( 'sp', sub { sp($req_nif) } );
# Simple SP access
my $res;
ok(
$res = $sp->_get(
'/', accept => 'text/html',
),
'Unauth SP request'
);
count(1);
expectOK($res);
my ( $host, $url, $s ) =
expectAutoPost( $res, 'auth.idp.com', '/saml/singleSignOn',
'SAMLRequest' );
my $sr = expectSamlRequest($s);
expectXPath(
$sr, '/samlp:AuthnRequest/samlp:NameIDPolicy/@Format',
$expect_req_nif, 'Found expected NameID Format in request',
);
# Push SAML request to IdP
ok(
$res = $issuer->_post(
$url,
IO::String->new($s),
accept => 'text/html',
length => length($s)
),
'Post SAML request to IdP'
);
count(1);
expectOK($res);
my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
# Try to authenticate with an authorized user to IdP
$s = "user=french&password=french&$s";
ok(
$res = $issuer->_post(
$url,
IO::String->new($s),
accept => 'text/html',
cookie => $pdata,
length => length($s),
),
'Post authentication'
);
count(1);
my $idpId = expectCookie($res);
# Expect pdata to be cleared
$pdata = expectCookie( $res, 'lemonldappdata' );
ok( $pdata !~ 'issuerRequestsaml', 'SAML request cleared from pdata' )
or explain( $pdata, 'not issuerRequestsaml' );
count(1);
( $host, $url, $s ) =
expectAutoPost( $res, 'auth.sp.com', '/saml/proxySingleSignOnPost',
'SAMLResponse' );
my ($sr) = expectSamlResponse($s);
expectXPath(
$sr, '/samlp:Response/saml:Assertion/saml:Subject/saml:NameID/@Format',
$expect_res_nif, 'Found expected NameID Format in response',
);
if ($expect_nameid) {
my $nameidvalue = expectXPath( $sr,
'/samlp:Response/saml:Assertion/saml:Subject/saml:NameID/text()' );
if ( ref($expect_nameid) eq "Regexp" ) {
like( $nameidvalue, $expect_nameid, "NameID matches" );
}
else {
is( $nameidvalue, $expect_nameid, "NameID matches" );
}
}
}
SKIP: {
eval "use Lasso";
if ($@) {
skip 'Lasso not found';
}
# Default settings use the email NIF
runTest(
# requested NameIDFormat (sp side)
undef,
# returned NameIDFormat (idp side)
undef,
# Name ID session key
undef,
# Expected NameIDFormat in SAMLRequest
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
# Expected NameIDFormat in SAMLResponse
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
# Expected NameID value
'fa@badwolf.org'
);
# Override session key
runTest(
undef,
undef,
"uid",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
'french'
);
# Using email explicitely return the email
runTest(
"email",
"email",
undef,
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
'fa@badwolf.org'
);
# Changing the format on the IDP side has no effect if the client
# specifies a NIF
runTest(
"email",
"kerberos",
undef,
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
'fa@badwolf.org'
);
# Using unspecified on the requesting side causes IDP settings to be honored
# specifies a NIF
runTest(
"unspecified",
"kerberos",
undef,
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos",
'french'
);
# Unspecified both ways + no forced key returns no value
runTest(
"unspecified",
"unspecified",
undef,
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
undef,
);
# Unspecified both ways returns forced key in unspecified format
runTest(
"unspecified",
"unspecified",
'mail',
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
'fa@badwolf.org',
);
# persistent asked by SP returns a value
runTest(
"persistent",
"email",
undef,
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
qr/./,
);
# persistent chosen by IDP returns a value
runTest(
"unspecified",
"persistent",
undef,
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
qr/./,
);
}
clean_sessions();
done_testing();
sub issuer {
my ( $res_nif, $force_attr ) = @_;
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'idp.com',
portal => 'http://auth.idp.com',
authentication => 'Demo',
userDB => 'Same',
issuerDBSAMLActivation => 1,
issuerDBSAMLRule => '$uid eq "french"',
samlSPMetaDataOptions => {
'sp.com' => {
samlSPMetaDataOptionsEncryptionMode => 'none',
samlSPMetaDataOptionsSignSSOMessage => 1,
samlSPMetaDataOptionsSignSLOMessage => 1,
samlSPMetaDataOptionsCheckSSOMessageSignature => 0,
samlSPMetaDataOptionsCheckSLOMessageSignature => 0,
(
$res_nif
? ( samlSPMetaDataOptionsNameIDFormat => $res_nif, )
: ()
),
(
$force_attr
? ( samlSPMetaDataOptionsNameIDSessionKey =>
$force_attr, )
: ()
),
}
},
samlSPMetaDataExportedAttributes => {
'sp.com' => {
cn =>
'1;cn;urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
uid =>
'1;uid;urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
}
},
samlOrganizationDisplayName => "IDP",
samlOrganizationName => "IDP",
samlOrganizationURL => "http://www.idp.com/",
samlServicePrivateKeyEnc => saml_key_idp_private_enc,
samlServicePrivateKeySig => saml_key_idp_private_sig,
samlServicePublicKeyEnc => saml_key_idp_public_enc,
samlServicePublicKeySig => saml_key_idp_public_sig,
samlSPMetaDataXML => {
"sp.com" => {
samlSPMetaDataXML =>
samlSPMetaDataXML( 'sp', 'HTTP-POST' )
},
},
}
}
);
}
sub sp {
my ($req_nif) = @_;
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'sp.com',
portal => 'http://auth.sp.com',
authentication => 'SAML',
userDB => 'Same',
issuerDBSAMLActivation => 0,
restSessionServer => 1,
samlIDPMetaDataExportedAttributes => {
idp => {
mail => "0;mail;;",
uid => "1;uid",
cn => "0;cn"
}
},
samlIDPMetaDataOptions => {
idp => {
samlIDPMetaDataOptionsEncryptionMode => 'none',
samlIDPMetaDataOptionsSSOBinding => 'post',
samlIDPMetaDataOptionsSLOBinding => 'post',
samlIDPMetaDataOptionsSignSSOMessage => 1,
samlIDPMetaDataOptionsSignSLOMessage => 1,
samlIDPMetaDataOptionsCheckSSOMessageSignature => 1,
samlIDPMetaDataOptionsCheckSLOMessageSignature => 1,
samlIDPMetaDataOptionsForceUTF8 => 1,
samlIDPMetaDataOptionsSignSSOMessage => 0,
(
$req_nif
? ( samlIDPMetaDataOptionsNameIDFormat => $req_nif,
)
: ()
),
}
},
samlIDPMetaDataExportedAttributes => {
idp => {
"uid" => "0;uid;;",
"cn" => "1;cn;;",
},
},
samlIDPMetaDataXML => {
idp => {
samlIDPMetaDataXML =>
samlIDPMetaDataXML( 'idp', 'HTTP-POST' )
}
},
samlOrganizationDisplayName => "SP",
samlOrganizationName => "SP",
samlOrganizationURL => "http://www.sp.com",
samlServicePublicKeySig => saml_key_sp_public_sig,
samlServicePrivateKeyEnc => saml_key_sp_private_enc,
samlServicePrivateKeySig => saml_key_sp_private_sig,
samlServicePublicKeyEnc => saml_key_sp_public_enc,
samlSPSSODescriptorAuthnRequestsSigned => 1,
},
}
);
}

View File

@ -11,7 +11,7 @@ BEGIN {
require 't/saml-lib.pm';
}
my $maintests = 21;
my $maintests = 22;
my $debug = 'error';
my ( $issuer, $sp, $res );
@ -35,8 +35,22 @@ SKIP: {
$issuer = register( 'issuer', \&issuer );
$sp = register( 'sp', \&sp );
# Simple SP access
# Try to authenticate
# -------------------
switch ('issuer');
my $res;
ok(
$res = $issuer->_post(
'/', IO::String->new('user=french&password=french'),
length => 27
),
'Auth query'
);
expectOK($res);
my $id = expectCookie($res);
# Simple SP access
switch ('sp');
ok(
$res = $sp->_get(
'/', accept => 'text/html',
@ -140,7 +154,7 @@ SKIP: {
# Verify authentication on SP
expectRedirection( $res, 'http://auth.sp.com' );
my $spId = expectCookie($res);
my $spId = expectCookie($res);
$rawCookie = getHeader( $res, 'Set-Cookie' );
ok( $rawCookie =~ /;\s*SameSite=None/, 'Found SameSite=None' );
@ -236,6 +250,8 @@ sub issuer {
portal => 'http://auth.idp.com',
authentication => 'Demo',
userDB => 'Same',
globalLogoutRule => 1,
globalLogoutTimer => 0,
issuerDBSAMLActivation => 1,
issuerDBSAMLRule => '$uid eq "french"',
samlSPMetaDataOptions => {

View File

@ -439,6 +439,7 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
),
'Push U2F signature'
);
$id = expectCookie($res);
ok(
$res = $client->_get(
'/2fregisters',

View File

@ -0,0 +1,120 @@
use Test::More;
use strict;
use IO::String;
use Data::Dumper;
require 't/test-lib.pm';
require 't/smtp.pm';
use_ok('Lemonldap::NG::Common::FormEncode');
count(1);
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'debug',
mail2fActivation => '$uid eq "msmith"',
mail2fAuthnLevel => 3,
mail2fCodeRegex => '\d{4}',
authentication => 'Demo',
userDB => 'Same',
}
}
);
# Login as dwho
ok(
my $res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23,
accept => 'text/html',
),
'Auth query'
);
count(1);
my $dwho_id = expectCookie($res);
ok(
$res = $client->_get(
'/', cookie => "lemonldap=$dwho_id",
),
'Get portal'
);
expectAuthenticatedAs($res, 'dwho');
count(1);
# Start logging in as msmith
my $s = buildForm({
user => 'msmith',
password => 'msmith',
});
ok(
$res = $client->_post(
'/',
IO::String->new($s),
length => length($s),
accept => 'text/html',
),
'Auth query'
);
count(1);
my ( $host, $url, $query ) =
expectForm( $res, undef, '/mail2fcheck?skin=bootstrap', 'token', 'code' );
ok(
$res->[2]->[0] =~
qr%<input name="code" value="" type="text" class="form-control" id="extcode" trplaceholder="code" autocomplete="off" />%,
'Found EXTCODE input'
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
ok( mail() =~ m%<b>(\d{4})</b>%, 'Found 2F code in mail' )
or print STDERR Dumper( mail() );
my $code = $1;
count(1);
# Fill the handler cache with dwho session info
ok(
$res = $client->_get(
'/', cookie => "lemonldap=$dwho_id",
),
'Get portal'
);
count(1);
# Finish logging in as msmith,
# this corrupts the dwho cache with msmith macros
$query =~ s/code=/code=${code}/;
ok(
$res = $client->_post(
'/mail2fcheck',
IO::String->new($query),
length => length($query),
accept => 'text/html',
),
'Post code'
);
count(1);
diag Dumper($res);
# Reuse the corrupted cache
ok(
$res = $client->_get(
'/', cookie => "lemonldap=$dwho_id",
),
'Get portal'
);
count(1);
TODO: {
local $TODO = "Fix 2539";
expectAuthenticatedAs($res, 'dwho');
}
clean_sessions();
done_testing( count() );

View File

@ -1,3 +1,7 @@
use XML::LibXML;
use URI::Escape;
use MIME::Base64;
sub saml_key_proxy_private_enc {
"-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2vzoUiQ4GsM5qLjoxslEDKj+RrPh/A743JCWe1Hbadjd5yD4
@ -800,4 +804,60 @@ EOF
;
}
=head4 expectXPath($xml_string, $xpath, $namespaces, $value, $message)
Match a XPath expression against the provided string, and verify that the correct value is
=cut
sub expectXPath {
my ( $xml_string, $xpath, $value, $message ) = @_;
my $dom = XML::LibXML->load_xml( string => $xml_string );
return unless ok( $dom, 'XML successfully parsed' );
my $xpc = XML::LibXML::XPathContext->new($dom);
my $namespaces = {
samlp => 'urn:oasis:names:tc:SAML:2.0:protocol',
saml => 'urn:oasis:names:tc:SAML:2.0:assertion',
};
if ( ref($namespaces) eq "HASH" ) {
for my $key ( keys %{$namespaces} ) {
$xpc->registerNs( $key, $namespaces->{$key} );
}
}
my ($match1) = $xpc->findnodes($xpath);
return unless ok( $match1, 'Found a match for XPath Expression ' . $xpath );
if ( ref($match1) eq 'XML::LibXML::Attr' ) {
if ($value) {
is( $match1->value, $value, $message );
}
return $match1->value;
}
elsif ( ref($match1) eq 'XML::LibXML::Text' ) {
if ($value) {
is( $match1->data, $value, $message );
}
return $match1->data;
}
else {
fail( "Unexpected XPath result: " . ref($match1) );
}
}
sub expectSamlRequest {
my ($string) = @_;
my ($sr) = $string =~ m/SAMLRequest=([^&]*)/;
ok( $sr, "Found SAMLRequest" );
return decode_base64( uri_unescape($sr) );
}
sub expectSamlResponse {
my ($string) = @_;
my ($sr) = $string =~ m/SAMLResponse=([^&]*)/;
ok( $sr, "Found SAMLResponse" );
return decode_base64( uri_unescape($sr) );
}
1;

View File

@ -337,9 +337,7 @@ Verify that result has a C<Lm-Remote-User> header and value is $user
sub expectAuthenticatedAs {
my ( $res, $user ) = @_;
ok( getHeader( $res, 'Lm-Remote-User' ) eq $user,
" Authenticated as $user" )
or explain( $res->[1], "Lm-Remote-User => $user" );
is( getHeader( $res, 'Lm-Remote-User' ), $user, "Authenticated as $user" );
count(1);
}