Merge branch 'v2.0'
This commit is contained in:
commit
a3b24418c6
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' );
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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} ) {
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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} );
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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() );
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 =
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() );
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue