Merge remote-tracking branch 'origin/master' into favapps
This commit is contained in:
commit
e9b26bb79a
|
@ -270,6 +270,7 @@ Depends: ${misc:Depends},
|
|||
Recommends: libcrypt-openssl-bignum-perl,
|
||||
libconvert-base32-perl,
|
||||
libemail-sender-perl (>=1.300027) | libemail-sender-transport-smtps-perl,
|
||||
libio-string-perl,
|
||||
libipc-run-perl,
|
||||
libgd-securityimage-perl,
|
||||
libmime-tools-perl,
|
||||
|
|
|
@ -24,7 +24,7 @@ use constant MANAGERSECTION => "manager";
|
|||
use constant SESSIONSEXPLORERSECTION => "sessionsExplorer";
|
||||
use constant APPLYSECTION => "apply";
|
||||
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va)|issuerDBGetParamete)r|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|macro)s|o(?:idc(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|c(?:as(?:S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions)|A(?:ppMetaData(?:(?:ExportedVar|Option)s|Node)|ttributes))|(?:ustomAddParam|ombModule)s)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
|
||||
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)|ingle(?:Session(?:UserByIP)?|(?:UserBy)?IP)|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|kipRenewConfirmation|howLanguages|slByAjax)|o(?:idc(?:ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken)|RPMetaDataOptions(?:LogoutSessionRequired|BypassConsent))|ldNotifFormat)|p(?:ortal(?:ErrorOn(?:ExpiredSession|MailNotFound)|DisplayRe(?:setPassword|gister)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|RequireOldPassword|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:Group(?:DecodeSearchedValu|Recursiv)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl)|oginHistoryEnabled)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:PersistentInfo|EmptyValues))?|State|XSS)|da)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonation(?:SkipEmptyValue|MergeSSOgroup)s)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|no(?:tif(?:ication(?:Server)?|y(?:Deleted|Other))|AjaxHook)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|(?:(?:rest(?:Session|Config)|wsdl)Serv|activeTim)er|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|dbiDynamicHashEnabled|bruteForceProtection)$/;
|
||||
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)|ingle(?:Session(?:UserByIP)?|(?:UserBy)?IP)|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|kipRenewConfirmation|howLanguages|slByAjax)|o(?:idc(?:ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|RPMetaDataOptions(?:LogoutSessionRequired|BypassConsent|RequirePKCE|Public)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|p(?:ortal(?:ErrorOn(?:ExpiredSession|MailNotFound)|DisplayRe(?:setPassword|gister)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|RequireOldPassword|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:Group(?:DecodeSearchedValu|Recursiv)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl)|oginHistoryEnabled)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:PersistentInfo|EmptyValues))?|State|XSS)|da)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonation(?:SkipEmptyValue|MergeSSOgroup)s)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|no(?:tif(?:ication(?:Server)?|y(?:Deleted|Other))|AjaxHook)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|(?:(?:rest(?:Session|Config)|wsdl)Serv|activeTim)er|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|dbiDynamicHashEnabled|bruteForceProtection)$/;
|
||||
|
||||
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ our $specialNodeHash = {
|
|||
our $doubleHashKeys = 'issuerDBGetParameters';
|
||||
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|c(?:as(?:StorageOption|Attribute)|ustomAddParam|ombModule)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|S(?:MTPTLSOpts|SLVarIf))';
|
||||
our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaDataNode|virtualHost)s';
|
||||
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:Servic|Rul)e|ExportedVars)';
|
||||
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:UserAttribut|Servic|Rul)e|ExportedVars)';
|
||||
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';
|
||||
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|S(?:toreIDToken|ortNumber|cope)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
|
||||
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:(?:PostLogoutRedirectUri|ExtraClaim)s|I(?:DToken(?:Expiration|SignAlg)|con)|Logout(?:SessionRequired|Type|Url)|AccessTokenExpiration|R(?:edirectUris|ule)|Client(?:Secret|ID)|BypassConsent|DisplayName|UserIDAttr)|ExportedVars)';
|
||||
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:I(?:DToken(?:Expiration|SignAlg)|con)|Logout(?:SessionRequired|Type|Url)|R(?:e(?:directUris|quirePKCE)|ule)|P(?:ostLogoutRedirectUris|ublic)|AccessTokenExpiration|Client(?:Secret|ID)|BypassConsent|DisplayName|ExtraClaims|UserIDAttr)|ExportedVars)';
|
||||
our $samlIDPMetaDataNodeKeys = 'samlIDPMetaData(?:Options(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|EncryptionMod|UserAttribut|DisplayNam)e|S(?:ignS[LS]OMessage|toreSAMLToken|[LS]OBinding|ortNumber)|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Re(?:questedAuthnContext|solutionRule|layStateURL)|Force(?:Authn|UTF8)|I(?:sPassive|con)|NameIDFormat)|ExportedAttributes|XML)';
|
||||
our $samlSPMetaDataNodeKeys = 'samlSPMetaData(?:Options(?:N(?:ameID(?:SessionKey|Format)|otOnOrAfterTimeout)|S(?:essionNotOnOrAfterTimeout|ignS[LS]OMessage)|(?:CheckS[LS]OMessageSignatur|OneTimeUs|Rul)e|En(?:ableIDPInitiatedURL|cryptionMode)|ForceUTF8)|ExportedAttributes|XML)';
|
||||
our $virtualHostKeys = '(?:vhost(?:A(?:uthnLevel|liases)|(?:Maintenanc|Typ)e|Https|Port)|(?:exportedHeader|locationRule)s|post)';
|
||||
|
|
|
@ -678,6 +678,9 @@ sub attributes {
|
|||
'casAppMetaDataOptionsService' => {
|
||||
'type' => 'url'
|
||||
},
|
||||
'casAppMetaDataOptionsUserAttribute' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
'casAttr' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
|
@ -2002,9 +2005,17 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
|
|||
'oidcRPMetaDataOptionsPostLogoutRedirectUris' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
'oidcRPMetaDataOptionsPublic' => {
|
||||
'default' => 0,
|
||||
'type' => 'bool'
|
||||
},
|
||||
'oidcRPMetaDataOptionsRedirectUris' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
'oidcRPMetaDataOptionsRequirePKCE' => {
|
||||
'default' => 0,
|
||||
'type' => 'bool'
|
||||
},
|
||||
'oidcRPMetaDataOptionsRule' => {
|
||||
'test' => sub {
|
||||
my ( $val, $conf ) = @_;
|
||||
|
|
|
@ -1836,6 +1836,10 @@ sub attributes {
|
|||
type => 'url',
|
||||
documentation => 'CAS App service',
|
||||
},
|
||||
casAppMetaDataOptionsUserAttribute => {
|
||||
type => 'text',
|
||||
documentation => 'CAS User attribute',
|
||||
},
|
||||
casAppMetaDataOptionsRule => {
|
||||
type => 'text',
|
||||
test => $perlExpr,
|
||||
|
@ -3361,11 +3365,6 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
|
|||
oidcOPMetaDataOptionsIcon => { type => 'text', },
|
||||
oidcOPMetaDataOptionsStoreIDToken => { type => 'bool', default => 0 },
|
||||
oidcOPMetaDataOptionsSortNumber => { type => 'int', },
|
||||
oidcRPMetaDataOptionsRule => {
|
||||
type => 'text',
|
||||
test => $perlExpr,
|
||||
documentation => 'Rule to grant access to this SP',
|
||||
},
|
||||
|
||||
# OpenID Connect relying parties
|
||||
oidcRPMetaDataExportedVars => {
|
||||
|
@ -3425,6 +3424,21 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
|
|||
default => 0,
|
||||
documentation => 'Session required for logout',
|
||||
},
|
||||
oidcRPMetaDataOptionsPublic => {
|
||||
type => 'bool',
|
||||
default => 0,
|
||||
documentation => 'Declare this RP as public client',
|
||||
},
|
||||
oidcRPMetaDataOptionsRequirePKCE => {
|
||||
type => 'bool',
|
||||
default => 0,
|
||||
documentation => 'Require PKCE',
|
||||
},
|
||||
oidcRPMetaDataOptionsRule => {
|
||||
type => 'text',
|
||||
test => $perlExpr,
|
||||
documentation => 'Rule to grant access to this RP',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -194,7 +194,9 @@ sub cTrees {
|
|||
form => 'simpleInputContainer',
|
||||
nodes => [
|
||||
'oidcRPMetaDataOptionsClientID',
|
||||
'oidcRPMetaDataOptionsClientSecret'
|
||||
'oidcRPMetaDataOptionsClientSecret',
|
||||
'oidcRPMetaDataOptionsPublic',
|
||||
'oidcRPMetaDataOptionsRequirePKCE',
|
||||
]
|
||||
},
|
||||
'oidcRPMetaDataOptionsUserIDAttr',
|
||||
|
@ -252,6 +254,7 @@ sub cTrees {
|
|||
form => 'simpleInputContainer',
|
||||
nodes => [
|
||||
'casAppMetaDataOptionsService',
|
||||
'casAppMetaDataOptionsUserAttribute',
|
||||
'casAppMetaDataOptionsRule'
|
||||
]
|
||||
},
|
||||
|
|
|
@ -17,6 +17,11 @@ function templates(tpl,key) {
|
|||
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsService",
|
||||
"title" : "casAppMetaDataOptionsService"
|
||||
},
|
||||
{
|
||||
"get" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsUserAttribute",
|
||||
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsUserAttribute",
|
||||
"title" : "casAppMetaDataOptionsUserAttribute"
|
||||
},
|
||||
{
|
||||
"get" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsRule",
|
||||
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsRule",
|
||||
|
@ -405,6 +410,20 @@ function templates(tpl,key) {
|
|||
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsClientSecret",
|
||||
"title" : "oidcRPMetaDataOptionsClientSecret",
|
||||
"type" : "password"
|
||||
},
|
||||
{
|
||||
"default" : 0,
|
||||
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsPublic",
|
||||
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsPublic",
|
||||
"title" : "oidcRPMetaDataOptionsPublic",
|
||||
"type" : "bool"
|
||||
},
|
||||
{
|
||||
"default" : 0,
|
||||
"get" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRequirePKCE",
|
||||
"id" : tpl+"s/"+key+"/"+"oidcRPMetaDataOptionsRequirePKCE",
|
||||
"title" : "oidcRPMetaDataOptionsRequirePKCE",
|
||||
"type" : "bool"
|
||||
}
|
||||
],
|
||||
"id" : "oidcRPMetaDataOptionsAuthentication",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"خيارات",
|
||||
"casAppMetaDataOptionsService":"خدمة أل يو أر ل",
|
||||
"casAppMetaDataOptionsRule":"القاعدة",
|
||||
"casAppMetaDataOptionsUserAttribute":"خاصّيّة المستخدم",
|
||||
"casAppName":"اسم التطبيق كاس",
|
||||
"casAttr":"تسجيل الدخول كاس",
|
||||
"casAttributes":"السمات المصدرة لي كاس",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"نوع",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"يو آر إل",
|
||||
"oidcOPMetaDataOptionsProtocol":"بروتوكول",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"قاعدة الدخول",
|
||||
"oidcOPMetaDataOptionsScope":"نطاق",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"مخزن تعريف التوكن",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"Optionen",
|
||||
"casAppMetaDataOptionsService":"Service URL",
|
||||
"casAppMetaDataOptionsRule":"Regel",
|
||||
"casAppMetaDataOptionsUserAttribute":"User attribute",
|
||||
"casAppName":"CAS App Name",
|
||||
"casAttr":"CAS login",
|
||||
"casAttributes":"CAS exported attributes",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Type",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Protocol",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"Access rule",
|
||||
"oidcOPMetaDataOptionsScope":"Scope",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"Options",
|
||||
"casAppMetaDataOptionsService":"Service URL",
|
||||
"casAppMetaDataOptionsRule":"Rule",
|
||||
"casAppMetaDataOptionsUserAttribute":"User attribute",
|
||||
"casAppName":"CAS App Name",
|
||||
"casAttr":"CAS login",
|
||||
"casAttributes":"CAS exported attributes",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Type",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Protocol",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"Access rule",
|
||||
"oidcOPMetaDataOptionsScope":"Scope",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"Options",
|
||||
"casAppMetaDataOptionsService":"URL du service",
|
||||
"casAppMetaDataOptionsRule":"Règle",
|
||||
"casAppMetaDataOptionsUserAttribute":"Attribut de l'identifiant",
|
||||
"casAppName":"Nom de l'application CAS",
|
||||
"casAttr":"Identifiant CAS",
|
||||
"casAttributes":"Attributs CAS",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Type",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Protocole",
|
||||
"oidcRPMetaDataOptionsPublic":"Client publique",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"PKCE requis",
|
||||
"oidcRPMetaDataOptionsRule":"Règle d'accès",
|
||||
"oidcOPMetaDataOptionsScope":"Étendue",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Conserver le jeton d'identité",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"Opzioni",
|
||||
"casAppMetaDataOptionsService":"URL del servizio",
|
||||
"casAppMetaDataOptionsRule":"Regola",
|
||||
"casAppMetaDataOptionsUserAttribute":"Attributo utente",
|
||||
"casAppName":"Nome App CAS",
|
||||
"casAttr":"Login CAS",
|
||||
"casAttributes":"Attributi CAS esportati",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Tipo",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Protocollo",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"Regola di accesso",
|
||||
"oidcOPMetaDataOptionsScope":"Scopo",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Immagazzina ID Token",
|
||||
|
|
|
@ -117,6 +117,7 @@
|
|||
"casAppMetaDataOptions":"Tùy chọn",
|
||||
"casAppMetaDataOptionsService":"Dịch vụ URL",
|
||||
"casAppMetaDataOptionsRule":"Quy tắc",
|
||||
"casAppMetaDataOptionsUserAttribute":"thuộc tính người dùng",
|
||||
"casAppName":"Tên ứng dụng CAS",
|
||||
"casAttr":"Đăng nhập CAS ",
|
||||
"casAttributes":"Thuộc tính CAS đã được xuất",
|
||||
|
@ -509,6 +510,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Loại",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Giao thức",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"Quy tắc truy cập",
|
||||
"oidcOPMetaDataOptionsScope":"Phạm vi",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Mã thông báo Cửa hàng",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"选项",
|
||||
"casAppMetaDataOptionsService":"服务 URL",
|
||||
"casAppMetaDataOptionsRule":"规则",
|
||||
"casAppMetaDataOptionsUserAttribute":"User attribute",
|
||||
"casAppName":"CAS App 名称",
|
||||
"casAttr":"CAS 登录",
|
||||
"casAttributes":"CAS 声明的attributes",
|
||||
|
@ -508,6 +509,8 @@
|
|||
"oidcRPMetaDataOptionsLogoutType":"Type",
|
||||
"oidcRPMetaDataOptionsLogoutUrl":"URL",
|
||||
"oidcOPMetaDataOptionsProtocol":"Protocol",
|
||||
"oidcRPMetaDataOptionsPublic":"Public client",
|
||||
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
|
||||
"oidcRPMetaDataOptionsRule":"Access rule",
|
||||
"oidcOPMetaDataOptionsScope":"Scope",
|
||||
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",
|
||||
|
|
|
@ -446,6 +446,7 @@ t/30-SAML-Head-to-Tail-POST.t
|
|||
t/30-SAML-ReAuth-with-choice.t
|
||||
t/30-SAML-ReAuth.t
|
||||
t/30-SAML-SP-rule.t
|
||||
t/31-Auth-and-issuer-CAS-declared-app-userattr.t
|
||||
t/31-Auth-and-issuer-CAS-declared-app.t
|
||||
t/31-Auth-and-issuer-CAS-declared-apps.t
|
||||
t/31-Auth-and-issuer-CAS-default.t
|
||||
|
@ -454,11 +455,13 @@ t/31-Auth-and-issuer-CAS-proxied.t
|
|||
t/31-Auth-and-issuer-CAS-with-choice-and-cancel.t
|
||||
t/31-Auth-and-issuer-CAS-with-choice.t
|
||||
t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
|
||||
t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
|
||||
t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
|
||||
t/32-Auth-and-issuer-OIDC-authorization_code.t
|
||||
t/32-Auth-and-issuer-OIDC-hybrid.t
|
||||
t/32-Auth-and-issuer-OIDC-implicit.t
|
||||
t/32-Auth-and-issuer-OIDC-sorted.t
|
||||
t/32-CAS-10.t
|
||||
t/32-OIDC-RP-rule.t
|
||||
t/33-Auth-and-issuer-OpenID2.t
|
||||
t/34-Auth-Proxy-and-REST-Server.t
|
||||
|
|
|
@ -107,9 +107,16 @@ sub extractFormInfo {
|
|||
# Call kerberos.js if Kerberos is the only Auth module
|
||||
# kerberosChoice.js is used by Choice
|
||||
$self->{AjaxInitScript} =~ s/kerberosChoice/kerberos/;
|
||||
$req->data->{customScript} .= $self->{AjaxInitScript};
|
||||
$self->logger->debug(
|
||||
"Send init/script -> " . $req->data->{customScript} );
|
||||
|
||||
# In some Combination scenarios, Kerberos may be called multiple
|
||||
# times but we only want to add the JS once
|
||||
unless ( $req->data->{_krbJsAlreadySent} ) {
|
||||
|
||||
$req->data->{customScript} .= $self->{AjaxInitScript};
|
||||
$self->logger->debug(
|
||||
"Send init/script -> " . $req->data->{customScript} );
|
||||
$req->data->{_krbJsAlreadySent} = 1;
|
||||
}
|
||||
|
||||
#$self->p->setHiddenFormValue( $req, kerberos => 0, '', 0 );
|
||||
eval( $self->InitCmd );
|
||||
|
|
|
@ -458,14 +458,23 @@ sub validate {
|
|||
}
|
||||
|
||||
# Get username
|
||||
my $username = $localSession->data->{ $self->conf->{casAttr}
|
||||
|| $self->conf->{whatToTrace} };
|
||||
my $app = $casServiceSession->data->{_casApp};
|
||||
my $username_attribute =
|
||||
( $app
|
||||
and $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute} )
|
||||
? $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute}
|
||||
: ( $self->conf->{casAttr}
|
||||
|| $self->conf->{whatToTrace} );
|
||||
|
||||
my $username = $localSession->data->{$username_attribute};
|
||||
|
||||
$self->logger->debug("Get username $username");
|
||||
|
||||
# Return success message
|
||||
$self->deleteCasSession($casServiceSession);
|
||||
return $self->returnCasValidateSuccess($username);
|
||||
return $self->returnCasValidateSuccess( $req, $username );
|
||||
}
|
||||
|
||||
sub proxy {
|
||||
|
@ -728,8 +737,16 @@ sub _validate2 {
|
|||
}
|
||||
|
||||
# Get username
|
||||
my $username = $localSession->data->{ $self->conf->{casAttr}
|
||||
|| $self->conf->{whatToTrace} };
|
||||
my $username_attribute =
|
||||
( $app
|
||||
and $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute} )
|
||||
? $self->conf->{casAppMetaDataOptions}->{$app}
|
||||
->{casAppMetaDataOptionsUserAttribute}
|
||||
: ( $self->conf->{casAttr}
|
||||
|| $self->conf->{whatToTrace} );
|
||||
|
||||
my $username = $localSession->data->{$username_attribute};
|
||||
|
||||
$self->logger->debug("Get username $username");
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ sub run {
|
|||
foreach my $param (
|
||||
qw/response_type scope client_id state redirect_uri nonce
|
||||
response_mode display prompt max_age ui_locales id_token_hint
|
||||
login_hint acr_values request request_uri/
|
||||
login_hint acr_values request request_uri code_challenge code_challenge_method/
|
||||
)
|
||||
{
|
||||
if ( $req->param($param) ) {
|
||||
|
@ -574,6 +574,24 @@ sub run {
|
|||
my $session_state =
|
||||
$self->createSessionState( $req->id, $client_id );
|
||||
|
||||
# Check if PKCE is required
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsRequirePKCE}
|
||||
and !$oidc_request->{'code_challenge'} )
|
||||
{
|
||||
$self->userLogger->error(
|
||||
"Relying Party must use PKCE protection");
|
||||
return $self->returnRedirectError(
|
||||
$req,
|
||||
$oidc_request->{'redirect_uri'},
|
||||
'invalid_request',
|
||||
"Code challenge is required",
|
||||
undef,
|
||||
$oidc_request->{'state'},
|
||||
( $flow ne "authorizationcode" )
|
||||
);
|
||||
}
|
||||
|
||||
# Authorization Code Flow
|
||||
if ( $flow eq "authorizationcode" ) {
|
||||
|
||||
|
@ -586,6 +604,9 @@ sub run {
|
|||
user_session_id => $req->id,
|
||||
_utime => time,
|
||||
nonce => $oidc_request->{'nonce'},
|
||||
code_challenge => $oidc_request->{'code_challenge'},
|
||||
code_challenge_method =>
|
||||
$oidc_request->{'code_challenge_method'},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -944,7 +965,7 @@ sub token {
|
|||
my ( $client_id, $client_secret ) =
|
||||
$self->getEndPointAuthenticationCredentials($req);
|
||||
|
||||
unless ( $client_id && $client_secret ) {
|
||||
unless ($client_id) {
|
||||
$self->logger->error(
|
||||
"No authentication provided to get token, or authentication type not supported"
|
||||
);
|
||||
|
@ -964,11 +985,25 @@ sub token {
|
|||
}
|
||||
|
||||
# Check client_secret
|
||||
unless ( $client_secret eq $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsClientSecret} )
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsPublic} )
|
||||
{
|
||||
$self->logger->error("Wrong credentials for $rp");
|
||||
return $self->p->sendError( 'invalid_request', 400 );
|
||||
$self->logger->debug(
|
||||
"Relying Party $rp is public, do not check client secret");
|
||||
}
|
||||
else {
|
||||
unless ($client_secret) {
|
||||
$self->logger->error(
|
||||
"Relying Party $rp is confidential but no client secret was provided to authenticate on token endpoint"
|
||||
);
|
||||
return $self->p->sendError( 'invalid_request', 400 );
|
||||
}
|
||||
unless ( $client_secret eq $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsClientSecret} )
|
||||
{
|
||||
$self->logger->error("Wrong credentials for $rp");
|
||||
return $self->p->sendError( 'invalid_request', 400 );
|
||||
}
|
||||
}
|
||||
|
||||
# Get code session
|
||||
|
@ -983,11 +1018,27 @@ sub token {
|
|||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
|
||||
# Check PKCE
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsRequirePKCE} )
|
||||
{
|
||||
unless (
|
||||
$self->validatePKCEChallenge(
|
||||
$req->param('code_verifier'),
|
||||
$codeSession->data->{'code_challenge'},
|
||||
$codeSession->data->{'code_challenge_method'}
|
||||
)
|
||||
)
|
||||
{
|
||||
return $self->p->sendError( $req, 'invalid_grant', 400 );
|
||||
}
|
||||
}
|
||||
|
||||
# Check we have the same redirect_uri value
|
||||
unless ( $req->param("redirect_uri") eq $codeSession->data->{redirect_uri} )
|
||||
{
|
||||
$self->userLogger->error( "Provided redirect_uri does not match "
|
||||
. $codeSession->{redirect_uri} );
|
||||
. $codeSession->data->{redirect_uri} );
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
|
||||
|
@ -1454,10 +1505,13 @@ sub metadata {
|
|||
[qw/none HS256 HS384 HS512 RS256 RS384 RS512/],
|
||||
userinfo_signing_alg_values_supported =>
|
||||
[qw/none HS256 HS384 HS512 RS256 RS384 RS512/],
|
||||
|
||||
# PKCE
|
||||
code_challenge_methods_supported => [qw/plain S256/],
|
||||
}
|
||||
);
|
||||
|
||||
# response_modes_supported}
|
||||
# response_modes_supported
|
||||
|
||||
# id_token_encryption_alg_values_supported
|
||||
# id_token_encryption_enc_values_supported
|
||||
|
|
|
@ -1047,6 +1047,10 @@ sub getEndPointAuthenticationCredentials {
|
|||
$client_id = $req->param('client_id');
|
||||
$client_secret = $req->param('client_secret');
|
||||
}
|
||||
elsif ( $req->param('client_id') and !$req->param('client_secret') ) {
|
||||
$self->logger->debug("Method none used");
|
||||
$client_id = $req->param('client_id');
|
||||
}
|
||||
|
||||
return ( $client_id, $client_secret );
|
||||
}
|
||||
|
@ -1385,6 +1389,52 @@ sub addRouteFromConf {
|
|||
}
|
||||
}
|
||||
|
||||
# Validate PKCE code challenge with given code challenge method
|
||||
# @param code_verifier
|
||||
# @param code_challenge
|
||||
# @param code_challenge_method
|
||||
# @return boolean 1 if challenge succeed, 0 else
|
||||
sub validatePKCEChallenge {
|
||||
my ( $self, $code_verifier, $code_challenge, $code_challenge_method ) = @_;
|
||||
|
||||
unless ($code_verifier) {
|
||||
$self->logger->error("PKCE required but no code verifier provided");
|
||||
return 0;
|
||||
}
|
||||
|
||||
$self->logger->debug("PKCE code verifier received: $code_verifier");
|
||||
|
||||
if ( !$code_challenge_method or $code_challenge_method eq "plain" ) {
|
||||
if ( $code_verifier eq $code_challenge ) {
|
||||
$self->logger->debug("PKCE challenge validated (plain method)");
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
$self->logger->error("PKCE challenge failed (plain method)");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
elsif ( $code_challenge_method eq "S256" ) {
|
||||
my $code_verifier_hashed = encode_base64url( sha256($code_verifier) );
|
||||
if ( $code_verifier_hashed eq $code_challenge ) {
|
||||
$self->logger->debug("PKCE challenge validated (S256 method)");
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
$self->logger->error("PKCE challenge failed (S256 method)");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
$self->logger->error("PKCE challenge method not valid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -1543,6 +1593,10 @@ Build Logout Response URI
|
|||
Build a Lemonldap::NG::Common::PSGI::Router route from OIDC configuration
|
||||
attribute
|
||||
|
||||
=head2 validatePKCEChallenge
|
||||
|
||||
Validate PKCE code challenge with given code challenge method
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Lemonldap::NG::Portal::AuthOpenIDConnect>, L<Lemonldap::NG::Portal::UserDBOpenIDConnect>
|
||||
|
|
|
@ -11,7 +11,7 @@ extends 'Lemonldap::NG::Portal::Main::Plugin';
|
|||
|
||||
# INITIALIZATION
|
||||
|
||||
use constant endAuth => 'run';
|
||||
use constant afterData => 'run';
|
||||
|
||||
has rule => ( is => 'rw', default => sub { 1 } );
|
||||
has idRule => ( is => 'rw', default => sub { 1 } );
|
||||
|
@ -137,6 +137,8 @@ sub run {
|
|||
|
||||
# Main session
|
||||
$self->p->updateSession( $req, $spoofSession );
|
||||
$req->steps( [ $self->p->validSession, @{ $self->p->endAuth } ] );
|
||||
|
||||
return $statut;
|
||||
}
|
||||
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"٪ s من الأيام و٪ s من الساعات و٪ s من الدقائق و٪ s من الثواني قبل انتهاء صلاحية كلمة المرور، قم بتغييرها!",
|
||||
"redirectedFrom":"تمت إعادة توجيهك من",
|
||||
"redirectedIn":"ستتم إعادة توجيهك خلال 30 ثانية",
|
||||
"redirectionInProgres":"جار إعادة التوجيه قيد التقدم ...",
|
||||
"redirectionInProgress":"جار إعادة التوجيه قيد التقدم ...",
|
||||
"redirectionToIdp":"إعادة توجيهك إلى موفر الهوية الخاص بك",
|
||||
"refreshrights":"قم بتحديث حقوقي",
|
||||
"refuse":"رفض",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s Tage, %s Stunden, %s Minuten und %s Sekunden bevor dein Passwort abläuft, bitte ändere es!",
|
||||
"redirectedFrom":"Du wurdest umgeleitet von",
|
||||
"redirectedIn":"Du wirst in 30 Sekunden umgeleitet",
|
||||
"redirectionInProgres":"Umleitung läuft ...",
|
||||
"redirectionInProgress":"Umleitung läuft ...",
|
||||
"redirectionToIdp":"Weiterleitung an deinen Identitätsanbieter",
|
||||
"refreshrights":"Aktualisiere meine Rechte",
|
||||
"refuse":"Verweigern",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
|
||||
"redirectedFrom":"You were redirect from ",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights": "Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
|
||||
"redirectedFrom":"You were redirect from ",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights":"Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%d päivää, %d tuntia, %d minuuttia ja %sekunttia jäljellä salasanan vanhentumiseen, vaihda salasana!",
|
||||
"redirectedFrom":"Olet uudelleenohjattu",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights":"Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s jours, %s heures, %s minutes et %s secondes avant expiration de votre mot de passe, pensez à le changer !",
|
||||
"redirectedFrom":"Vous avez été redirigé depuis ",
|
||||
"redirectedIn":"Vous allez être redirigé(e) automatiquement dans 30 secondes",
|
||||
"redirectionInProgres":"Redirection en cours ...",
|
||||
"redirectionInProgress":"Redirection en cours ...",
|
||||
"redirectionToIdp":"Redirection vers votre fournisseur d'identité",
|
||||
"refreshrights": "Rafraîchir mes droits",
|
||||
"refuse":"Refuser",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s giorni, %s ore, %s minuti e %s secondi prima della scadenza della password, cambiala!",
|
||||
"redirectedFrom":"Sei stato reindirizzato da",
|
||||
"redirectedIn":"Sarai reindirizzato in 30 secondi",
|
||||
"redirectionInProgres":"Reindirizzamento in corso ...",
|
||||
"redirectionInProgress":"Reindirizzamento in corso ...",
|
||||
"redirectionToIdp":"Reindirizzamento al tuo provider di identità",
|
||||
"refreshrights":"Aggiorna i miei diritti",
|
||||
"refuse":"Rifiuta",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
|
||||
"redirectedFrom":"You were redirect from ",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights":"Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
|
||||
"redirectedFrom":"You were redirect from ",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights":"Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s days, %s hours, %s minutes and %s seconds before password expiration, change it!",
|
||||
"redirectedFrom":"You were redirect from ",
|
||||
"redirectedIn":"You'll be redirected in 30 seconds",
|
||||
"redirectionInProgres":"Redirection in progress...",
|
||||
"redirectionInProgress":"Redirection in progress...",
|
||||
"redirectionToIdp":"Redirection to your Identity Provider",
|
||||
"refreshrights":"Refresh my rights",
|
||||
"refuse":"Refuse",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"%s ngày, %s giờ, %s phút và %s giây trước khi hết hạn mật khẩu, hãy thay đổi nó!",
|
||||
"redirectedFrom":"Bạn đã được chuyển hướng từ",
|
||||
"redirectedIn":"Bạn sẽ được chuyển hướng trong 30 giây",
|
||||
"redirectionInProgres":"Đang tiến hành chuyển hướng...",
|
||||
"redirectionInProgress":"Đang tiến hành chuyển hướng...",
|
||||
"redirectionToIdp":"Chuyển hướng tới Bộ cung cấp Nhận dạng của bạn",
|
||||
"refreshrights":"Làm mới lại quyền của tôi",
|
||||
"refuse":"Từ chối",
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
"pwdWillExpire":"距离密码失效还有 %d 天, %d 小时, %d 分钟, %d 秒, 请修改!",
|
||||
"redirectedFrom":"您重定向自",
|
||||
"redirectedIn":"您将30秒后重定向",
|
||||
"redirectionInProgres":"重定向进行中",
|
||||
"redirectionInProgress":"重定向进行中",
|
||||
"redirectionToIdp":"重定向至你的Identity Provider",
|
||||
"refreshrights":"刷新我的权限",
|
||||
"refuse":"拒绝",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../common/redirect.tpl
|
|
@ -0,0 +1,40 @@
|
|||
<TMPL_INCLUDE NAME="header.tpl">
|
||||
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/redirect.min.js"></script>
|
||||
<script id="redirect" type="custom">
|
||||
<TMPL_IF NAME="HIDDEN_INPUTS">
|
||||
form
|
||||
<TMPL_ELSE>
|
||||
<TMPL_VAR NAME="URL">
|
||||
</TMPL_IF>
|
||||
</script>
|
||||
|
||||
<main id="redirectcontent" class="container">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header text-white bg-secondary">
|
||||
<h4 class="text-center card-title"><span trspan="redirectionInProgress">Redirection in progress...</span></h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
|
||||
</div>
|
||||
<noscript>
|
||||
<div class="message message-warning alert">It appears that your browser does not support Javascript.</div>
|
||||
</noscript>
|
||||
<TMPL_IF NAME="HIDDEN_INPUTS">
|
||||
<form id="form" action="<TMPL_VAR NAME="URL">" method="<TMPL_VAR NAME="FORM_METHOD">" class="login">
|
||||
<TMPL_VAR NAME="HIDDEN_INPUTS">
|
||||
<noscript>
|
||||
<input type="submit" />
|
||||
</noscript>
|
||||
</form>
|
||||
<TMPL_ELSE>
|
||||
<noscript>
|
||||
<p><a href="<TMPL_VAR NAME="URL">">Please click here</a></p>
|
||||
</noscript>
|
||||
</TMPL_IF>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<TMPL_INCLUDE NAME="footer.tpl">
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title trspan="authPortal">Authentication portal</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Script-Type" content="text/javascript" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<link href="<TMPL_VAR NAME="STATIC_PREFIX">common/favicon.ico" rel="icon" type="image/x-icon" />
|
||||
<link href="<TMPL_VAR NAME="STATIC_PREFIX">common/favicon.ico" rel="shortcut icon" />
|
||||
<!-- //if:jsminified
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/redirect.min.js"></script>
|
||||
//else -->
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/redirect.js"></script>
|
||||
<!-- //endif -->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script id="redirect" type="custom">
|
||||
<TMPL_IF NAME="HIDDEN_INPUTS">
|
||||
form
|
||||
<TMPL_ELSE>
|
||||
<TMPL_VAR NAME="URL">
|
||||
</TMPL_IF>
|
||||
</script>
|
||||
|
||||
<h1>Redirection in progress...</h1>
|
||||
<noscript>
|
||||
<p>It appears that your browser does not support Javascript.</p>
|
||||
</noscript>
|
||||
<TMPL_IF NAME="HIDDEN_INPUTS">
|
||||
<form id="form" action="<TMPL_VAR NAME="URL">" method="<TMPL_VAR NAME="FORM_METHOD">" class="login">
|
||||
<TMPL_VAR NAME="HIDDEN_INPUTS">
|
||||
<noscript>
|
||||
<input type="submit" value="Please click here"/>
|
||||
</noscript>
|
||||
</form>
|
||||
<TMPL_ELSE>
|
||||
<noscript>
|
||||
<p><a href="<TMPL_VAR NAME="URL">">Please click here</a></p>
|
||||
</noscript>
|
||||
</TMPL_IF>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
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, $sp, $res );
|
||||
my %handlerOR = ( issuer => [], sp => [] );
|
||||
|
||||
# Redefine LWP methods for tests
|
||||
LWP::Protocol::PSGI->register(
|
||||
sub {
|
||||
my $req = Plack::Request->new(@_);
|
||||
ok( $req->uri =~ m#http://auth.((?:id|s)p).com([^\?]*)(?:\?(.*))?$#,
|
||||
'SOAP request' );
|
||||
my $host = $1;
|
||||
my $url = $2;
|
||||
my $query = $3;
|
||||
my $res;
|
||||
my $client = ( $host eq 'idp' ? $issuer : $sp );
|
||||
if ( $req->method eq 'POST' ) {
|
||||
my $s = $req->content;
|
||||
ok(
|
||||
$res = $client->_post(
|
||||
$url, IO::String->new($s),
|
||||
length => length($s),
|
||||
query => $query,
|
||||
type => 'application/xml',
|
||||
),
|
||||
"Execute POST request to $url"
|
||||
);
|
||||
}
|
||||
else {
|
||||
ok(
|
||||
$res = $client->_get(
|
||||
$url,
|
||||
type => 'application/xml',
|
||||
query => $query,
|
||||
),
|
||||
"Execute request to $url"
|
||||
);
|
||||
}
|
||||
expectOK($res);
|
||||
ok( getHeader( $res, 'Content-Type' ) =~ m#xml#, 'Content is XML' )
|
||||
or explain( $res->[1], 'Content-Type => application/xml' );
|
||||
count(3);
|
||||
return $res;
|
||||
}
|
||||
);
|
||||
|
||||
ok( $issuer = issuer(), 'Issuer portal' );
|
||||
$handlerOR{issuer} = \@Lemonldap::NG::Handler::Main::_onReload;
|
||||
count(1);
|
||||
switch ('sp');
|
||||
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
|
||||
|
||||
ok( $sp = sp(), 'SP portal' );
|
||||
count(1);
|
||||
$handlerOR{sp} = \@Lemonldap::NG::Handler::Main::_onReload;
|
||||
|
||||
# Simple SP access
|
||||
ok(
|
||||
$res = $sp->_get(
|
||||
'/', accept => 'text/html',
|
||||
),
|
||||
'Unauth SP request'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res,
|
||||
'http://auth.idp.com/cas/login?service=http%3A%2F%2Fauth.sp.com%2F' );
|
||||
|
||||
# Query IdP
|
||||
switch ('issuer');
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/login',
|
||||
query => 'service=http://auth.sp.com/',
|
||||
accept => 'text/html'
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
count(1);
|
||||
expectOK($res);
|
||||
my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
|
||||
|
||||
# Try to authenticate with an unauthorized user to IdP
|
||||
my $body = $res->[2]->[0];
|
||||
$body =~ s/^.*?<form.*?>//s;
|
||||
$body =~ s#</form>.*$##s;
|
||||
my %fields =
|
||||
( $body =~ /<input type="hidden".+?name="(.+?)".+?value="(.*?)"/sg );
|
||||
$fields{user} = $fields{password} = 'dwho';
|
||||
use URI::Escape;
|
||||
my $s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
|
||||
ok(
|
||||
$res = $issuer->_post(
|
||||
'/cas/login',
|
||||
IO::String->new($s),
|
||||
cookie => $pdata,
|
||||
accept => 'text/html',
|
||||
length => length($s),
|
||||
),
|
||||
'Post authentication'
|
||||
);
|
||||
count(1);
|
||||
ok( $res->[2]->[0] =~ /trmsg="68"/, 'Reject reason is 68' )
|
||||
or print STDERR Dumper( $res->[2]->[0] );
|
||||
count(1);
|
||||
|
||||
# Simple SP access
|
||||
ok(
|
||||
$res = $sp->_get(
|
||||
'/', accept => 'text/html',
|
||||
),
|
||||
'Unauth SP request'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res,
|
||||
'http://auth.idp.com/cas/login?service=http%3A%2F%2Fauth.sp.com%2F' );
|
||||
|
||||
# Query IdP
|
||||
switch ('issuer');
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/login',
|
||||
query => 'service=http://auth.sp.com/',
|
||||
accept => 'text/html'
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
count(1);
|
||||
expectOK($res);
|
||||
$pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
|
||||
|
||||
# Try to authenticate with an authorized to IdP
|
||||
$body = $res->[2]->[0];
|
||||
$body =~ s/^.*?<form.*?>//s;
|
||||
$body =~ s#</form>.*$##s;
|
||||
%fields = ( $body =~ /<input type="hidden".+?name="(.+?)".+?value="(.*?)"/sg );
|
||||
$fields{user} = $fields{password} = 'french';
|
||||
use URI::Escape;
|
||||
$s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
|
||||
ok(
|
||||
$res = $issuer->_post(
|
||||
'/cas/login',
|
||||
IO::String->new($s),
|
||||
cookie => $pdata,
|
||||
accept => 'text/html',
|
||||
length => length($s),
|
||||
),
|
||||
'Post authentication'
|
||||
);
|
||||
count(1);
|
||||
my ($query) =
|
||||
expectRedirection( $res, qr#^http://auth.sp.com/\?(ticket=[^&]+)$# );
|
||||
my $idpId = expectCookie($res);
|
||||
|
||||
# Back to SP
|
||||
switch ('sp');
|
||||
ok( $res = $sp->_get( '/', query => $query, accept => 'text/html' ),
|
||||
'Query SP with ticket' );
|
||||
count(1);
|
||||
my $spId = expectCookie($res);
|
||||
|
||||
# Test authentication, with mail as the main identity attribute
|
||||
ok( $res = $sp->_get( '/', cookie => "lemonldap=$spId" ), 'Get / on SP' );
|
||||
count(1);
|
||||
expectOK($res);
|
||||
expectAuthenticatedAs( $res, 'fa@badwolf.org' );
|
||||
|
||||
# Test attributes
|
||||
ok( $res = $sp->_get("/sessions/global/$spId"), 'Get UTF-8' );
|
||||
expectOK($res);
|
||||
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
|
||||
or print STDERR $@;
|
||||
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
|
||||
or explain( $res, 'cn => Frédéric Accents' );
|
||||
count(3);
|
||||
|
||||
# Logout initiated by SP
|
||||
ok(
|
||||
$res = $sp->_get(
|
||||
'/',
|
||||
query => 'logout',
|
||||
cookie => "lemonldap=$spId",
|
||||
accept => 'text/html'
|
||||
),
|
||||
'Query SP for logout'
|
||||
);
|
||||
count(1);
|
||||
expectOK($res);
|
||||
ok(
|
||||
$res->[2]->[0] =~ m#iframe src="http://auth.idp.com(/cas/logout)\?(.+?)"#s,
|
||||
'Found iframe'
|
||||
);
|
||||
count(1);
|
||||
|
||||
# Query IdP with iframe src
|
||||
my $url = $1;
|
||||
$query = $2;
|
||||
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
|
||||
'Frame is authorizated' )
|
||||
or
|
||||
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
|
||||
count(1);
|
||||
|
||||
switch ('issuer');
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
$url,
|
||||
query => $query,
|
||||
accept => 'text/html',
|
||||
cookie => "lemonldap=$idpId"
|
||||
),
|
||||
'Get iframe from IdP'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res, 'http://auth.sp.com/?logout' );
|
||||
my $h = getHeader( $res, 'Content-Security-Policy' );
|
||||
ok( ( not $h or $h !~ /frame-ancestors/ ), ' Frame can be embedded' )
|
||||
or explain( $res->[1],
|
||||
'Content-Security-Policy does not contain a frame-ancestors' );
|
||||
count(1);
|
||||
|
||||
# Verify that user has been disconnected
|
||||
ok( $res = $issuer->_get( '/', cookie => "lemonldap=$idpId" ), 'Query IdP' );
|
||||
count(1);
|
||||
expectReject($res);
|
||||
|
||||
switch ('sp');
|
||||
ok(
|
||||
$res =
|
||||
$sp->_get( '/', accept => 'text/html', cookie => "lemonldap=$idpId" ),
|
||||
'Query IdP'
|
||||
);
|
||||
count(1);
|
||||
expectRedirection( $res,
|
||||
'http://auth.idp.com/cas/login?service=http%3A%2F%2Fauth.sp.com%2F' );
|
||||
|
||||
clean_sessions();
|
||||
done_testing( count() );
|
||||
|
||||
sub switch {
|
||||
my $type = shift;
|
||||
@Lemonldap::NG::Handler::Main::_onReload = @{
|
||||
$handlerOR{$type};
|
||||
};
|
||||
}
|
||||
|
||||
sub issuer {
|
||||
return LLNG::Manager::Test->new( {
|
||||
ini => {
|
||||
logLevel => $debug,
|
||||
templatesDir => 'site/htdocs/static',
|
||||
domain => 'idp.com',
|
||||
portal => 'http://auth.idp.com',
|
||||
authentication => 'Demo',
|
||||
userDB => 'Same',
|
||||
issuerDBCASActivation => 1,
|
||||
issuerDBCASRule => '$uid eq "french"',
|
||||
casAttr => 'uid',
|
||||
casAccessControlPolicy => 'error',
|
||||
multiValuesSeparator => ';',
|
||||
casAppMetaDataExportedVars => {
|
||||
sp => {
|
||||
cn => 'cn',
|
||||
mail => 'mail',
|
||||
uid => 'uid',
|
||||
}
|
||||
},
|
||||
casAppMetaDataOptions => {
|
||||
sp => {
|
||||
casAppMetaDataOptionsService => 'http://auth.sp.com',
|
||||
casAppMetaDataOptionsUserAttribute => 'mail',
|
||||
},
|
||||
sp2 => {
|
||||
casAppMetaDataOptionsService => 'http://auth.sp2.com',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sub sp {
|
||||
return LLNG::Manager::Test->new( {
|
||||
ini => {
|
||||
logLevel => $debug,
|
||||
domain => 'sp.com',
|
||||
portal => 'http://auth.sp.com',
|
||||
authentication => 'CAS',
|
||||
userDB => 'CAS',
|
||||
restSessionServer => 1,
|
||||
issuerDBCASActivation => 0,
|
||||
multiValuesSeparator => ';',
|
||||
exportedVars => {
|
||||
cn => 'cn',
|
||||
},
|
||||
casSrvMetaDataExportedVars => {
|
||||
idp => {
|
||||
cn => 'cn',
|
||||
mail => 'mail',
|
||||
uid => 'uid',
|
||||
}
|
||||
},
|
||||
casSrvMetaDataOptions => {
|
||||
idp => {
|
||||
casSrvMetaDataOptionsUrl => 'http://auth.idp.com/cas',
|
||||
casSrvMetaDataOptionsGateway => 0,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
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';
|
||||
}
|
||||
|
||||
my $debug = 'error';
|
||||
my ( $op, $rp, $res );
|
||||
my %handlerOR = ( op => [], rp => [] );
|
||||
|
||||
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/json#,
|
||||
' Content is JSON' )
|
||||
or explain( $res->[1], 'Content-Type => application/json' );
|
||||
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'
|
||||
);
|
||||
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
|
||||
or print STDERR $@;
|
||||
ok( $res->{name} eq 'Frédéric Accents', 'UTF-8 values' )
|
||||
or explain( $res, 'name => Frédéric Accents' );
|
||||
count(3);
|
||||
|
||||
ok( $res = $op->_get("/sessions/global/$spId"), 'Get UTF-8' );
|
||||
expectOK($res);
|
||||
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
|
||||
or print STDERR $@;
|
||||
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
|
||||
or explain( $res, 'cn => Frédéric Accents' );
|
||||
count(3);
|
||||
|
||||
switch ('rp');
|
||||
ok( $res = $rp->_get("/sessions/global/$spId"), 'Get UTF-8' );
|
||||
expectOK($res);
|
||||
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
|
||||
or print STDERR $@;
|
||||
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
|
||||
or explain( $res, 'cn => Frédéric Accents' );
|
||||
count(3);
|
||||
|
||||
# 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#.# );
|
||||
|
||||
# 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 =>
|
||||
"lemonldapidp=http://auth.idp.com/saml/metadata; 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 switch {
|
||||
my $type = shift;
|
||||
pass( '==> Switching to ' . uc($type) . ' <==' );
|
||||
count(1);
|
||||
@Lemonldap::NG::Handler::Main::_onReload = @{
|
||||
$handlerOR{$type};
|
||||
};
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
},
|
||||
oidcServiceMetaDataIssuer => "http://auth.op.com/",
|
||||
oidcServiceMetaDataAuthorizeURI => "authorize",
|
||||
oidcServiceMetaDataCheckSessionURI => "checksession.html",
|
||||
oidcServiceMetaDataJWKSURI => "jwks",
|
||||
oidcServiceMetaDataEndSessionURI => "logout",
|
||||
oidcServiceMetaDataRegistrationURI => "register",
|
||||
oidcServiceMetaDataTokenURI => "token",
|
||||
oidcServiceMetaDataUserInfoURI => "userinfo",
|
||||
oidcServiceAllowHybridFlow => 1,
|
||||
oidcServiceAllowImplicitFlow => 1,
|
||||
oidcServiceAllowDynamicRegistration => 1,
|
||||
oidcServiceAllowAuthorizationCodeFlow => 1,
|
||||
oidcRPMetaDataOptions => {
|
||||
rp => {
|
||||
oidcRPMetaDataOptionsDisplayName => "RP",
|
||||
oidcRPMetaDataOptionsIDTokenExpiration => 3600,
|
||||
oidcRPMetaDataOptionsClientID => "rpid",
|
||||
oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
|
||||
oidcRPMetaDataOptionsBypassConsent => 0,
|
||||
oidcRPMetaDataOptionsPublic => 1,
|
||||
oidcRPMetaDataOptionsUserIDAttr => "",
|
||||
oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
|
||||
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 => "-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAs2jsmIoFuWzMkilJaA8//5/T30cnuzX9GImXUrFR2k9EKTMt
|
||||
GMHCdKlWOl3BV+BTAU9TLz7Jzd/iJ5GJ6B8TrH1PHFmHpy8/qE/S5OhinIpIi7eb
|
||||
ABqnoVcwDdCa8ugzq8k8SWxhRNXfVIlwz4NH1caJ8lmiERFj7IvNKqEhzAk0pyDr
|
||||
8hubveTC39xREujKlsqutpPAFPJ3f2ybVsdykX5rx0h5SslG3jVWYhZ/SOb2aIzO
|
||||
r0RMjhQmsYRwbpt3anjlBZ98aOzg7GAkbO8093X5VVk9vaPRg0zxJQ0Do0YLyzkR
|
||||
isSAIFb0tdKuDnjRGK6y/N2j6At2HjkxntbtGQIDAQABAoIBADYq6LxJd977LWy3
|
||||
0HT9nboFPIf+SM2qSEc/S5Po+6ipJBA4ZlZCMf7dHa6znet1TDpqA9iQ4YcqIHMH
|
||||
6xZNQ7hhgSAzG9TrXBHqP+djDlrrGWotvjuy0IfS9ixFnnLWjrtAH9afRWLuG+a/
|
||||
NHNC1M6DiiTE0TzL/lpt/zzut3CNmWzH+t19X6UsxUg95AzooEeewEYkv25eumWD
|
||||
mfQZfCtSlIw1sp/QwxeJa/6LJw7KcPZ1wXUm1BN0b9eiKt9Cmni1MS7elgpZlgGt
|
||||
xtfGTZtNLQ7bgDiM8MHzUfPBhbceNSIx2BeCuOCs/7eaqgpyYHBbAbuBQex2H61l
|
||||
Lcc3Tz0CgYEA4Kx/avpCPxnvsJ+nHVQm5d/WERuDxk4vH1DNuCYBvXTdVCGADf6a
|
||||
F5No1JcTH3nPTyPWazOyGdT9LcsEJicLyD8vCM6hBFstG4XjqcAuqG/9DRsElpHQ
|
||||
yi1zc5DNP7Vxmiz9wII0Mjy0abYKtxnXh9YK4a9g6wrcTpvShhIcIb8CgYEAzGzG
|
||||
lorVCfX9jXULIznnR/uuP5aSnTEsn0xJeqTlbW0RFWLdj8aIL1peirh1X89HroB9
|
||||
GeTNqEJXD+3CVL2cx+BRggMDUmEz4hR59meZCDGUyT5fex4LIsceb/ESUl2jo6Sw
|
||||
HXwWbN67rQ55N4oiOcOppsGxzOHkl5HdExKidycCgYEAr5Qev2tz+fw65LzfzHvH
|
||||
Kj4S/KuT/5V6He731cFd+sEpdmX3vPgLVAFPG1Q1DZQT/rTzDDQKK0XX1cGiLG63
|
||||
NnaqOye/jbfzOF8Z277kt51NFMDYhRLPKDD82IOA4xjY/rPKWndmcxwdob8yAIWh
|
||||
efY76sMz6ntCT+xWSZA9i+ECgYBWMZM2TIlxLsBfEbfFfZewOUWKWEGvd9l5vV/K
|
||||
D5cRIYivfMUw5yPq2267jPUolayCvniBH4E7beVpuPVUZ7KgcEvNxtlytbt7muil
|
||||
5Z6X3tf+VodJ0Swe2NhTmNEB26uwxzLe68BE3VFCsbSYn2y48HAq+MawPZr18bHG
|
||||
ZfgMxwKBgHHRg6HYqF5Pegzk1746uH2G+OoCovk5ylGGYzcH2ghWTK4agCHfBcDt
|
||||
EYqYAev/l82wi+OZ5O8U+qjFUpT1CVeUJdDs0o5u19v0UJjunU1cwh9jsxBZAWLy
|
||||
PAGd6SWf4S3uQCTw6dLeMna25YIlPh5qPA6I/pAahe8e3nSu2ckl
|
||||
-----END RSA PRIVATE KEY-----
|
||||
",
|
||||
oidcServicePublicKeySig => "-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2jsmIoFuWzMkilJaA8/
|
||||
/5/T30cnuzX9GImXUrFR2k9EKTMtGMHCdKlWOl3BV+BTAU9TLz7Jzd/iJ5GJ6B8T
|
||||
rH1PHFmHpy8/qE/S5OhinIpIi7ebABqnoVcwDdCa8ugzq8k8SWxhRNXfVIlwz4NH
|
||||
1caJ8lmiERFj7IvNKqEhzAk0pyDr8hubveTC39xREujKlsqutpPAFPJ3f2ybVsdy
|
||||
kX5rx0h5SslG3jVWYhZ/SOb2aIzOr0RMjhQmsYRwbpt3anjlBZ98aOzg7GAkbO80
|
||||
93X5VVk9vaPRg0zxJQ0Do0YLyzkRisSAIFb0tdKuDnjRGK6y/N2j6At2Hjkxntbt
|
||||
GQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
",
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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 => {
|
||||
oidcOPMetaDataOptionsJWKSTimeout => 0,
|
||||
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,109 @@
|
|||
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 );
|
||||
my %handlerOR = ( issuer => [] );
|
||||
|
||||
ok( $issuer = issuer(), 'Issuer portal' );
|
||||
count(1);
|
||||
switch ('issuer');
|
||||
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/login',
|
||||
query => 'service=http://auth.sp.com/',
|
||||
accept => 'text/html'
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
count(1);
|
||||
expectOK($res);
|
||||
my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
|
||||
|
||||
# Try to authenticate to IdP
|
||||
my $body = $res->[2]->[0];
|
||||
$body =~ s/^.*?<form.*?>//s;
|
||||
$body =~ s#</form>.*$##s;
|
||||
my %fields =
|
||||
( $body =~ /<input type="hidden".+?name="(.+?)".+?value="(.*?)"/sg );
|
||||
$fields{user} = $fields{password} = 'french';
|
||||
use URI::Escape;
|
||||
my $s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
|
||||
ok(
|
||||
$res = $issuer->_post(
|
||||
'/cas/login',
|
||||
IO::String->new($s),
|
||||
cookie => $pdata,
|
||||
accept => 'text/html',
|
||||
length => length($s),
|
||||
),
|
||||
'Post authentication'
|
||||
);
|
||||
count(1);
|
||||
my $idpId = expectCookie($res);
|
||||
|
||||
# Expect pdata to be cleared
|
||||
$pdata = expectCookie( $res, 'lemonldappdata' );
|
||||
ok( $pdata !~ 'issuerRequestsaml', 'SAML request cleared from pdata' );
|
||||
count(1);
|
||||
|
||||
my ($query) =
|
||||
expectRedirection( $res, qr#^http://auth.sp.com/\?(ticket=[^&]+)$# );
|
||||
|
||||
ok(
|
||||
$res = $issuer->_get(
|
||||
'/cas/validate',
|
||||
query => 'service=http://auth.sp.com/&' . $query,
|
||||
accept => 'text/html'
|
||||
),
|
||||
'Query CAS server'
|
||||
);
|
||||
|
||||
expectOK($res);
|
||||
count(1);
|
||||
|
||||
my @resp = split /\n/, $res->[2]->[0];
|
||||
|
||||
ok( $resp[0] eq 'yes', 'Ticket is valid' );
|
||||
count(1);
|
||||
ok( $resp[1] eq 'french', 'Username is returned' );
|
||||
count(1);
|
||||
|
||||
clean_sessions();
|
||||
done_testing( count() );
|
||||
|
||||
sub switch {
|
||||
my $type = shift;
|
||||
@Lemonldap::NG::Handler::Main::_onReload = @{
|
||||
$handlerOR{$type};
|
||||
};
|
||||
}
|
||||
|
||||
sub issuer {
|
||||
return LLNG::Manager::Test->new( {
|
||||
ini => {
|
||||
logLevel => $debug,
|
||||
templatesDir => 'site/htdocs/static',
|
||||
domain => 'idp.com',
|
||||
portal => 'http://auth.idp.com',
|
||||
authentication => 'Demo',
|
||||
userDB => 'Same',
|
||||
issuerDBCASActivation => 1,
|
||||
casAttr => 'uid',
|
||||
casAttributes => { cn => 'cn', uid => 'uid', },
|
||||
casAccessControlPolicy => 'none',
|
||||
multiValuesSeparator => ';',
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -54,8 +54,9 @@ ok( $res->[2]->[0] =~ m%<span trmsg="40"></span>%, ' PE40 found' )
|
|||
or explain( $res->[2]->[0], "PE40 - Bad formed user" );
|
||||
count(2);
|
||||
|
||||
my $id = expectCookie($res);
|
||||
$client->logout($id);
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
count(1);
|
||||
expectForm( $res, '#', undef, 'user', 'password', 'spoofId' );
|
||||
|
||||
## Try to impersonate with a forbidden identity
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
|
@ -82,8 +83,9 @@ m%<div class="message message-negative alert"><span trmsg="5"></span></div>%,
|
|||
) or explain( $res->[2]->[0], "PE5 - Forbidden identity" );
|
||||
count(2);
|
||||
|
||||
$id = expectCookie($res);
|
||||
$client->logout($id);
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
count(1);
|
||||
expectForm( $res, '#', undef, 'user', 'password', 'spoofId' );
|
||||
|
||||
## An unauthorized user try to impersonate
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
|
@ -110,8 +112,9 @@ m%<div class="message message-negative alert"><span trmsg="93"></span></div>%,
|
|||
) or explain( $res->[2]->[0], "PE93 - Impersonation service not allowed" );
|
||||
count(2);
|
||||
|
||||
$id = expectCookie($res);
|
||||
$client->logout($id);
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
count(1);
|
||||
expectForm( $res, '#', undef, 'user', 'password', 'spoofId' );
|
||||
|
||||
## An unauthorized user to impersonate tries to authenticate
|
||||
ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
|
||||
|
@ -132,7 +135,7 @@ ok(
|
|||
);
|
||||
count(1);
|
||||
|
||||
$id = expectCookie($res);
|
||||
my $id = expectCookie($res);
|
||||
expectRedirection( $res, 'http://auth.example.com/' );
|
||||
|
||||
# CheckUser form
|
||||
|
|
|
@ -4,7 +4,7 @@ use IO::String;
|
|||
use Data::Dumper;
|
||||
|
||||
require 't/test-lib.pm';
|
||||
my $maintests = 45;
|
||||
my $maintests = 51;
|
||||
|
||||
SKIP: {
|
||||
eval { require Convert::Base32 };
|
||||
|
@ -172,7 +172,7 @@ SKIP: {
|
|||
) or print STDERR Dumper( $res->[2]->[0] );
|
||||
|
||||
# Wait to have two different epoch values
|
||||
sleep 2;
|
||||
sleep 1;
|
||||
|
||||
# Ajax registration request
|
||||
ok(
|
||||
|
@ -268,6 +268,13 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
|
|||
'Found 2fregisters/totp link' )
|
||||
or print STDERR Dumper($res);
|
||||
|
||||
# Two 2F devices must be registered
|
||||
my @sf = map m%<span device=\'(TOTP|U2F)\' epoch=\'\d{10}\'%g, $res->[2]->[0];
|
||||
ok( scalar @sf == 2, 'Two 2F devices found' )
|
||||
or print STDERR Dumper($res);
|
||||
ok( $sf[0] eq 'TOTP', 'TOTP device found' ) or print STDERR Dumper( \@sf );
|
||||
ok( $sf[1] eq 'U2F', 'U2F device found' ) or print STDERR Dumper( \@sf );
|
||||
|
||||
# Unregister TOTP
|
||||
ok( $res->[2]->[0] =~ qr%TOTP.*epoch.*(\d{10})%m, "TOTP epoch $1 found" )
|
||||
or print STDERR Dumper( $res->[2]->[0] );
|
||||
|
@ -299,6 +306,12 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
|
|||
'Found 2fregisters/totp link' )
|
||||
or print STDERR Dumper($res);
|
||||
|
||||
# One 2F device must be registered
|
||||
@sf = map m%<span device=\'(TOTP|U2F)\' epoch=\'\d{10}\'%g, $res->[2]->[0];
|
||||
ok( scalar @sf == 1, 'One 2F device found' )
|
||||
or print STDERR Dumper($res);
|
||||
ok( $sf[0] eq 'U2F', 'U2F device found' ) or print STDERR Dumper( \@sf );
|
||||
|
||||
# Unregister U2F key
|
||||
ok( $res->[2]->[0] =~ qr%U2F.*epoch.*(\d{10})%m, "U2F key epoch $1 found" )
|
||||
or print STDERR Dumper( $res->[2]->[0] );
|
||||
|
@ -314,6 +327,11 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
|
|||
ok( $data->{result} == 1, 'U2F key is unregistered' )
|
||||
or explain( $data, '"result":1' );
|
||||
|
||||
# No more 2F device must be registered
|
||||
@sf = map m%<span device=\'(TOTP|U2F)\' epoch=\'\d{10}\'%g, $res->[2]->[0];
|
||||
ok( scalar @sf == 0, 'No 2F device found' )
|
||||
or print STDERR Dumper($res);
|
||||
|
||||
$client->logout($id);
|
||||
}
|
||||
count($maintests);
|
||||
|
|
Loading…
Reference in New Issue