Merge branch 'v2.0'

This commit is contained in:
Xavier 2019-04-30 21:03:14 +02:00
commit 29b71569de
44 changed files with 1358 additions and 133 deletions

1
debian/control vendored
View File

@ -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,

View File

@ -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' );

View File

@ -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)';

View File

@ -678,6 +678,9 @@ sub attributes {
'casAppMetaDataOptionsService' => {
'type' => 'url'
},
'casAppMetaDataOptionsUserAttribute' => {
'type' => 'text'
},
'casAttr' => {
'type' => 'text'
},
@ -1998,9 +2001,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 ) = @_;

View File

@ -1824,6 +1824,10 @@ sub attributes {
type => 'url',
documentation => 'CAS App service',
},
casAppMetaDataOptionsUserAttribute => {
type => 'text',
documentation => 'CAS User attribute',
},
casAppMetaDataOptionsRule => {
type => 'text',
test => $perlExpr,
@ -3349,11 +3353,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 => {
@ -3413,6 +3412,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',
},
};
}

View File

@ -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'
]
},

View File

@ -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

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"خيارات",
"casAppMetaDataOptionsService":"خدمة أل يو أر ل",
"casAppMetaDataOptionsRule":"القاعدة",
"casAppMetaDataOptionsUserAttribute":"خاصّيّة المستخدم",
"casAppName":"اسم التطبيق كاس",
"casAttr":"تسجيل الدخول كاس",
"casAttributes":"السمات المصدرة لي كاس",
@ -506,6 +507,8 @@
"oidcRPMetaDataOptionsLogoutType":"نوع",
"oidcRPMetaDataOptionsLogoutUrl":"يو آر إل",
"oidcOPMetaDataOptionsProtocol":"بروتوكول",
"oidcRPMetaDataOptionsPublic":"Public client",
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
"oidcRPMetaDataOptionsRule":"قاعدة الدخول",
"oidcOPMetaDataOptionsScope":"نطاق",
"oidcOPMetaDataOptionsStoreIDToken":"مخزن تعريف التوكن",

View File

@ -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",
@ -506,6 +507,8 @@
"oidcRPMetaDataOptionsLogoutType":"Type",
"oidcRPMetaDataOptionsLogoutUrl":"URL",
"oidcOPMetaDataOptionsProtocol":"Protocol",
"oidcRPMetaDataOptionsPublic":"Public client",
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcOPMetaDataOptionsScope":"Scope",
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",

View File

@ -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",
@ -506,6 +507,8 @@
"oidcRPMetaDataOptionsLogoutType":"Type",
"oidcRPMetaDataOptionsLogoutUrl":"URL",
"oidcOPMetaDataOptionsProtocol":"Protocol",
"oidcRPMetaDataOptionsPublic":"Public client",
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcOPMetaDataOptionsScope":"Scope",
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",

View File

@ -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",
@ -506,6 +507,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é",

View File

@ -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",
@ -506,6 +507,8 @@
"oidcRPMetaDataOptionsLogoutType":"Tipo",
"oidcRPMetaDataOptionsLogoutUrl":"URL",
"oidcOPMetaDataOptionsProtocol":"Protocollo",
"oidcRPMetaDataOptionsPublic":"Public client",
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
"oidcRPMetaDataOptionsRule":"Regola di accesso",
"oidcOPMetaDataOptionsScope":"Scopo",
"oidcOPMetaDataOptionsStoreIDToken":"Immagazzina ID Token",

View File

@ -116,6 +116,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",
@ -506,6 +507,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",

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"选项",
"casAppMetaDataOptionsService":"服务 URL",
"casAppMetaDataOptionsRule":"规则",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"CAS App 名称",
"casAttr":"CAS 登录",
"casAttributes":"CAS 声明的attributes",
@ -506,6 +507,8 @@
"oidcRPMetaDataOptionsLogoutType":"Type",
"oidcRPMetaDataOptionsLogoutUrl":"URL",
"oidcOPMetaDataOptionsProtocol":"Protocol",
"oidcRPMetaDataOptionsPublic":"Public client",
"oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
"oidcRPMetaDataOptionsRule":"Access rule",
"oidcOPMetaDataOptionsScope":"Scope",
"oidcOPMetaDataOptionsStoreIDToken":"Store ID Token",

View File

@ -440,6 +440,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
@ -448,11 +449,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

View File

@ -295,24 +295,17 @@ sub run {
return $self->p->sendError( $req, "Corrupted session", 500 );
}
}
else {
$self->logger->debug("No 2F Device found");
$_2fDevices = [];
}
# Delete TOTP 2F device
my @keep = ();
while (@$_2fDevices) {
my $element = shift @$_2fDevices;
$self->logger->debug("Looking for 2F device to delete ...");
push @keep, $element unless ( $element->{epoch} eq $epoch );
}
@$_2fDevices = grep { $_->{epoch} ne $epoch } @$_2fDevices;
$self->logger->debug(
"Delete 2F Device : { type => 'TOTP', epoch => $epoch }");
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json( \@keep ) } );
{ _2fDevices => to_json( $_2fDevices ) } );
$self->userLogger->notice('TOTP deletion succeed');
return [
200,

View File

@ -287,17 +287,12 @@ sub run {
$_2fDevices = [];
}
my @keep = ();
while (@$_2fDevices) {
my $element = shift @$_2fDevices;
$self->logger->debug("Looking for 2F device to delete ...");
push @keep, $element unless ( $element->{epoch} eq $epoch );
}
# Delete U2F device
@$_2fDevices = grep { $_->{epoch} ne $epoch } @$_2fDevices;
$self->logger->debug(
"Delete 2F Device : { type => 'U2F', epoch => $epoch }");
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json( \@keep ) } );
{ _2fDevices => to_json( $_2fDevices ) } );
$self->userLogger->notice('U2F key unregistration succeed');
return [
200,

View File

@ -176,31 +176,23 @@ sub run {
return $self->p->sendError( $req, "Corrupted session", 500 );
}
}
else {
$self->logger->debug("No 2F Device found");
$_2fDevices = [];
}
my @keep = ();
while (@$_2fDevices) {
my $element = shift @$_2fDevices;
$self->logger->debug("Looking for 2F device to delete ...");
push @keep, $element unless ( $element->{epoch} eq $epoch );
}
# Delete Yubikey device
@$_2fDevices = grep { $_->{epoch} ne $epoch } @$_2fDevices;
$self->logger->debug(
"Delete 2F Device : { type => 'UBK', epoch => $epoch }");
$self->p->updatePersistentSession( $req,
{ _2fDevices => to_json( \@keep ) } );
{ _2fDevices => to_json( $_2fDevices ) } );
$self->userLogger->notice('Yubikey deletion succeed');
return [
200,
[ 'Content-Type' => 'application/json', 'Content-Length' => 12, ],
['{"result":1}']
];
}
else {
$self->logger->error("Unknown Yubikey action -> $action");

View File

@ -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 );

View File

@ -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");

View File

@ -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

View File

@ -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>

View File

@ -288,8 +288,9 @@ sub _urlFormat {
$port ||= '';
$vhost =~ s/:\d+$//;
$vhost .= $self->conf->{domain} unless ( $vhost =~ /\./ );
#$appuri ||= '/';
return lc ("$proto$vhost$port") . "$appuri";
return lc("$proto$vhost$port") . "$appuri";
}
sub _userDatas {
@ -387,6 +388,27 @@ sub _splitAttributes {
}
push @$others, $element unless $ok;
}
# Sort real and spoofed attributes if required
if ( $self->conf->{impersonationRule} ) {
$self->logger->debug('Dispatching real and spoofed attributes...');
my ( $realAttrs, $spoofedAttrs ) = ( [], [] );
my $prefix = "$self->{conf}->{impersonationPrefix}";
while (@$others) {
my $element = shift @$others;
$self->logger->debug(
'Processing attribute: ' . Data::Dumper::Dumper($element) );
if ( $element->{key} =~ /^$prefix.+$/ ) {
push @$realAttrs, $element;
$self->logger->debug(' -> Real attribute');
}
else {
push @$spoofedAttrs, $element;
$self->logger->debug(' -> Spoofed attribute');
}
}
@$others = ( @$spoofedAttrs, @$realAttrs );
}
return [ $grps, $mcrs, $others ];
}

View File

@ -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;
}

View File

@ -189,7 +189,7 @@
"pwdWillExpire":"٪ s من الأيام و٪ s من الساعات و٪ s من الدقائق و٪ s من الثواني قبل انتهاء صلاحية كلمة المرور، قم بتغييرها!",
"redirectedFrom":"تمت إعادة توجيهك من",
"redirectedIn":"ستتم إعادة توجيهك خلال 30 ثانية",
"redirectionInProgres":"جار إعادة التوجيه قيد التقدم ...",
"redirectionInProgress":"جار إعادة التوجيه قيد التقدم ...",
"redirectionToIdp":"إعادة توجيهك إلى موفر الهوية الخاص بك",
"refreshrights":"قم بتحديث حقوقي",
"refuse":"رفض",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,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",

View File

@ -189,7 +189,7 @@
"pwdWillExpire":"距离密码失效还有 %d 天, %d 小时, %d 分钟, %d 秒, 请修改!",
"redirectedFrom":"您重定向自",
"redirectedIn":"您将30秒后重定向",
"redirectionInProgres":"重定向进行中",
"redirectionInProgress":"重定向进行中",
"redirectionToIdp":"重定向至你的Identity Provider",
"refreshrights":"刷新我的权限",
"refuse":"拒绝",

View File

@ -1 +0,0 @@
../common/redirect.tpl

View File

@ -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">

View File

@ -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>

View File

@ -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,
}
},
},
}
);
}

View File

@ -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,
}
}
}
);
}

View File

@ -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 => ';',
}
}
);
}

View File

@ -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

View File

@ -4,7 +4,7 @@ use IO::String;
use Data::Dumper;
require 't/test-lib.pm';
my $maintests = 29;
my $maintests = 51;
SKIP: {
eval { require Convert::Base32 };
@ -54,7 +54,6 @@ SKIP: {
'Form registration'
);
#expectRedirection( $res, qr#/2fregisters/totp$# );
ok(
$res = $client->_get(
'/2fregisters/totp',
@ -70,6 +69,7 @@ SKIP: {
) or print STDERR Dumper( $res->[2]->[0] );
count(1);
# Register TOTP
# JS query
ok(
$res = $client->_post(
@ -105,7 +105,7 @@ SKIP: {
eval { $res = JSON::from_json( $res->[2]->[0] ) };
ok( not($@), 'Content is JSON' )
or explain( $res->[2]->[0], 'JSON content' );
ok( $res->{result} == 1, 'Key is registered' );
ok( $res->{result} == 1, 'TOTP is registered' );
# Try to sign-in
$client->logout($id);
@ -132,6 +132,7 @@ SKIP: {
);
$id = expectCookie($res);
# Get 2F register form
ok(
$res = $client->_get(
'/2fregisters',
@ -142,7 +143,6 @@ SKIP: {
);
ok( $res->[2]->[0] =~ /2fregistration\.(?:min\.)?js/,
'Found 2f registration js' );
ok( $res->[2]->[0] =~ qr%<img src="/static/bootstrap/totp.png".*?/>%,
'Found totp.png' )
or print STDERR Dumper( $res->[2]->[0] );
@ -155,9 +155,129 @@ SKIP: {
ok( $res->[2]->[0] =~ qr%<a href="/2fregisters/totp".*?>%,
'Found 2fregisters/totp link' )
or print STDERR Dumper( $res->[2]->[0] );
# Get U2F register form
ok(
$res = $client->_get(
'/2fregisters/u',
cookie => "lemonldap=$id",
accept => 'text/html',
),
'Form registration'
);
ok( $res->[2]->[0] =~ /u2fregistration\.(?:min\.)?js/, 'Found U2F js' );
ok(
$res->[2]->[0] =~ qr%<img src="/static/common/logos/logo_llng_old.png"%,
'Found custom Main Logo'
) or print STDERR Dumper( $res->[2]->[0] );
# Wait to have two different epoch values
sleep 1;
# Ajax registration request
ok(
$res = $client->_post(
'/2fregisters/u/register', IO::String->new(''),
accept => 'application/json',
cookie => "lemonldap=$id",
length => 0,
),
'Get registration challenge'
);
expectOK($res);
my $data;
eval { $data = JSON::from_json( $res->[2]->[0] ) };
ok( not($@), ' Content is JSON' )
or explain( [ $@, $res->[2] ], 'JSON content' );
ok( ( $data->{challenge} and $data->{appId} ), ' Get challenge and appId' )
or explain( $data, 'challenge and appId' );
# Build U2F tester
my $tester = Authen::U2F::Tester->new(
certificate => Crypt::OpenSSL::X509->new_from_string(
'-----BEGIN CERTIFICATE-----
MIIB6DCCAY6gAwIBAgIJAJKuutkN2sAfMAoGCCqGSM49BAMCME8xCzAJBgNVBAYT
AlVTMQ4wDAYDVQQIDAVUZXhhczEaMBgGA1UECgwRVW50cnVzdGVkIFUyRiBPcmcx
FDASBgNVBAMMC3ZpcnR1YWwtdTJmMB4XDTE4MDMyODIwMTc1OVoXDTI3MTIyNjIw
MTc1OVowTzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRowGAYDVQQKDBFV
bnRydXN0ZWQgVTJGIE9yZzEUMBIGA1UEAwwLdmlydHVhbC11MmYwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAAQTij+9mI1FJdvKNHLeSQcOW4ob3prvIXuEGJMrQeJF
6OYcgwxrVqsmNMl5w45L7zx8ryovVOti/mtqkh2pQjtpo1MwUTAdBgNVHQ4EFgQU
QXKKf+rrZwA4WXDCU/Vebe4gYXEwHwYDVR0jBBgwFoAUQXKKf+rrZwA4WXDCU/Ve
be4gYXEwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAiCdOEmw5
hknzHR1FoyFZKRrcJu17a1PGcqTFMJHTC70CIHeCZ8KVuuMIPjoofQd1l1E221rv
RJY1Oz1fUNbrIPsL
-----END CERTIFICATE-----', Crypt::OpenSSL::X509::FORMAT_PEM()
),
key => Crypt::PK::ECC->new(
\'-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOdbZw1swQIL+RZoDQ9zwjWY5UjA1NO81WWjwbmznUbgoAoGCCqGSM49
AwEHoUQDQgAEE4o/vZiNRSXbyjRy3kkHDluKG96a7yF7hBiTK0HiRejmHIMMa1ar
JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
-----END EC PRIVATE KEY-----'
),
);
my $r = $tester->register( $data->{appId}, $data->{challenge} );
ok( $r->is_success, ' Good challenge value' )
or diag( $r->error_message );
my $registrationData = JSON::to_json( {
clientData => $r->client_data,
errorCode => 0,
registrationData => $r->registration_data,
version => "U2F_V2"
}
);
my ( $host, $url, $query );
$query = Lemonldap::NG::Common::FormEncode::build_urlencoded(
registration => $registrationData,
challenge => $res->[2]->[0],
);
ok(
$res = $client->_post(
'/2fregisters/u/registration', IO::String->new($query),
length => length($query),
accept => 'application/json',
cookie => "lemonldap=$id",
),
'Push registration data'
);
expectOK($res);
eval { $data = JSON::from_json( $res->[2]->[0] ) };
ok( not($@), ' Content is JSON' )
or explain( [ $@, $res->[2] ], 'JSON content' );
ok( $data->{result} == 1, 'Key is registered' )
or explain( $data, '"result":1' );
# Get 2F register form
ok(
$res = $client->_get(
'/2fregisters',
cookie => "lemonldap=$id",
accept => 'text/html',
),
'Form 2fregisters'
);
ok( $res->[2]->[0] =~ /2fregistration\.(?:min\.)?js/,
'Found 2f registration js' );
ok( $res->[2]->[0] =~ qr%<a href="/2fregisters/u" class="nodecor">%,
'Found 2fregisters/u link' )
or print STDERR Dumper($res);
ok( $res->[2]->[0] =~ qr%<a href="/2fregisters/totp" class="nodecor">%,
'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] );
ok(
$res = $client->_post(
'/2fregisters/totp/delete',
@ -167,7 +287,10 @@ SKIP: {
),
'Delete TOTP query'
);
ok( $data->{result} == 1, 'TOTP is unregistered' )
or explain( $data, '"result":1' );
# Get 2F register form
ok(
$res = $client->_get(
'/2fregisters',
@ -176,20 +299,38 @@ SKIP: {
),
'Form 2fregisters'
);
ok( $res->[2]->[0] =~ /2fregistration\.(?:min\.)?js/,
'Found 2f registration js' );
ok( $res->[2]->[0] =~ qr%<a href="/2fregisters/u" class="nodecor">%,
'Found 2fregisters/u link' )
or print STDERR Dumper($res);
ok( $res->[2]->[0] =~ qr%<a href="/2fregisters/totp" class="nodecor">%,
'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] );
ok(
$res->[2]->[0] !~
qr%<td class="align-middle" >TOTP</td><td class="align-middle">(\d{10})</td><td class="data-epoch">\d{10}</td>%,
"TOTP deleted"
) or print STDERR Dumper( $res->[2]->[0] );
$res = $client->_post(
'/2fregisters/u/delete',
IO::String->new("epoch=$1"),
length => 16,
cookie => "lemonldap=$id",
),
'Delete U2F key query'
);
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);
}