Merge branch 'v2.0'

This commit is contained in:
Yadd 2021-06-07 18:36:32 +02:00
commit a3b24418c6
45 changed files with 1391 additions and 175 deletions

View File

@ -46,14 +46,13 @@ Make sure you have already
:doc:`enabled OpenID Connect<../idpopenidconnect>` on your LemonLDAP::NG
server
Then, add a Relaying Party with the following configuration
Then, add a Relaying Party with the following configuration:
- Options » Authentification » Client ID : same as ``client_id`` above
- Options » Allowed redirection address : same as ''client_secret ''
above
- Options » Authentification » Client Secret : same as ``client_secret`` above
- Options » Allowed redirection address : ``https://<grafana domain>/login/generic_oauth``
If you want to transmit user attributes to Grafana, you also need to
configure
If you want to transmit extra user attributes to Grafana, you also need to configure:
- Extra Claims »
@ -72,6 +71,11 @@ configure
- map them to your corresponding LemonLDAP::NG session attribute
.. tip::
To trigger OIDC authentication directly, you can register grafana in application menu and
set as URL: ``https://<grafana domain>/login/generic_oauth``
.. |image0| image:: /applications/grafana_logo.png
:class: align-center

View File

@ -52,6 +52,32 @@ Sample code::
}
oidcGenerateCode
~~~~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered when LemonLDAP::NG is about to generate an Authorization Code for a Relying Party.
The hook's parameters are:
* A hash of the parameters for the OIDC Authorize request, which you can modify
* the configuration key of the relying party which will receive the token
* A hash of the session keys for the (internal) Authorization Code session
Sample code::
use constant hook => {
oidcGenerateCode => 'modifyRedirectUri',
};
sub modifyRedirectUri {
my ( $self, $req, $oidc_request, $rp, $code_payload ) = @_;
my $original_uri = $oidc_request->{redirect_uri};
$oidc_request->{redirect_uri} = "$original_uri?hooked=1";
return PE_OK;
}
oidcGenerateUserInfoResponse
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -83,7 +109,7 @@ This hook is triggered when LemonLDAP::NG is generating an ID Token.
The hook's parameters are:
* A hash of the claims to be contained in the ID Token
* the configuration key of the relying party which will receive the token
* the configuration key of the relying party which will receive the token
Sample code::
@ -161,7 +187,7 @@ The hook's parameter is the Lasso::Login object
Sample code::
use constant hook => {
use constant hook => {
samlGotAuthnRequest => 'gotRequest',
};
@ -182,7 +208,7 @@ The hook's parameter is the Lasso::Login object
Sample code::
use constant hook => {
use constant hook => {
samlBuildAuthnResponse => 'buildResponse',
};
@ -203,7 +229,7 @@ The hook's parameter is the Lasso::Logout object
Sample code::
use constant hook => {
use constant hook => {
samlGotLogoutRequest => 'gotLogout',
};
@ -224,7 +250,7 @@ The hook's parameter is the Lasso::Logout object
Sample code::
use constant hook => {
use constant hook => {
samlGotLogoutResponse => 'gotLogoutResponse',
};
@ -245,7 +271,7 @@ The hook's parameter is the Lasso::Logout object
Sample code::
use constant hook => {
use constant hook => {
samlBuildLogoutResponse => 'buildLogoutResponse',
};
@ -254,3 +280,142 @@ Sample code::
# Your code here
}
CAS Issuer hooks
-----------------
casGotRequest
~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered when LemonLDAP::NG received an CAS authentication request on the `/cas/login` endpoint.
The hook's parameter is a hash containing the CAS request parameters.
Sample code::
use constant hook => {
casGotRequest => 'filterService'
};
sub filterService {
my ( $self, $req, $cas_request ) = @_;
if ( $cas_request->{service} eq "http://auth.sp.com/" ) {
return PE_OK;
}
else {
return 999;
}
}
casGenerateServiceTicket
~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered when LemonLDAP::NG is about to generate a Service Ticket for a CAS application
The hook's parameters are:
* A hash of the parameters for the CAS request, which you can modify
* the configuration key of the cas application which will receive the ticket
* A hash of the session keys for the (internal) CAS session
Sample code::
use constant hook => {
'casGenerateServiceTicket' => 'changeRedirectUrl',
};
sub changeRedirectUrl {
my ( $self, $req, $cas_request, $app, $Sinfos ) = @_;
$cas_request->{service} .= "?hooked=1";
return PE_OK;
}
casGenerateValidateResponse
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered when LemonLDAP::NG is about to send a CAS response to an application on the `/cas/serviceValidate` endpoint.
The hook's parameters are:
* The username (CAS principal)
* A hash of modifiable attributes to be sent
Sample code::
use constant hook => {
casGenerateValidateResponse => 'addAttributes',
};
sub addAttributes {
my ( $self, $req, $username, $attributes ) = @_;
$attributes->{hooked} = 1;
return PE_OK;
}
Password change hooks
---------------------
passwordBeforeChange
~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered when LemonLDAP::NG is about to change or reset a user's password. Returning an error will cancel the password change operation
The hook's parameters are:
* The main user identifier
* The new password
* The old password, if relevant
Sample code::
use constant hook => {
passwordBeforeChange => 'blacklistPassword',
};
sub blacklistPassword {
my ( $self, $req, $user, $password, $old ) = @_;
if ( $password eq "12345" ) {
$self->logger->error("I've got the same combination on my luggage");
return PE_PP_INSUFFICIENT_PASSWORD_QUALITY;
}
return PE_OK;
}
passwordAfterChange
~~~~~~~~~~~~~~~~~~~
.. versionadded:: 2.0.12
This hook is triggered after LemonLDAP::NG has changed the user's password successfully in the underlying password database
The hook's parameters are:
* The main user identifier
* The new password
* The old password, if relevant
Sample code::
use constant hook => {
passwordAfterChange => 'logPasswordChange',
};
sub logPasswordChange {
my ( $self, $req, $user, $password, $old ) = @_;
$old ||= "";
$self->userLogger->info("Password changed for $user: $old -> $password")
return PE_OK;
}

View File

@ -186,6 +186,8 @@ For each OpenID Connect claim you want to release to applications, you can defin
in User attribute parameter (see below).
.. _oidcextraclaims:
Extra Claims
^^^^^^^^^^^^
@ -216,6 +218,8 @@ Userinfo endpoint.
LemonLDAP::NG session attribute in the **Exported Attributes**
section
.. _oidcscoperules:
Scope Rules
^^^^^^^^^^^

View File

@ -48,6 +48,12 @@ Security
configuration in the backend per registration request. You can limit
this by protecting in the WebServer the registration end point with
an authentication module, and give the credentials to clients.
- **Only allow declared scopes**: By default, LemonLDAP::NG will grant all requested scopes. When this option is in use, LemonLDAP will only grant:
- Standard OIDC scopes (``openid`` ``profile`` ``email`` ``address`` ``phone``)
- Scopes declared in :ref:`Extra Claims <oidcextraclaims>`
- Scopes declared in :ref:`Scope Rules <oidcscoperules>` (if they match the rule)
- **Authorization Code flow**: Set to 1 to allow Authorization Code
flow
- **Implicit flow**: Set to 1 to allow Implicit flow

View File

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

View File

@ -69,6 +69,6 @@ our $issuerParameters = {
issuerOptions => [qw(issuersTimeout)],
};
our $samlServiceParameters = [qw(samlEntityID samlServicePrivateKeySig samlServicePrivateKeySigPwd samlServicePublicKeySig samlServicePrivateKeyEnc samlServicePrivateKeyEncPwd samlServicePublicKeyEnc samlServiceUseCertificateInResponse samlServiceSignatureMethod samlNameIDFormatMapEmail samlNameIDFormatMapX509 samlNameIDFormatMapWindows samlNameIDFormatMapKerberos samlAuthnContextMapPassword samlAuthnContextMapPasswordProtectedTransport samlAuthnContextMapTLSClient samlAuthnContextMapKerberos samlOrganizationDisplayName samlOrganizationName samlOrganizationURL samlSPSSODescriptorAuthnRequestsSigned samlSPSSODescriptorWantAssertionsSigned samlSPSSODescriptorSingleLogoutServiceHTTPRedirect samlSPSSODescriptorSingleLogoutServiceHTTPPost samlSPSSODescriptorSingleLogoutServiceSOAP samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact samlSPSSODescriptorAssertionConsumerServiceHTTPPost samlSPSSODescriptorArtifactResolutionServiceArtifact samlIDPSSODescriptorWantAuthnRequestsSigned samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect samlIDPSSODescriptorSingleSignOnServiceHTTPPost samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect samlIDPSSODescriptorSingleLogoutServiceHTTPPost samlIDPSSODescriptorSingleLogoutServiceSOAP samlIDPSSODescriptorArtifactResolutionServiceArtifact samlAttributeAuthorityDescriptorAttributeServiceSOAP samlMetadataForceUTF8 samlRelayStateTimeout samlUseQueryStringSpecific samlOverrideIDPEntityID samlStorage samlStorageOptions samlCommonDomainCookieActivation samlCommonDomainCookieDomain samlCommonDomainCookieReader samlCommonDomainCookieWriter samlDiscoveryProtocolActivation samlDiscoveryProtocolURL samlDiscoveryProtocolPolicy samlDiscoveryProtocolIsPassive)];
our $oidcServiceParameters = [qw(oidcServiceMetaDataAuthorizeURI oidcServiceMetaDataTokenURI oidcServiceMetaDataUserInfoURI oidcServiceMetaDataJWKSURI oidcServiceMetaDataRegistrationURI oidcServiceMetaDataIntrospectionURI oidcServiceMetaDataEndSessionURI oidcServiceMetaDataCheckSessionURI oidcServiceMetaDataFrontChannelURI oidcServiceMetaDataBackChannelURI oidcServiceMetaDataAuthnContext oidcServicePrivateKeySig oidcServicePublicKeySig oidcServiceKeyIdSig oidcServiceAllowDynamicRegistration oidcServiceAllowAuthorizationCodeFlow oidcServiceAllowImplicitFlow oidcServiceAllowHybridFlow oidcServiceAuthorizationCodeExpiration oidcServiceAccessTokenExpiration oidcServiceIDTokenExpiration oidcServiceOfflineSessionExpiration oidcStorage oidcStorageOptions oidcServiceDynamicRegistrationExportedVars oidcServiceDynamicRegistrationExtraClaims)];
our $oidcServiceParameters = [qw(oidcServiceMetaDataAuthorizeURI oidcServiceMetaDataTokenURI oidcServiceMetaDataUserInfoURI oidcServiceMetaDataJWKSURI oidcServiceMetaDataRegistrationURI oidcServiceMetaDataIntrospectionURI oidcServiceMetaDataEndSessionURI oidcServiceMetaDataCheckSessionURI oidcServiceMetaDataFrontChannelURI oidcServiceMetaDataBackChannelURI oidcServiceMetaDataAuthnContext oidcServicePrivateKeySig oidcServicePublicKeySig oidcServiceKeyIdSig oidcServiceAllowDynamicRegistration oidcServiceAllowOnlyDeclaredScopes oidcServiceAllowAuthorizationCodeFlow oidcServiceAllowImplicitFlow oidcServiceAllowHybridFlow oidcServiceAuthorizationCodeExpiration oidcServiceAccessTokenExpiration oidcServiceIDTokenExpiration oidcServiceOfflineSessionExpiration oidcStorage oidcStorageOptions oidcServiceDynamicRegistrationExportedVars oidcServiceDynamicRegistrationExtraClaims)];
1;

View File

@ -2488,6 +2488,10 @@ m[^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
'default' => 0,
'type' => 'bool'
},
'oidcServiceAllowOnlyDeclaredScopes' => {
'default' => 0,
'type' => 'bool'
},
'oidcServiceAuthorizationCodeExpiration' => {
'default' => 60,
'type' => 'int'

View File

@ -4097,6 +4097,11 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
default => 0,
documentation => 'OpenID Connect allow dynamic client registration',
},
oidcServiceAllowOnlyDeclaredScopes => {
type => 'bool',
default => 0,
documentation => 'OpenID Connect allow only declared scopes',
},
oidcServiceAllowAuthorizationCodeFlow => {
type => 'bool',
default => 1,

View File

@ -1336,6 +1336,7 @@ sub tree {
],
},
'oidcServiceAllowDynamicRegistration',
'oidcServiceAllowOnlyDeclaredScopes',
'oidcServiceAllowAuthorizationCodeFlow',
'oidcServiceAllowImplicitFlow',
'oidcServiceAllowHybridFlow',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"تدفق هجين",
"oidcServiceAllowImplicitFlow":"التدفق الضمني",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Hybrid Flow",
"oidcServiceAllowImplicitFlow":"Implicit Flow",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Hybrid Flow",
"oidcServiceAllowImplicitFlow":"Implicit Flow",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Flujo híbrido",
"oidcServiceAllowImplicitFlow":"Flujo implícito",
"oidcServiceAllowOffline":"Permitir acceso offline",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Caducidad del código de autorización",
"oidcServiceDynamicRegistrationExportedVars":"Variables exportadas para registro dinámico",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Hybrid Flow",
"oidcServiceAllowImplicitFlow":"Implicit Flow",
"oidcServiceAllowOffline":"Autoriser l'accès hors ligne",
"oidcServiceAllowOnlyDeclaredScopes":"N'autoriser que les scopes déclarés",
"oidcServiceAuthorizationCodeExpiration":"Expiration des codes d'autorisation",
"oidcServiceDynamicRegistrationExportedVars":"Variables exportées pour l'enregistrement dynamique",
"oidcServiceDynamicRegistrationExtraClaims":"Claims supplémentaires pour l'enregistrement dynamique",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Flusso ibrido",
"oidcServiceAllowImplicitFlow":"Flusso implicito",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Przepływ hybrydowy",
"oidcServiceAllowImplicitFlow":"Implikowany przepływ",
"oidcServiceAllowOffline":"Zezwalaj na dostęp offline",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Wygaśnięcie kodu autoryzacji",
"oidcServiceDynamicRegistrationExportedVars":"Zmienne wyeksportowane do dynamicznej rejestracji",
"oidcServiceDynamicRegistrationExtraClaims":"Dodatkowe roszczenia dotyczące rejestracji dynamicznej",

View File

@ -243,7 +243,7 @@
"crowdsec":"Aktivasyon",
"crowdsecAction":"Eylem",
"crowdsecKey":"API anahtarı",
"crowdsecUrl":"Base URL of local API",
"crowdsecUrl":"Yerel API'nin temel URL'si",
"cspConnect":"Ajax hedefleri",
"cspDefault":"Varsayılan değer",
"cspFont":"Font kaynağı",
@ -309,7 +309,7 @@
"demoParams":"Gösterim parametreleri",
"description":"Açıklama",
"dest":"Alıcı",
"devOpsCheck":"Check DevOps handler file",
"devOpsCheck":"DevOps eğitici dosyasını kontrol edin",
"diffViewer":"Fark görüntüleyici",
"diffWithPrevious":"önceki ile farkı",
"disablePersistentStorage":"Depolamayı devre dışı bırak",
@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Hibrit Akış",
"oidcServiceAllowImplicitFlow":"Kapalı Akış",
"oidcServiceAllowOffline":"Çevrimdışı erişime izin ver",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Yetkilendirme Kodu sona erme",
"oidcServiceDynamicRegistrationExportedVars":"Dinamik kayıtlanma için dışa aktarılan değişkenler",
"oidcServiceDynamicRegistrationExtraClaims":"Dinamik kayıtlanma için ekstra talepler",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Dòng chảy hỗn hợp",
"oidcServiceAllowImplicitFlow":"Dòng chảy ngầm",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"Hybrid Flow",
"oidcServiceAllowImplicitFlow":"Implicit Flow",
"oidcServiceAllowOffline":"Allow offline access",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"Authorization Code expiration",
"oidcServiceDynamicRegistrationExportedVars":"Exported vars for dynamic registration",
"oidcServiceDynamicRegistrationExtraClaims":"Extra claims for dynamic registration",

View File

@ -698,6 +698,7 @@
"oidcServiceAllowHybridFlow":"混合流程",
"oidcServiceAllowImplicitFlow":"內含流程",
"oidcServiceAllowOffline":"允許離線存取",
"oidcServiceAllowOnlyDeclaredScopes":"Only allow declared scopes",
"oidcServiceAuthorizationCodeExpiration":"授權碼到期",
"oidcServiceDynamicRegistrationExportedVars":"用於動態註冊的已匯出變數",
"oidcServiceDynamicRegistrationExtraClaims":"動態註冊的額外聲明",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -146,14 +146,22 @@ sub run {
$self->logger->debug("URL $url detected as an CAS LOGIN URL");
# GET parameters
my $service = $self->p->getHiddenFormValue( $req, 'service' )
|| $req->param('service');
my $cas_request = {};
foreach my $param (qw/service renew gateway/) {
$cas_request->{$param} =
$self->p->getHiddenFormValue( $req, $param )
|| $req->param($param);
}
my $h = $self->p->processHook( $req, 'casGotRequest', $cas_request );
return $h if ( $h != PE_OK );
my $service = $cas_request->{service};
$service = '' if ( $self->p->checkXSSAttack( 'service', $service ) );
my $renew = $self->p->getHiddenFormValue( $req, 'renew' )
|| $req->param('renew');
my $gateway = $self->p->getHiddenFormValue( $req, 'gateway' )
|| $req->param('gateway');
my $renew = $cas_request->{renew};
my $gateway = $cas_request->{gateway};
my $casServiceTicket;
# If no service defined, exit
@ -281,6 +289,10 @@ sub run {
$Sinfos->{_utime} = $time;
$Sinfos->{_casApp} = $app;
my $h = $self->p->processHook( $req, 'casGenerateServiceTicket',
$cas_request, $app, $Sinfos );
return $h if ( $h != PE_OK );
my $casServiceSession = $self->getCasSession( undef, $Sinfos );
unless ($casServiceSession) {
@ -296,8 +308,9 @@ sub run {
}
# Redirect to service
my $service_url = $service;
$service_url .= ( $service =~ /\?/ ? '&' : '?' )
# cas_request may have been modified by hook
my $service_url = $cas_request->{service};
$service_url .= ( $service_url =~ /\?/ ? '&' : '?' )
. build_urlencoded( ticket => $casServiceTicket );
$self->logger->debug("Redirect user to $service_url");
@ -542,6 +555,11 @@ sub validate {
# Return success message
$self->deleteCasSession($casServiceSession);
my $h =
$self->p->processHook( $req, 'casGenerateValidateResponse', $username );
return $self->returnCasValidateError() if ( $h != PE_OK );
return $self->returnCasValidateSuccess( $req, $username );
}
@ -839,6 +857,12 @@ sub _validate2 {
# Return success message
$self->deleteCasSession($casServiceSession);
my $h =
$self->p->processHook( $req, 'casGenerateValidateResponse', $username,
$attributes );
return $self->returnCasValidateError() if ( $h != PE_OK );
return $self->returnCasServiceValidateSuccess( $req, $username,
$casProxyGrantingTicketIOU, $proxies, $attributes );
}

View File

@ -700,21 +700,25 @@ sub run {
if ( $flow eq "authorizationcode" ) {
# Store data in session
my $codeSession = $self->newAuthorizationCode(
$rp,
{
code_challenge => $oidc_request->{'code_challenge'},
code_challenge_method =>
$oidc_request->{'code_challenge_method'},
nonce => $oidc_request->{'nonce'},
offline => $offline,
redirect_uri => $oidc_request->{'redirect_uri'},
scope => $scope,
req_scope => $req_scope,
client_id => $client_id,
user_session_id => $req->id,
}
);
my $code_payload = {
code_challenge => $oidc_request->{'code_challenge'},
code_challenge_method =>
$oidc_request->{'code_challenge_method'},
nonce => $oidc_request->{'nonce'},
offline => $offline,
redirect_uri => $oidc_request->{'redirect_uri'},
scope => $scope,
req_scope => $req_scope,
client_id => $client_id,
user_session_id => $req->id,
};
my $h = $self->p->processHook( $req, 'oidcGenerateCode',
$oidc_request, $rp, $code_payload );
return PE_ERROR if ( $h != PE_OK );
my $codeSession =
$self->newAuthorizationCode( $rp, $code_payload );
# Generate code
my $code = $codeSession->id();

View File

@ -56,45 +56,60 @@ sub loadSrv {
# Load CAS application list
sub loadApp {
my ($self) = @_;
if ( $self->conf->{casAppMetaDataOptions}
unless ( $self->conf->{casAppMetaDataOptions}
and %{ $self->conf->{casAppMetaDataOptions} } )
{
$self->casAppList( $self->conf->{casAppMetaDataOptions} );
}
else {
$self->logger->info("No CAS apps found in configuration");
}
foreach ( keys %{ $self->conf->{casAppMetaDataOptions} } ) {
my $valid = 1;
# Load access rule
my $rule = $self->conf->{casAppMetaDataOptions}->{$_}
my $rule =
$self->conf->{casAppMetaDataOptions}->{$_}
->{casAppMetaDataOptionsRule};
if ( length $rule ) {
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->error( 'CAS App rule error: '
$self->logger->error(
"Unable to build access rule for CAS Application $_: "
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
$valid = 0;
}
$self->spRules->{$_} = $rule;
}
# Load per-application macros
my $macros = $self->conf->{casAppMetaDataMacros}->{$_};
my $macros = $self->conf->{casAppMetaDataMacros}->{$_};
my $compiledMacros = {};
for my $macroAttr ( keys %{$macros} ) {
my $macroRule = $macros->{$macroAttr};
if ( length $macroRule ) {
$macroRule = $self->p->HANDLER->substitute($macroRule);
unless ( $macroRule = $self->p->HANDLER->buildSub($macroRule) )
{
$self->error( 'SAML SP macro error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
if ( $macroRule = $self->p->HANDLER->buildSub($macroRule) ) {
$compiledMacros->{$macroAttr} = $macroRule;
}
else {
$self->logger->error(
"Unable to build macro $macroAttr for CAS Application $_: "
. $self->p->HANDLER->tsv->{jail}->error );
$valid = 0;
}
$self->spMacros->{$_}->{$macroAttr} = $macroRule;
}
}
if ($valid) {
$self->casAppList->{$_} =
$self->conf->{casAppMetaDataOptions}->{$_};
$self->spRules->{$_} = $rule;
$self->spMacros->{$_} = $compiledMacros;
}
else {
$self->logger->error(
"CAS Application $_ has errors and will be ignored");
}
}
return 1;
}

View File

@ -34,6 +34,8 @@ use constant ADDRESS =>
[qw/formatted street_address locality region postal_code country/];
use constant PHONE => [qw/phone_number phone_number_verified/];
use constant OIDC_SCOPES => [qw/openid profile email address phone/];
# PROPERTIES
has oidcOPList => ( is => 'rw', default => sub { {} }, );
@ -105,8 +107,11 @@ sub loadRPs {
"No OpenID Connect Relying Party found in configuration");
return 1;
}
$self->oidcRPList( $self->conf->{oidcRPMetaDataOptions} );
foreach my $rp ( keys %{ $self->oidcRPList } ) {
foreach my $rp ( keys %{ $self->conf->{oidcRPMetaDataOptions} || {} } ) {
my $valid = 1;
# Handle attributes
my $attributes = {
profile => PROFILE,
email => EMAIL,
@ -125,50 +130,70 @@ sub loadRPs {
$attributes->{$claim} = \@extraAttributes;
}
}
$self->rpAttributes->{$rp} = $attributes;
my $rule = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsRule};
# Access rule
my $rule = $self->conf->{oidcRPMetaDataOptions}->{$rp}
->{oidcRPMetaDataOptionsRule};
if ( length $rule ) {
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->error( 'OIDC RP rule error: '
$self->logger->error( "Unable to build access rule for RP $rp: "
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
$valid = 0;
}
$self->spRules->{$rp} = $rule;
}
# Load per-RP macros
my $macros = $self->conf->{oidcRPMetaDataMacros}->{$rp};
my $macros = $self->conf->{oidcRPMetaDataMacros}->{$rp};
my $compiledMacros = {};
for my $macroAttr ( keys %{$macros} ) {
my $macroRule = $macros->{$macroAttr};
if ( length $macroRule ) {
$macroRule = $self->p->HANDLER->substitute($macroRule);
unless ( $macroRule = $self->p->HANDLER->buildSub($macroRule) )
{
$self->error( 'OIDC RP macro error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
if ( $macroRule = $self->p->HANDLER->buildSub($macroRule) ) {
$compiledMacros->{$macroAttr} = $macroRule;
}
else {
$self->logger->error(
"Unable to build macro $macroAttr for RP $rp:"
. $self->p->HANDLER->tsv->{jail}->error );
$valid = 0;
}
$self->spMacros->{$rp}->{$macroAttr} = $macroRule;
}
}
# Load per-RP dynamic scopes
my $scopes = $self->conf->{oidcRPMetaDataScopeRules}->{$rp};
my $scopes = $self->conf->{oidcRPMetaDataScopeRules}->{$rp};
my $compiledScopes = {};
for my $scopeName ( keys %{$scopes} ) {
my $scopeRule = $scopes->{$scopeName};
if ( length $scopeRule ) {
$scopeRule = $self->p->HANDLER->substitute($scopeRule);
unless ( $scopeRule = $self->p->HANDLER->buildSub($scopeRule) )
{
$self->error( 'OIDC RP dynamic scope rule error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
if ( $scopeRule = $self->p->HANDLER->buildSub($scopeRule) ) {
$compiledScopes->{$scopeName} = $scopeRule;
}
else {
$self->logger->error(
"Unable to build scope $scopeName for RP $rp:"
. $self->p->HANDLER->tsv->{jail}->error );
$valid = 0;
}
$self->spScopeRules->{$rp}->{$scopeName} = $scopeRule;
}
}
if ($valid) {
# Register RP
$self->oidcRPList->{$rp} =
$self->conf->{oidcRPMetaDataOptions}->{$rp};
$self->rpAttributes->{$rp} = $attributes;
$self->spMacros->{$rp} = $compiledMacros;
$self->spScopeRules->{$rp} = $compiledScopes;
$self->spRules->{$rp} = $rule;
}
else {
$self->logger->error(
"Relaying Party $rp has errors and will be ignored");
}
}
return 1;
}
@ -654,7 +679,7 @@ sub getUserInfo {
return $self->decodeUserInfo($userinfo_content);
}
elsif ( $content_type =~ /jwt/ ) {
return unless $self->verifyJWTSignature( $op, $userinfo_content );
return unless $self->verifyJWTSignature( $userinfo_content, $op );
return getJWTPayload($userinfo_content);
}
}
@ -1464,6 +1489,30 @@ sub getScope {
my @scope_values = split( /\s+/, $scope );
# Clean up unknown scopes
if ( $self->conf->{oidcServiceAllowOnlyDeclaredScopes} ) {
my @known_scopes = (
keys( %{ $self->spScopeRules->{$rp} || {} } ),
@{ OIDC_SCOPES() },
keys(
%{
$self->conf->{oidcRPMetaDataOptionsExtraClaims}->{$rp} || {}
}
)
);
my @scope_values_tmp;
for my $scope_value (@scope_values) {
if ( grep { $_ eq $scope_value } @known_scopes ) {
push @scope_values_tmp, $scope_value;
}
else {
$self->logger->warn(
"Unknown scope $scope_value requested for service $rp");
}
}
@scope_values = @scope_values_tmp;
}
# If this RP has dynamic scopes
if ( $self->spScopeRules->{$rp} ) {

View File

@ -394,6 +394,54 @@ sub loadSPs {
$sp_metadata = encode( "utf8", $sp_metadata );
}
# Get SP entityID
my ( $tmp, $entityID ) = ( $sp_metadata =~ /entityID=(['"])(.+?)\1/si );
# Decode HTML entities from entityID
# TODO: see Lasso comment below
decode_entities($entityID);
my $valid = 1;
my $rule = $self->conf->{samlSPMetaDataOptions}->{$_}
->{samlSPMetaDataOptionsRule};
if ( length $rule ) {
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->logger->error( 'SAML SP rule error: '
. $self->p->HANDLER->tsv->{jail}->error );
$valid = 0;
}
}
# Load per-SP macros
my $macros = $self->conf->{samlSPMetaDataMacros}->{$_};
my $compiledMacros = {};
for my $macroAttr ( keys %{$macros} ) {
my $macroRule = $macros->{$macroAttr};
if ( length $macroRule ) {
$macroRule = $self->p->HANDLER->substitute($macroRule);
if ( $macroRule = $self->p->HANDLER->buildSub($macroRule) ) {
$compiledMacros->{$macroAttr} = $macroRule;
}
else {
$valid = 0;
$self->logger->error(
"Error processing macro $macroAttr for SAML SP $_"
. $self->p->HANDLER->tsv->{jail}->error );
}
}
}
if ($valid) {
$self->spRules->{$_} = $rule;
$self->spMacros->{$entityID} = $compiledMacros;
}
else {
$self->logger->error("SAML SP $_ has errors and will be ignored");
next;
}
# Add this SP to Lasso::Server
# TODO: when Lasso issue #35061 is fixed in all distros,
# we could load the metadata into a new LassoProvider, extract the
@ -407,13 +455,7 @@ sub loadSPs {
next;
}
# Store SP entityID and Organization Name
my ( $tmp, $entityID ) = ( $sp_metadata =~ /entityID=(['"])(.+?)\1/si );
# Decode HTML entities from entityID
# TODO: see Lasso comment above
decode_entities($entityID);
# Store Org name
my $name = $self->getOrganizationName( $self->lassoServer, $entityID )
|| ucfirst($_);
$self->spList->{$entityID}->{confKey} = $_;
@ -460,34 +502,6 @@ sub loadSPs {
"Set signature method $signature_method on SP $_");
}
my $rule = $self->conf->{samlSPMetaDataOptions}->{$_}
->{samlSPMetaDataOptionsRule};
if ( length $rule ) {
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->logger->error( 'SAML SP rule error: '
. $self->p->HANDLER->tsv->{jail}->error );
next;
}
$self->spRules->{$_} = $rule;
}
# Load per-SP macros
my $macros = $self->conf->{samlSPMetaDataMacros}->{$_};
for my $macroAttr ( keys %{$macros} ) {
my $macroRule = $macros->{$macroAttr};
if ( length $macroRule ) {
$macroRule = $self->p->HANDLER->substitute($macroRule);
unless ( $macroRule = $self->p->HANDLER->buildSub($macroRule) )
{
$self->error( 'SAML SP macro error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
}
$self->spMacros->{$entityID}->{$macroAttr} = $macroRule;
}
}
$self->logger->debug("SP $_ added");
}
@ -747,13 +761,23 @@ sub addProvider {
and defined $role
and defined $metadata );
# https://dev.entrouvert.org/issues/51415
my $save_env = $ENV{'SSL_CERT_FILE'};
$ENV{'SSL_CERT_FILE'} = "/dev/null";
eval {
Lasso::Server::add_provider_from_buffer( $server, $role, $metadata,
$public_key, $ca_cert_chain );
};
return $self->checkLassoError($@);
if ( defined $save_env ) {
$ENV{'SSL_CERT_FILE'} = $save_env;
}
else {
delete $ENV{'SSL_CERT_FILE'};
}
return $self->checkLassoError($@);
}
## @method string getOrganizationName(Lasso::Server server, string idp)

View File

@ -91,9 +91,23 @@ sub _modifyPassword {
: PE_OK;
return $cpq unless ( $cpq == PE_OK );
my $hook_result = $self->p->processHook(
$req, 'passwordBeforeChange', $req->user,
$req->data->{newpassword},
$req->data->{oldpassword}
);
return $hook_result if ( $hook_result != PE_OK );
# Call password package
my $res = $self->modifyPassword( $req, $req->data->{newpassword} );
if ( $res == PE_PASSWORD_OK ) {
$self->p->processHook(
$req, 'passwordAfterChange', $req->user,
$req->data->{newpassword},
$req->data->{oldpassword}
);
$self->logger->debug( 'Update password in session for ' . $req->user );
my $userlog = $req->sessionInfo->{ $self->conf->{whatToTrace} };
my $iplog = $req->sessionInfo->{ipAddr};
@ -210,4 +224,33 @@ sub checkPasswordQuality {
return PE_OK;
}
# This method should be called when resetting the password
# in order to call the password hook
sub setNewPassword {
my ( $self, $req, $pwd, $useMail ) = @_;
my $hook_result =
$self->p->processHook( $req, 'passwordBeforeChange', $req->user, $pwd );
return $hook_result if ( $hook_result != PE_OK );
# Delegate to subclass
my $mod_result = $self->modifyPassword( $req, $pwd, $useMail );
if ( $mod_result == PE_PASSWORD_OK ) {
$hook_result =
$self->p->processHook( $req, 'passwordAfterChange', $req->user,
$pwd );
if ( $hook_result != PE_OK ) {
return $hook_result;
}
else {
return PE_PASSWORD_OK;
}
}
else {
return $mod_result;
}
}
1;

View File

@ -480,7 +480,7 @@ sub changePwd {
$req->user( $req->{sessionInfo}->{_user} );
my $result =
$self->p->_passwordDB->modifyPassword( $req,
$self->p->_passwordDB->setNewPassword( $req,
$req->data->{newpassword}, 1 );
$req->{user} = undef;

View File

@ -660,7 +660,7 @@ sub pwdReset {
return $self->p->sendError( $req, "User not found", 400 );
}
$result =
$self->p->_passwordDB->modifyPassword( $req, $password, $mail ? 1 : 0 );
$self->p->_passwordDB->setNewPassword( $req, $password, $mail ? 1 : 0 );
$req->{user} = undef;
$self->conf->{portalRequireOldPassword} = $tmp;

View File

@ -65,38 +65,50 @@ sub run {
my $moduleOptions = $self->conf->{globalStorageOptions} || {};
$moduleOptions->{backend} = $self->conf->{globalStorage};
my $sessions = $self->module->searchOn(
$moduleOptions,
$self->conf->{whatToTrace},
$req->{sessionInfo}->{ $self->conf->{whatToTrace} }
);
my $singleSessionRuleMatched =
$self->singleSessionRule->( $req, $req->sessionInfo );
my $singleIPRuleMatched = $self->singleIPRule->( $req, $req->sessionInfo );
my $singleUserByIPRuleMatched =
$self->singleUserByIPRule->( $req, $req->sessionInfo );
if ( $self->conf->{securedCookie} == 2 ) {
$self->logger->debug("Looking for double sessions...");
$linkedSessionId = $sessions->{ $req->id }->{_httpSession};
my $msg =
$linkedSessionId
? "Linked session found -> $linkedSessionId / " . $req->id
: "NO linked session found!";
$self->logger->debug($msg);
}
if ( $singleSessionRuleMatched
or $singleIPRuleMatched
or $self->conf->{notifyOther} )
{
my $sessions = $self->module->searchOn(
$moduleOptions,
$self->conf->{whatToTrace},
$req->{sessionInfo}->{ $self->conf->{whatToTrace} }
);
foreach my $id ( keys %$sessions ) {
next if ( $req->id eq $id );
next if ( $linkedSessionId and $id eq $linkedSessionId );
my $session = $self->p->getApacheSession($id) or next;
if (
$self->singleSessionRule->( $req, $req->sessionInfo )
or ( $self->singleIPRule->( $req, $req->sessionInfo )
and $req->{sessionInfo}->{ipAddr} ne $session->data->{ipAddr} )
)
{
push @$deleted, $self->p->_sumUpSession( $session->data );
$self->p->_deleteSession( $req, $session, 1 );
if ( $self->conf->{securedCookie} == 2 ) {
$self->logger->debug("Looking for double sessions...");
$linkedSessionId = $sessions->{ $req->id }->{_httpSession};
my $msg =
$linkedSessionId
? "Linked session found -> $linkedSessionId / " . $req->id
: "NO linked session found!";
$self->logger->debug($msg);
}
else {
push @$otherSessions, $self->p->_sumUpSession( $session->data );
push @otherSessionsId, $id;
foreach my $id ( keys %$sessions ) {
next if ( $req->id eq $id );
next if ( $linkedSessionId and $id eq $linkedSessionId );
my $session = $self->p->getApacheSession($id) or next;
if (
$self->singleSessionRule->( $req, $req->sessionInfo )
or ( $self->singleIPRule->( $req, $req->sessionInfo )
and $req->{sessionInfo}->{ipAddr} ne
$session->data->{ipAddr} )
)
{
push @$deleted, $self->p->_sumUpSession( $session->data );
$self->p->_deleteSession( $req, $session, 1 );
}
else {
push @$otherSessions, $self->p->_sumUpSession( $session->data );
push @otherSessionsId, $id;
}
}
}
@ -106,7 +118,7 @@ sub run {
}
) if @otherSessionsId;
if ( $self->singleUserByIPRule->( $req, $req->sessionInfo ) ) {
if ($singleUserByIPRuleMatched) {
my $sessions =
$self->module->searchOn( $moduleOptions, 'ipAddr',
$req->sessionInfo->{ipAddr} );

View File

@ -10,7 +10,7 @@
"PE101":"Parola izin verilmeyen karakterler içeriyor",
"PE102":"Oturum yükseltilmeli",
"PE103":"Hesabınız için ikinci faktör kullanılabilir değil",
"PE104":"Bad DevOps handler file",
"PE104":"Kötü DevOps eğitici dosyası",
"PE105":"Dosya bulunamadı",
"PE2":"Kullanıcı adı ve parola alanları doldurulmalı",
"PE20":"Parola back-end'i tanımlanmadı",
@ -128,7 +128,7 @@
"certificateReset":"Reset my certificate",
"changeKey":"Yeni anahtar üret",
"changePwd":"Parolanı değiştir",
"checkDevOps":"Check DevOps handler file",
"checkDevOps":"DevOps eğitici dosyasını kontrol edin",
"checkLastLogins":"Son girişlerimi kontrol et",
"checkUser":"Kullanıcı TOA profilini kontrol et",
"checkUserComputeSession":"Hesaplanan oturum verisi!",

View File

@ -0,0 +1,91 @@
use Test::More;
use strict;
use IO::String;
use JSON;
use Lemonldap::NG::Portal::Main::Constants
qw(PE_BADOLDPASSWORD PE_PASSWORD_MISMATCH PE_PP_MUST_SUPPLY_OLD_PASSWORD);
require 't/test-lib.pm';
my $res;
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
passwordDB => 'Demo',
portalRequireOldPassword => 1,
customPlugins => 't::PasswordHookPlugin',
}
}
);
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu' );
count(1);
# Try to authenticate
# -------------------
ok(
$res = $client->_post(
'/',
IO::String->new('user=dwho&password=dwho'),
length => 23
),
'Auth query'
);
count(1);
expectOK($res);
my $id = expectCookie($res);
# Test bad new password
my $s = buildForm( {
oldpassword => "dwho",
newpassword => "12345",
confirmpassword => "12345",
}
);
ok(
$res = $client->_post(
'/',
IO::String->new($s),
cookie => "lemonldap=$id",
accept => 'application/json',
length => length($s),
),
'Bad new password'
);
count(1);
expectReject( $res, 400, 28 );
# Test good new password
$s = buildForm( {
oldpassword => "dwho",
newpassword => "12346",
confirmpassword => "12346",
}
);
ok(
$res = $client->_post(
'/',
IO::String->new($s),
cookie => "lemonldap=$id",
accept => 'application/json',
length => length($s),
),
'Correct new password'
);
count(1);
expectReject( $res, 200, 35, "Expect PE_PASSWORD_OK" );
my $pdata = expectPdata($res);
is( $pdata->{afterHook}, "dwho-dwho-12346",
"passwordAfterChange hook worked as expected" );
count(1);
# Test $client->logout
$client->logout($id);
#print STDERR Dumper($res);
clean_sessions();
done_testing( count() );

View File

@ -0,0 +1,395 @@
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/oidc-lib.pm';
}
my $debug = 'error';
my ( $op, $rp, $res );
my $access_token;
LWP::Protocol::PSGI->register(
sub {
my $req = Plack::Request->new(@_);
ok( $req->uri =~ m#http://auth.((?:o|r)p).com(.*)#, ' REST request' );
my $host = $1;
my $url = $2;
my ( $res, $client );
count(1);
if ( $host eq 'op' ) {
pass(" Request from RP to OP, endpoint $url");
$client = $op;
}
elsif ( $host eq 'rp' ) {
pass(' Request from OP to RP');
$client = $rp;
}
else {
fail(' Aborting REST request (external)');
return [ 500, [], [] ];
}
if ( $req->method =~ /^post$/i ) {
my $s = $req->content;
ok(
$res = $client->_post(
$url, IO::String->new($s),
length => length($s),
type => $req->header('Content-Type'),
),
' Execute request'
);
}
else {
ok(
$res = $client->_get(
$url,
custom => {
HTTP_AUTHORIZATION => $req->header('Authorization'),
}
),
' Execute request'
);
}
ok( $res->[0] == 200, ' Response is 200' );
ok( getHeader( $res, 'Content-Type' ) =~ m#^application/j(son|wt)#,
' Content is JSON' )
or explain( $res->[1],
'Content-Type => application/json or application/jwt' );
count(4);
if ( $res->[2]->[0] =~ /"access_token":"(.*?)"/ ) {
$access_token = $1;
pass "Found access_token $access_token";
count(1);
}
return $res;
}
);
# Initialization
ok( $op = op(), 'OP portal' );
ok( $res = $op->_get('/oauth2/jwks'), 'Get JWKS, endpoint /oauth2/jwks' );
expectOK($res);
my $jwks = $res->[2]->[0];
ok(
$res = $op->_get('/.well-known/openid-configuration'),
'Get metadata, endpoint /.well-known/openid-configuration'
);
expectOK($res);
my $metadata = $res->[2]->[0];
count(3);
switch ('rp');
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
ok( $rp = rp( $jwks, $metadata ), 'RP portal' );
count(1);
# Query RP for auth
ok( $res = $rp->_get( '/', accept => 'text/html' ), 'Unauth SP request' );
count(1);
my ( $url, $query ) =
expectRedirection( $res, qr#http://auth.op.com(/oauth2/authorize)\?(.*)$# );
# Push request to OP
switch ('op');
ok( $res = $op->_get( $url, query => $query, accept => 'text/html' ),
"Push request to OP, endpoint $url" );
count(1);
expectOK($res);
# Try to authenticate to OP
$query = "user=french&password=french&$query";
ok(
$res = $op->_post(
$url,
IO::String->new($query),
accept => 'text/html',
length => length($query),
),
"Post authentication, endpoint $url"
);
count(1);
my $idpId = expectCookie($res);
my ( $host, $tmp );
( $host, $tmp, $query ) = expectForm( $res, '#', undef, 'confirm' );
ok(
$res = $op->_post(
$url,
IO::String->new($query),
accept => 'text/html',
cookie => "lemonldap=$idpId",
length => length($query),
),
"Post confirmation, endpoint $url"
);
count(1);
($query) = expectRedirection( $res, qr#^http://auth.rp.com/?\?(.*)$# );
# Push OP response to RP
switch ('rp');
ok( $res = $rp->_get( '/', query => $query, accept => 'text/html' ),
'Call openidconnectcallback on RP' );
count(1);
my $spId = expectCookie($res);
switch ('op');
ok(
$res = $op->_get( '/oauth2/checksession.html', accept => 'text.html' ),
'Check session, endpoint /oauth2/checksession.html'
);
count(1);
expectOK($res);
ok( getHeader( $res, 'Content-Security-Policy' ) !~ /frame-ancestors/,
' Frame can be embedded' )
or explain( $res->[1],
'Content-Security-Policy does not contain a frame-ancestors' );
count(1);
# Verify UTF-8
ok(
$res = $op->_get(
'/oauth2/userinfo', query => 'access_token=' . $access_token,
),
'Get userinfo'
);
count(1);
$res = expectJWT( $res->[2]->[0], name => 'Frédéric Accents' );
ok( $res = $op->_get("/sessions/global/$spId"), 'Get UTF-8' );
$res = expectJSON($res);
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
or explain( $res, 'cn => Frédéric Accents' );
count(2);
switch ('rp');
ok( $res = $rp->_get("/sessions/global/$spId"), 'Get UTF-8' );
$res = expectJSON($res);
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
or explain( $res, 'cn => Frédéric Accents' );
count(2);
# Logout initiated by RP
ok(
$res = $rp->_get(
'/',
query => 'logout',
cookie => "lemonldap=$spId",
accept => 'text/html'
),
'Query RP for logout'
);
count(1);
( $url, $query ) = expectRedirection( $res,
qr#http://auth.op.com(/oauth2/logout)\?(post_logout_redirect_uri=.+)$# );
# Push logout to OP
switch ('op');
ok(
$res = $op->_get(
$url,
query => $query,
cookie => "lemonldap=$idpId",
accept => 'text/html'
),
"Push logout request to OP, endpoint $url"
);
count(1);
( $host, $tmp, $query ) = expectForm( $res, '#', undef, 'confirm' );
ok(
$res = $op->_post(
$url, IO::String->new($query),
length => length($query),
cookie => "lemonldap=$idpId",
accept => 'text/html',
),
"Confirm logout, endpoint $url"
);
count(1);
( $url, $query ) = expectRedirection( $res, qr#.# );
my $removedCookie = expectCookie($res);
is( $removedCookie, 0, "SSO cookie removed" );
count(1);
# Test logout endpoint without session
ok(
$res = $op->_get(
'/oauth2/logout',
accept => 'text/html',
query => 'post_logout_redirect_uri=http://auth.rp.com/?logout=1'
),
'logout endpoint with redirect, endpoint /oauth2/logout'
);
count(1);
expectRedirection( $res, 'http://auth.rp.com/?logout=1' );
ok( $res = $op->_get('/oauth2/logout'),
'logout endpoint, endpoint /oauth2/logout' );
count(1);
expectReject($res);
# Test if logout is done
ok(
$res = $op->_get(
'/', cookie => "lemonldap=$idpId",
),
'Test if user is reject on IdP'
);
count(1);
expectReject($res);
switch ('rp');
ok(
$res = $rp->_get(
'/',
accept => 'text/html',
cookie => "lemonldap=$spId"
),
'Test if user is reject on SP'
);
count(1);
( $url, $query ) =
expectRedirection( $res, qr#^http://auth.op.com(/oauth2/authorize)\?(.*)$# );
# Test if consent was saved
# -------------------------
# Push request to OP
switch ('op');
ok( $res = $op->_get( $url, query => $query, accept => 'text/html' ),
"Push request to OP, endpoint $url" );
count(1);
expectOK($res);
# Try to authenticate to OP
$query = "user=french&password=french&$query";
ok(
$res = $op->_post(
$url,
IO::String->new($query),
accept => 'text/html',
length => length($query),
),
"Post authentication, endpoint $url"
);
count(1);
$idpId = expectCookie($res);
#expectRedirection( $res, qr#^http://auth.rp.com/# );
#print STDERR Dumper($res);
clean_sessions();
done_testing( count() );
sub op {
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'idp.com',
portal => 'http://auth.op.com/',
authentication => 'Demo',
userDB => 'Same',
issuerDBOpenIDConnectActivation => "1",
restSessionServer => 1,
oidcRPMetaDataExportedVars => {
rp => {
email => "mail",
family_name => "cn",
name => "cn"
}
},
oidcServiceAllowHybridFlow => 1,
oidcServiceAllowImplicitFlow => 1,
oidcServiceAllowAuthorizationCodeFlow => 1,
oidcRPMetaDataOptions => {
rp => {
oidcRPMetaDataOptionsDisplayName => "RP",
oidcRPMetaDataOptionsIDTokenExpiration => 3600,
oidcRPMetaDataOptionsClientID => "rpid",
oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
oidcRPMetaDataOptionsBypassConsent => 0,
oidcRPMetaDataOptionsClientSecret => "rpsecret",
oidcRPMetaDataOptionsUserIDAttr => "",
oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
oidcRPMetaDataOptionsUserInfoSignAlg => "HS512",
oidcRPMetaDataOptionsPostLogoutRedirectUris =>
"http://auth.rp.com/?logout=1"
}
},
oidcOPMetaDataOptions => {},
oidcOPMetaDataJSON => {},
oidcOPMetaDataJWKS => {},
oidcServiceMetaDataAuthnContext => {
'loa-4' => 4,
'loa-1' => 1,
'loa-5' => 5,
'loa-2' => 2,
'loa-3' => 3
},
oidcServicePrivateKeySig => oidc_key_op_private_sig,
oidcServicePublicKeySig => oidc_key_op_public_sig,
}
}
);
}
sub rp {
my ( $jwks, $metadata ) = @_;
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'rp.com',
portal => 'http://auth.rp.com/',
authentication => 'OpenIDConnect',
userDB => 'Same',
restSessionServer => 1,
oidcOPMetaDataExportedVars => {
op => {
cn => "name",
uid => "sub",
sn => "family_name",
mail => "email"
}
},
oidcOPMetaDataOptions => {
op => {
oidcOPMetaDataOptionsCheckJWTSignature => 1,
oidcOPMetaDataOptionsJWKSTimeout => 0,
oidcOPMetaDataOptionsClientSecret => "rpsecret",
oidcOPMetaDataOptionsScope => "openid profile",
oidcOPMetaDataOptionsStoreIDToken => 0,
oidcOPMetaDataOptionsMaxAge => 30,
oidcOPMetaDataOptionsDisplay => "",
oidcOPMetaDataOptionsClientID => "rpid",
oidcOPMetaDataOptionsConfigurationURI =>
"https://auth.op.com/.well-known/openid-configuration"
}
},
oidcOPMetaDataJWKS => {
op => $jwks,
},
oidcOPMetaDataJSON => {
op => $metadata,
}
}
}
);
}

View File

@ -0,0 +1,110 @@
use lib 'inc';
use Test::More; # skip_all => 'CAS is in rebuild';
use strict;
use IO::String;
use LWP::UserAgent;
use LWP::Protocol::PSGI;
use MIME::Base64;
BEGIN {
require 't/test-lib.pm';
}
my $debug = 'error';
my ( $issuer, $res );
eval { require XML::Simple };
plan skip_all => "Missing dependencies: $@" if ($@);
ok( $issuer = issuer(), 'Issuer portal' );
count(1);
my $s = "user=french&password=french";
# Login
ok(
$res = $issuer->_post(
'/',
IO::String->new($s),
accept => 'text/html',
length => length($s),
),
'Post authentication'
);
count(1);
my $idpId = expectCookie($res);
# Hook should make it fail with status 999
ok(
$res = $issuer->_get(
'/cas/login',
cookie => "lemonldap=$idpId",
query => 'service=http://auth.sp2.com/',
accept => 'text/html'
),
'Query CAS server'
);
count(1);
expectPortalError( $res, 999, "Hook rejected the request" );
ok(
$res = $issuer->_get(
'/cas/login',
cookie => "lemonldap=$idpId",
query => 'service=http://auth.sp.com/',
accept => 'text/html'
),
'Query CAS server'
);
count(1);
my ($query) =
expectRedirection( $res, qr#^http://auth.sp.com/\?hooked=1&(ticket=[^&]+)$# );
ok(
$res = $issuer->_get(
'/cas/p3/serviceValidate',
query => 'service=http://auth.sp.com/&' . $query,
accept => 'text/html'
),
'Query CAS server'
);
expectOK($res);
count(1);
ok( $res->[2]->[0] =~ m#<cas:hooked>1</cas:hooked>#, "Found hook attribute" );
count(1);
clean_sessions();
done_testing( count() );
sub issuer {
return LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'idp.com',
portal => 'http://auth.idp.com',
authentication => 'Demo',
userDB => 'Same',
issuerDBCASActivation => 1,
casAttr => 'uid',
casAppMetaDataOptions => {
sp => {
casAppMetaDataOptionsService => 'http://auth.sp.com/',
},
},
casAppMetaDataExportedVars => {
sp => {
cn => 'cn',
mail => 'mail',
uid => 'uid',
},
},
casAccessControlPolicy => 'error',
multiValuesSeparator => ';',
customPlugins => 't::CasHookPlugin',
}
}
);
}

View File

@ -90,7 +90,8 @@ ok(
"Get authorization code"
);
my ($code) = expectRedirection( $res, qr#http://rp2\.com/.*code=([^\&]*)# );
my ($code) =
expectRedirection( $res, qr#http://rp2\.com/\?hooked=1.*code=([^\&]*)# );
# Exchange code for AT
$query =

View File

@ -17,13 +17,14 @@ my $debug = 'error';
# Initialization
my $op = LLNG::Manager::Test->new( {
ini => {
logLevel => $debug,
domain => 'op.com',
portal => 'http://auth.op.com',
authentication => 'Demo',
userDB => 'Same',
issuerDBOpenIDConnectActivation => 1,
oidcRPMetaDataExportedVars => {
logLevel => $debug,
domain => 'op.com',
portal => 'http://auth.op.com',
authentication => 'Demo',
userDB => 'Same',
issuerDBOpenIDConnectActivation => 1,
oidcServiceAllowOnlyDeclaredScopes => 1,
oidcRPMetaDataExportedVars => {
rp => {
email => "mail",
family_name => "cn",
@ -37,13 +38,18 @@ my $op = LLNG::Manager::Test->new( {
},
oidcRPMetaDataScopeRules => {
rp => {
"read" => '$requested and $uid eq "french"',
"write" => '$uid eq "russian"',
"read" => '$requested and $uid eq "french"',
"write" => '$uid eq "russian"',
"ifrequested" => '$requested and $uid eq "french"',
"always" => '$uid eq "french"',
"always" => '$uid eq "french"',
},
},
oidcRPMetaDataOptions => {
oidcRPMetaDataOptionsExtraClaims => {
rp => {
extrascope => "dummy",
},
},
oidcRPMetaDataOptions => {
rp => {
oidcRPMetaDataOptionsDisplayName => "RP",
oidcRPMetaDataOptionsIDTokenExpiration => 3600,
@ -73,7 +79,7 @@ my $code = authorize(
$op, $idpId,
{
response_type => "code",
scope => "openid profile email read write",
scope => "openid profile email read write extrascope unknown",
client_id => "rpid",
state => "af0ifjsldkj",
redirect_uri => "http://rp2.com/"
@ -85,7 +91,7 @@ my $json = expectJSON( codeGrant( $op, "rpid", $code, "http://rp2.com/" ) );
my $token = $json->{access_token};
ok( $token, 'Access token present' );
my $token_resp_scope = $json->{scope};
ok ($token_resp_scope, 'Token response returned granted scopes');
ok( $token_resp_scope, 'Token response returned granted scopes' );
my ( $res, $query );
@ -126,11 +132,20 @@ is( $json->{client_id}, "rpid", "Response contains the correct client id" );
like( $json->{scope}, qr/\bopenid\b/, "Response contains the default scopes" );
like( $json->{scope}, qr/\bprofile\b/, "Response contains the default scopes" );
like( $json->{scope}, qr/\bemail\b/, "Response contains the default scopes" );
unlike( $json->{scope}, qr/\bwrite\b/, "Response omits a dynamic scope that evaluates to false" );
unlike( $json->{scope}, qr/\bifrequested\b/, "Response omits a dynamic scope that was not requested" );
like( $json->{scope}, qr/\bread\b/, "Response contains a dynamic scope that is sent only when requested" );
like( $json->{scope}, qr/\balways\b/, "Response contains a dynamic scope that is not requested but always sent" );
is ($token_resp_scope, $json->{scope}, "Token response scope matches token scope");
unlike( $json->{scope}, qr/\bwrite\b/,
"Response omits a dynamic scope that evaluates to false" );
unlike( $json->{scope}, qr/\bifrequested\b/,
"Response omits a dynamic scope that was not requested" );
like( $json->{scope}, qr/\bread\b/,
"Response contains a dynamic scope that is sent only when requested" );
like( $json->{scope}, qr/\balways\b/,
"Response contains a dynamic scope that is not requested but always sent" );
unlike( $json->{scope}, qr/\bunknown\b/,
"Response omits a scope that is not declared anywhere" );
like( $json->{scope}, qr/\bextrascope\b/,
"Response contains scope coming from extra claims definition" );
is( $token_resp_scope, $json->{scope},
"Token response scope matches token scope" );
# Check status after expiration
Time::Fake->offset("+2h");

View File

@ -0,0 +1,134 @@
use Test::More;
use strict;
use IO::String;
BEGIN {
eval {
require 't/test-lib.pm';
require 't/smtp.pm';
};
}
my ( $res, $user, $pwd );
my $maintests = 15;
SKIP: {
eval
'require Email::Sender::Simple;use GD::SecurityImage;use Image::Magick;';
if ($@) {
skip 'Missing dependencies', $maintests;
}
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
useSafeJail => 1,
portalDisplayRegister => 1,
authentication => 'Demo',
userDB => 'Same',
passwordDB => 'Demo',
captcha_mail_enabled => 0,
portalDisplayResetPassword => 1,
customPlugins => 't::PasswordHookPlugin',
}
}
);
# Test form
# ------------------------
ok( $res = $client->_get( '/resetpwd', accept => 'text/html' ),
'Reset form', );
my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'mail' );
$query = 'mail=dwho%40badwolf.org';
# Post email
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($query),
length => length($query),
accept => 'text/html',
cookie => 'llnglanguage=en',
),
'Post mail'
);
like( mail(), qr#<span>Hello</span>#, "Found english greeting" );
ok( mail() =~ m#a href="http://auth.example.com/resetpwd\?(.*?)"#,
'Found link in mail' );
$query = $1;
ok(
$res = $client->_get(
'/resetpwd',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /newpassword/s, ' Ask for a new password' );
my $badquery = $query . '&newpassword=12345&confirmpassword=12345';
# Post failing password
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($badquery),
length => length($badquery),
accept => 'text/html'
),
'Post new password'
);
expectPortalError( $res, 28 );
# Post email again
$query = 'mail=dwho%40badwolf.org';
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($query),
length => length($query),
accept => 'text/html',
cookie => 'llnglanguage=en',
),
'Post mail'
);
like( mail(), qr#<span>Hello</span>#, "Found english greeting" );
ok( mail() =~ m#a href="http://auth.example.com/resetpwd\?(.*?)"#,
'Found link in mail' );
$query = $1;
ok(
$res = $client->_get(
'/resetpwd',
query => $query,
accept => 'text/html'
),
'Post mail token received by mail'
);
( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
ok( $res->[2]->[0] =~ /newpassword/s, ' Ask for a new password' );
my $goodquery = $query . '&newpassword=12346&confirmpassword=12346';
# Post accepted password
ok(
$res = $client->_post(
'/resetpwd', IO::String->new($goodquery),
length => length($goodquery),
accept => 'text/html'
),
'Post new password'
);
my $pdata = expectPdata($res);
is( $pdata->{afterHook}, "dwho--12346",
"passwordAfterChange hook worked as expected" );
ok( mail() =~ /Your password was changed/, 'Password was changed' );
}
count($maintests);
clean_sessions();
done_testing( count() );

View File

@ -0,0 +1,42 @@
package t::CasHookPlugin;
use Mouse;
extends 'Lemonldap::NG::Portal::Main::Plugin';
use constant hook => {
casGotRequest => 'filterService',
'casGenerateServiceTicket' => 'changeRedirectUrl',
'casGenerateValidateResponse' => 'genResponse',
};
sub init {
my ($self) = @_;
return 1;
}
sub filterService {
my ( $self, $req, $cas_request ) = @_;
if ( $cas_request->{service} eq "http://auth.sp.com/" ) {
return 0;
}
else {
return 999;
}
}
sub changeRedirectUrl {
my ( $self, $req, $cas_request, $app, $Sinfos ) = @_;
$cas_request->{service} .= "?hooked=1";
return 0;
}
sub genResponse {
my ( $self, $req, $username, $attributes ) = @_;
$attributes->{hooked} = 1;
return 0;
}
1;

View File

@ -8,6 +8,7 @@ use Data::Dumper;
use Test::More;
use constant hook => {
oidcGenerateCode => 'modifyRedirectUri',
oidcGenerateIDToken => 'addClaimToIDToken',
oidcGenerateUserInfoResponse => 'addClaimToUserInfo',
oidcGotRequest => 'addScopeToRequest',
@ -47,6 +48,13 @@ sub addHardcodedScope {
return PE_OK;
}
sub modifyRedirectUri {
my ( $self, $req, $oidc_request, $rp, $code_payload ) = @_;
my $original_uri = $oidc_request->{redirect_uri};
$oidc_request->{redirect_uri} = "$original_uri?hooked=1";
return PE_OK;
}
sub addClaimToAccessToken {
my ( $self, $req, $payload, $rp ) = @_;
$payload->{"access_token_hook"} = 1;

View File

@ -0,0 +1,34 @@
package t::PasswordHookPlugin;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants
qw/PE_PP_INSUFFICIENT_PASSWORD_QUALITY PE_OK/;
extends 'Lemonldap::NG::Portal::Main::Plugin';
use constant hook => {
passwordBeforeChange => 'beforeChange',
passwordAfterChange => 'afterChange',
};
sub init {
1;
}
sub beforeChange {
my ( $self, $req, $user, $password, $old ) = @_;
if ( $password eq "12345" ) {
$self->logger->error("I've got the same combination on my luggage");
return PE_PP_INSUFFICIENT_PASSWORD_QUALITY;
}
return PE_OK;
}
sub afterChange {
my ( $self, $req, $user, $password, $old ) = @_;
$old ||= "";
$req->pdata->{afterHook} = "$user-$old-$password";
$self->logger->debug("Password changed for $user: $old -> $password");
return PE_OK;
}
1;

View File

@ -156,8 +156,10 @@ sub expectJWT {
my ( $token, %claims ) = @_;
my $payload = getJWTPayload($token);
ok( $payload, "Token is a JWT" );
count(1);
for my $claim ( keys %claims ) {
is( $payload->{$claim}, $claims{$claim}, "Found claim in JWT" );
count(1);
}
return $payload;
}

View File

@ -455,6 +455,23 @@ sub expectCookie {
return $id;
}
=head4 expectPdata( $res );
Check if the pdata cookie exists and returns its deserialized value.
=cut
sub expectPdata {
my ($res) = @_;
my $val = expectCookie( $res, "lemonldappdata" );
ok( $val, "Pdata is not empty" );
count(1);
my $pdata;
eval { $pdata = JSON::from_json( uri_unescape($val) ); };
diag($@) if $@;
return $pdata;
}
=head4 exceptCspFormOK( $res, $host )
Verify that C<Content-Security-Policy> header allows one to connect to $host.
@ -487,9 +504,9 @@ sub expectCspChildOK {
my ( $res, $host ) = @_;
return 1 unless ($host);
my $csp = getHeader( $res, 'Content-Security-Policy' );
ok($csp, "Content-Security-Policy header found");
ok( $csp, "Content-Security-Policy header found" );
count(1);
like($csp, qr/child-src[^;]*\Q$host\E/, "Found $host in CSP child-src");
like( $csp, qr/child-src[^;]*\Q$host\E/, "Found $host in CSP child-src" );
count(1);
}