Merge branch 'v2.0'
This commit is contained in:
commit
174193e74c
105
lemonldap-ng-common/.prove
Normal file
105
lemonldap-ng-common/.prove
Normal file
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
generation: 2
|
||||
last_run_time: 1567071551.30841
|
||||
tests:
|
||||
t/01-Common-Conf.t:
|
||||
elapsed: 0.472490072250366
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.71014
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.71014
|
||||
last_todo: 0
|
||||
seq: 5
|
||||
total_passes: 1
|
||||
t/02-Common-Conf-File.t:
|
||||
elapsed: 0.0793302059173584
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.68052
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.68052
|
||||
last_todo: 0
|
||||
seq: 4
|
||||
total_passes: 1
|
||||
t/03-Common-Conf-CDBI.t:
|
||||
elapsed: 0.61043119430542
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.95767
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.95767
|
||||
last_todo: 0
|
||||
seq: 6
|
||||
total_passes: 1
|
||||
t/03-Common-Conf-RDBI.t:
|
||||
elapsed: 0.66497802734375
|
||||
gen: 2
|
||||
last_pass_time: 1567071551.00435
|
||||
last_result: 0
|
||||
last_run_time: 1567071551.00435
|
||||
last_todo: 0
|
||||
seq: 7
|
||||
total_passes: 1
|
||||
t/05-Common-Conf-LDAP.t:
|
||||
elapsed: 0.64878511428833
|
||||
gen: 2
|
||||
last_pass_time: 1567071551.07637
|
||||
last_result: 0
|
||||
last_run_time: 1567071551.07637
|
||||
last_todo: 0
|
||||
seq: 8
|
||||
total_passes: 1
|
||||
t/30-Common-Safelib.t:
|
||||
elapsed: 0.0283739566802979
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.40529
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.40529
|
||||
last_todo: 0
|
||||
seq: 1
|
||||
total_passes: 1
|
||||
t/35-Common-Crypto.t:
|
||||
elapsed: 0.190783977508545
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.63236
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.63236
|
||||
last_todo: 0
|
||||
seq: 3
|
||||
total_passes: 1
|
||||
t/36-Common-Regexp.t:
|
||||
elapsed: 0.0631709098815918
|
||||
gen: 2
|
||||
last_pass_time: 1567071550.50944
|
||||
last_result: 0
|
||||
last_run_time: 1567071550.50944
|
||||
last_todo: 0
|
||||
seq: 2
|
||||
total_passes: 1
|
||||
t/40-Common-Session.t:
|
||||
elapsed: 0.184284210205078
|
||||
gen: 2
|
||||
last_pass_time: 1567071551.11977
|
||||
last_result: 0
|
||||
last_run_time: 1567071551.11977
|
||||
last_todo: 0
|
||||
seq: 9
|
||||
total_passes: 1
|
||||
t/50-Combination-Parser.t:
|
||||
elapsed: 0.108580827713013
|
||||
gen: 2
|
||||
last_pass_time: 1567071551.1593
|
||||
last_result: 0
|
||||
last_run_time: 1567071551.1593
|
||||
last_todo: 0
|
||||
seq: 10
|
||||
total_passes: 1
|
||||
t/99-pod.t:
|
||||
elapsed: 0.128799915313721
|
||||
gen: 2
|
||||
last_pass_time: 1567071551.30716
|
||||
last_result: 0
|
||||
last_run_time: 1567071551.30716
|
||||
last_todo: 0
|
||||
seq: 11
|
||||
total_passes: 1
|
||||
version: 1
|
||||
...
|
|
@ -9,6 +9,8 @@ our $VERSION = '2.1.0';
|
|||
sub compactConf {
|
||||
my ( $self, $conf ) = @_;
|
||||
|
||||
return $conf if ( $conf->{'dontCompactConf'} );
|
||||
|
||||
# Remove unused auth parameters
|
||||
my %keep;
|
||||
foreach my $type (qw(authentication userDB passwordDB registerDB)) {
|
||||
|
|
|
@ -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|fExtra)|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|fRemovedUseNotif|howLanguages|slByAjax)|o(?:idc(?:ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|RPMetaDataOptions(?:LogoutSessionRequired|BypassConsent|RequirePKCE|Public)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:PersistentInfo|EmptyValues))?|State|XSS)|o(?:ntextSwitchingStopWithLogout|rsEnabled)|da)|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)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?)?|y(?:Deleted|Other))|AjaxHook)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|d(?:isablePersistentStorage|biDynamicHashEnabled)|rest(?:(?:Session|Config)Server|ExportSecretKeys)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|(?:activeTim|wsdlServ)er|krb(?:RemoveDomain|ByJs)|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|fRemovedUseNotif|howLanguages|slByAjax)|o(?:idc(?:ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration)|RPMetaDataOptions(?:LogoutSessionRequired|BypassConsent|RequirePKCE|Public)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:ptcha_(?:register|login|mail)_enabled|sSrvMetaDataOptions(?:Gateway|Renew))|heck(?:User(?:Display(?:PersistentInfo|EmptyValues))?|State|XSS)|o(?:ntextSwitchingStopWithLogout|rsEnabled)|da)|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)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?)?|y(?:Deleted|Other))|AjaxHook)|to(?:tp2f(?:UserCan(?:Chang|Remov)eKey|DisplayExistingSecret)|kenUseGlobalStorage)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|d(?:isablePersistentStorage|biDynamicHashEnabled|ontCompactConf)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|rest(?:(?:Session|Config)Server|ExportSecretKeys)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|(?:activeTim|wsdlServ)er|krb(?:RemoveDomain|ByJs)|bruteForceProtection)$/;
|
||||
|
||||
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );
|
||||
|
||||
|
|
|
@ -179,55 +179,56 @@ sub defaultValues {
|
|||
'loa-4' => 4,
|
||||
'loa-5' => 5
|
||||
},
|
||||
'oidcServiceMetaDataAuthorizeURI' => 'authorize',
|
||||
'oidcServiceMetaDataBackChannelURI' => 'blogout',
|
||||
'oidcServiceMetaDataCheckSessionURI' => 'checksession.html',
|
||||
'oidcServiceMetaDataEndSessionURI' => 'logout',
|
||||
'oidcServiceMetaDataFrontChannelURI' => 'flogout',
|
||||
'oidcServiceMetaDataJWKSURI' => 'jwks',
|
||||
'oidcServiceMetaDataRegistrationURI' => 'register',
|
||||
'oidcServiceMetaDataTokenURI' => 'token',
|
||||
'oidcServiceMetaDataUserInfoURI' => 'userinfo',
|
||||
'openIdAuthnLevel' => 1,
|
||||
'openIdExportedVars' => {},
|
||||
'openIdIDPList' => '0;',
|
||||
'openIdSPList' => '0;',
|
||||
'openIdSreg_email' => 'mail',
|
||||
'openIdSreg_fullname' => 'cn',
|
||||
'openIdSreg_nickname' => 'uid',
|
||||
'openIdSreg_timezone' => '_timezone',
|
||||
'pamAuthnLevel' => 2,
|
||||
'pamService' => 'login',
|
||||
'passwordDB' => 'Demo',
|
||||
'passwordResetAllowedRetries' => 3,
|
||||
'port' => -1,
|
||||
'portal' => 'http://auth.example.com/',
|
||||
'portalAntiFrame' => 1,
|
||||
'portalCheckLogins' => 1,
|
||||
'portalDisplayAppslist' => 1,
|
||||
'portalDisplayChangePassword' => '$_auth =~ /^(LDAP|DBI|Demo)$/',
|
||||
'portalDisplayFavApps' => 1,
|
||||
'portalDisplayLoginHistory' => 1,
|
||||
'portalDisplayLogout' => 1,
|
||||
'portalDisplayOidcConsents' => '$_oidcConnectedRP',
|
||||
'portalDisplayRegister' => 1,
|
||||
'portalErrorOnExpiredSession' => 1,
|
||||
'portalForceAuthnInterval' => 5,
|
||||
'portalMainLogo' => 'common/logos/logo_llng_400px.png',
|
||||
'portalPingInterval' => 60000,
|
||||
'portalRequireOldPassword' => 1,
|
||||
'portalSkin' => 'bootstrap',
|
||||
'portalUserAttr' => '_user',
|
||||
'proxyAuthnLevel' => 2,
|
||||
'radius2fActivation' => 0,
|
||||
'radius2fTimeout' => 20,
|
||||
'radiusAuthnLevel' => 3,
|
||||
'randomPasswordRegexp' => '[A-Z]{3}[a-z]{5}.\\d{2}',
|
||||
'redirectFormMethod' => 'get',
|
||||
'registerDB' => 'Null',
|
||||
'registerTimeout' => 0,
|
||||
'registerUrl' => 'http://auth.example.com/register',
|
||||
'reloadTimeout' => 5,
|
||||
'oidcServiceMetaDataAuthorizeURI' => 'authorize',
|
||||
'oidcServiceMetaDataBackChannelURI' => 'blogout',
|
||||
'oidcServiceMetaDataCheckSessionURI' => 'checksession.html',
|
||||
'oidcServiceMetaDataEndSessionURI' => 'logout',
|
||||
'oidcServiceMetaDataFrontChannelURI' => 'flogout',
|
||||
'oidcServiceMetaDataIntrospectionURI' => 'introspect',
|
||||
'oidcServiceMetaDataJWKSURI' => 'jwks',
|
||||
'oidcServiceMetaDataRegistrationURI' => 'register',
|
||||
'oidcServiceMetaDataTokenURI' => 'token',
|
||||
'oidcServiceMetaDataUserInfoURI' => 'userinfo',
|
||||
'openIdAuthnLevel' => 1,
|
||||
'openIdExportedVars' => {},
|
||||
'openIdIDPList' => '0;',
|
||||
'openIdSPList' => '0;',
|
||||
'openIdSreg_email' => 'mail',
|
||||
'openIdSreg_fullname' => 'cn',
|
||||
'openIdSreg_nickname' => 'uid',
|
||||
'openIdSreg_timezone' => '_timezone',
|
||||
'pamAuthnLevel' => 2,
|
||||
'pamService' => 'login',
|
||||
'passwordDB' => 'Demo',
|
||||
'passwordResetAllowedRetries' => 3,
|
||||
'port' => -1,
|
||||
'portal' => 'http://auth.example.com/',
|
||||
'portalAntiFrame' => 1,
|
||||
'portalCheckLogins' => 1,
|
||||
'portalDisplayAppslist' => 1,
|
||||
'portalDisplayChangePassword' => '$_auth =~ /^(LDAP|DBI|Demo)$/',
|
||||
'portalDisplayFavApps' => 1,
|
||||
'portalDisplayLoginHistory' => 1,
|
||||
'portalDisplayLogout' => 1,
|
||||
'portalDisplayOidcConsents' => '$_oidcConnectedRP',
|
||||
'portalDisplayRegister' => 1,
|
||||
'portalErrorOnExpiredSession' => 1,
|
||||
'portalForceAuthnInterval' => 5,
|
||||
'portalMainLogo' => 'common/logos/logo_llng_400px.png',
|
||||
'portalPingInterval' => 60000,
|
||||
'portalRequireOldPassword' => 1,
|
||||
'portalSkin' => 'bootstrap',
|
||||
'portalUserAttr' => '_user',
|
||||
'proxyAuthnLevel' => 2,
|
||||
'radius2fActivation' => 0,
|
||||
'radius2fTimeout' => 20,
|
||||
'radiusAuthnLevel' => 3,
|
||||
'randomPasswordRegexp' => '[A-Z]{3}[a-z]{5}.\\d{2}',
|
||||
'redirectFormMethod' => 'get',
|
||||
'registerDB' => 'Null',
|
||||
'registerTimeout' => 0,
|
||||
'registerUrl' => 'http://auth.example.com/register',
|
||||
'reloadTimeout' => 5,
|
||||
'remoteGlobalStorage' => 'Lemonldap::NG::Common::Apache::Session::SOAP',
|
||||
'remoteGlobalStorageOptions' => {
|
||||
'ns' =>
|
||||
|
|
|
@ -67,6 +67,6 @@ our $issuerParameters = {
|
|||
issuerDBSAML => [qw(issuerDBSAMLActivation issuerDBSAMLPath issuerDBSAMLRule)],
|
||||
};
|
||||
our $samlServiceParameters = [qw(samlEntityID samlServicePrivateKeySig samlServicePrivateKeySigPwd samlServicePublicKeySig samlServicePrivateKeyEnc samlServicePrivateKeyEncPwd samlServicePublicKeyEnc samlServiceUseCertificateInResponse samlServiceSignatureMethod samlNameIDFormatMapEmail samlNameIDFormatMapX509 samlNameIDFormatMapWindows samlNameIDFormatMapKerberos samlAuthnContextMapPassword samlAuthnContextMapPasswordProtectedTransport samlAuthnContextMapTLSClient samlAuthnContextMapKerberos samlOrganizationDisplayName samlOrganizationName samlOrganizationURL samlSPSSODescriptorAuthnRequestsSigned samlSPSSODescriptorWantAssertionsSigned samlSPSSODescriptorSingleLogoutServiceHTTPRedirect samlSPSSODescriptorSingleLogoutServiceHTTPPost samlSPSSODescriptorSingleLogoutServiceSOAP samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact samlSPSSODescriptorAssertionConsumerServiceHTTPPost samlSPSSODescriptorArtifactResolutionServiceArtifact samlIDPSSODescriptorWantAuthnRequestsSigned samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect samlIDPSSODescriptorSingleSignOnServiceHTTPPost samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect samlIDPSSODescriptorSingleLogoutServiceHTTPPost samlIDPSSODescriptorSingleLogoutServiceSOAP samlIDPSSODescriptorArtifactResolutionServiceArtifact samlAttributeAuthorityDescriptorAttributeServiceSOAP samlIdPResolveCookie samlMetadataForceUTF8 samlStorage samlStorageOptions samlRelayStateTimeout samlUseQueryStringSpecific samlCommonDomainCookieActivation samlCommonDomainCookieDomain samlCommonDomainCookieReader samlCommonDomainCookieWriter samlDiscoveryProtocolActivation samlDiscoveryProtocolURL samlDiscoveryProtocolPolicy samlDiscoveryProtocolIsPassive samlOverrideIDPEntityID)];
|
||||
our $oidcServiceParameters = [qw(oidcServiceMetaDataIssuer oidcServiceMetaDataAuthorizeURI oidcServiceMetaDataTokenURI oidcServiceMetaDataUserInfoURI oidcServiceMetaDataJWKSURI oidcServiceMetaDataRegistrationURI oidcServiceMetaDataEndSessionURI oidcServiceMetaDataCheckSessionURI oidcServiceMetaDataFrontChannelURI oidcServiceMetaDataBackChannelURI oidcServiceMetaDataAuthnContext oidcServicePrivateKeySig oidcServicePublicKeySig oidcServiceKeyIdSig oidcServiceAllowDynamicRegistration oidcServiceAllowAuthorizationCodeFlow oidcServiceAllowImplicitFlow oidcServiceAllowHybridFlow oidcStorage oidcStorageOptions)];
|
||||
our $oidcServiceParameters = [qw(oidcServiceMetaDataIssuer oidcServiceMetaDataAuthorizeURI oidcServiceMetaDataTokenURI oidcServiceMetaDataUserInfoURI oidcServiceMetaDataJWKSURI oidcServiceMetaDataRegistrationURI oidcServiceMetaDataIntrospectionURI oidcServiceMetaDataEndSessionURI oidcServiceMetaDataCheckSessionURI oidcServiceMetaDataFrontChannelURI oidcServiceMetaDataBackChannelURI oidcServiceMetaDataAuthnContext oidcServicePrivateKeySig oidcServicePublicKeySig oidcServiceKeyIdSig oidcServiceAllowDynamicRegistration oidcServiceAllowAuthorizationCodeFlow oidcServiceAllowImplicitFlow oidcServiceAllowHybridFlow oidcStorage oidcStorageOptions)];
|
||||
|
||||
1;
|
||||
|
|
|
@ -380,7 +380,8 @@ sub headersInit {
|
|||
$class->tsv->{headerList}->{$vhost} = [ keys %headers ];
|
||||
my $sub = '';
|
||||
foreach ( keys %headers ) {
|
||||
my $val = $class->substitute( $headers{$_} ) . " || ''";
|
||||
$headers{$_} ||= "''";
|
||||
my $val = $class->substitute( $headers{$_} ) . " // ''";
|
||||
$sub .= "('$_' => $val),";
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ init(
|
|||
exportedHeaders => {
|
||||
'test2.example.com' => {
|
||||
'Auth-User' => '$uid',
|
||||
'empty' => undef,
|
||||
'zero' => "'0'",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +116,13 @@ ok(
|
|||
ok( $res->[0] == 200, 'Code is 200' ) or explain( $res->[0], 200 );
|
||||
count(2);
|
||||
|
||||
my %headers = @{ $res->[1] };
|
||||
ok( $headers{'zero'} eq '0', 'Found "zero" header with "0"' )
|
||||
or print STDERR Data::Dumper::Dumper( $res->[1] );
|
||||
ok( $headers{'empty'} eq '', 'Found "empty" header without value' )
|
||||
or print STDERR Data::Dumper::Dumper( $res->[1] );
|
||||
count(2);
|
||||
|
||||
@headers = grep { /service|^XFromVH$/ } @{ $res->[1] };
|
||||
@values = grep { /\.example\.com|^$sessionId$/ } @{ $res->[1] };
|
||||
ok( @headers == 2, 'Found 2 service headers' )
|
||||
|
|
|
@ -1111,6 +1111,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
|
|||
qr/^(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])[.]?))?$/,
|
||||
'type' => 'text'
|
||||
},
|
||||
'dontCompactConf' => {
|
||||
'default' => 0,
|
||||
'type' => 'bool'
|
||||
},
|
||||
'exportedAttr' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
|
@ -2107,6 +2111,10 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
|
|||
'default' => 'flogout',
|
||||
'type' => 'text'
|
||||
},
|
||||
'oidcServiceMetaDataIntrospectionURI' => {
|
||||
'default' => 'introspect',
|
||||
'type' => 'text'
|
||||
},
|
||||
'oidcServiceMetaDataIssuer' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
|
|
|
@ -385,6 +385,11 @@ sub attributes {
|
|||
msgFail => '__badUrl__',
|
||||
documentation => 'URL to call on reload',
|
||||
},
|
||||
dontCompactConf => {
|
||||
type => 'bool',
|
||||
default => 0,
|
||||
documentation => 'Don t compact configuration',
|
||||
},
|
||||
portalMainLogo => {
|
||||
type => 'text',
|
||||
default => 'common/logos/logo_llng_400px.png',
|
||||
|
@ -3483,6 +3488,11 @@ m{^(?:ldapi://[^/]*/?|\w[\w\-\.]*(?::\d{1,5})?|ldap(?:s|\+tls)?://\w[\w\-\.]*(?:
|
|||
default => 'register',
|
||||
documentation => 'OpenID Connect registration endpoint',
|
||||
},
|
||||
oidcServiceMetaDataIntrospectionURI => {
|
||||
type => 'text',
|
||||
default => 'introspect',
|
||||
documentation => 'OpenID Connect introspection endpoint',
|
||||
},
|
||||
oidcServiceMetaDataEndSessionURI => {
|
||||
type => 'text',
|
||||
default => 'logout',
|
||||
|
|
|
@ -511,7 +511,8 @@ sub tree {
|
|||
title => 'logParams',
|
||||
help => 'logs.html',
|
||||
form => 'simpleInputContainer',
|
||||
nodes => [ 'whatToTrace', 'customToTrace', 'hiddenAttributes' ]
|
||||
nodes =>
|
||||
[ 'whatToTrace', 'customToTrace', 'hiddenAttributes' ]
|
||||
},
|
||||
{
|
||||
title => 'cookieParams',
|
||||
|
@ -564,7 +565,7 @@ sub tree {
|
|||
{
|
||||
title => 'reloadParams',
|
||||
help => 'configlocation.html#configuration_reload',
|
||||
nodes => [ 'reloadUrls', 'reloadTimeout', ]
|
||||
nodes => [ 'reloadUrls', 'reloadTimeout', 'dontCompactConf' ]
|
||||
},
|
||||
{
|
||||
title => 'plugins',
|
||||
|
@ -1125,6 +1126,7 @@ sub tree {
|
|||
'oidcServiceMetaDataUserInfoURI',
|
||||
'oidcServiceMetaDataJWKSURI',
|
||||
'oidcServiceMetaDataRegistrationURI',
|
||||
'oidcServiceMetaDataIntrospectionURI',
|
||||
'oidcServiceMetaDataEndSessionURI',
|
||||
'oidcServiceMetaDataCheckSessionURI',
|
||||
'oidcServiceMetaDataFrontChannelURI',
|
||||
|
|
|
@ -103,13 +103,30 @@ sub check {
|
|||
hdebug(" testNewConf() failed");
|
||||
return 0;
|
||||
}
|
||||
my $separator = $self->newConf->{multiValuesSeparator} || '; ';
|
||||
hdebug(" tests succeed");
|
||||
$self->compactConf( $self->newConf );
|
||||
my %conf = %{ $self->newConf() };
|
||||
my %compactedConf = %{ $self->compactConf( $self->newConf ) };
|
||||
my @removedKeys = ();
|
||||
unless ( $self->confChanged ) {
|
||||
hdebug(" no change detected");
|
||||
$self->message('__confNotChanged__');
|
||||
return 0;
|
||||
}
|
||||
unless ( $self->newConf->{dontCompactConf} ) {
|
||||
foreach ( sort keys %conf ) {
|
||||
push @removedKeys, $_ unless exists $compactedConf{$_};
|
||||
}
|
||||
}
|
||||
push @{ $self->changes },
|
||||
(
|
||||
$self->{newConf}->{dontCompactConf}
|
||||
? { confCompacted => '0' }
|
||||
: {
|
||||
confCompacted => '1',
|
||||
removedKeys => join( $separator, @removedKeys )
|
||||
}
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -252,6 +252,7 @@
|
|||
"dateTitle":"تاريخ",
|
||||
"dn":"دي أن",
|
||||
"domain":"نطاق",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"تحميل",
|
||||
"downloadIt":"نزله",
|
||||
"duplicate":"مكررة",
|
||||
|
@ -570,6 +571,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"المفاتيح",
|
||||
"oidcServiceMetaDataRegistrationURI":"التسجيل",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"الحماية",
|
||||
"oidcServiceMetaDataEndSessionURI":"نهاية الجلسة",
|
||||
"oidcServiceMetaDataAuthnContext":"سياق إثبات الهوية",
|
||||
|
@ -1051,4 +1053,4 @@
|
|||
"samlRelayStateTimeout":"تناوب حالة مهلة الجلسة ",
|
||||
"samlUseQueryStringSpecific":"استخدام أسلوب query_string المعين",
|
||||
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"Dates",
|
||||
"dn":"DN",
|
||||
"domain":"Domain",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"Download",
|
||||
"downloadIt":"Download it",
|
||||
"duplicate":"Duplicate",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"Keys",
|
||||
"oidcServiceMetaDataRegistrationURI":"Registration",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Security",
|
||||
"oidcServiceMetaDataEndSessionURI":"End of session",
|
||||
"oidcServiceMetaDataAuthnContext":"Authentication context",
|
||||
|
@ -1050,4 +1052,4 @@
|
|||
"samlRelayStateTimeout":"RelayState session timeout",
|
||||
"samlUseQueryStringSpecific":"Use specific query_string method",
|
||||
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"Dates",
|
||||
"dn":"DN",
|
||||
"domain":"Domain",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"Download",
|
||||
"downloadIt":"Download it",
|
||||
"duplicate":"Duplicate",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"Keys",
|
||||
"oidcServiceMetaDataRegistrationURI":"Registration",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Security",
|
||||
"oidcServiceMetaDataEndSessionURI":"End of session",
|
||||
"oidcServiceMetaDataAuthnContext":"Authentication context",
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"Dates",
|
||||
"dn":"DN",
|
||||
"domain":"Domaine",
|
||||
"dontCompactConf":"Ne pas compacter le fichier de configuration",
|
||||
"download":"Télécharger",
|
||||
"downloadIt":"Télécharger",
|
||||
"duplicate":"Dupliquer",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"Clefs",
|
||||
"oidcServiceMetaDataRegistrationURI":"Enregistrement",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Sécurité",
|
||||
"oidcServiceMetaDataEndSessionURI":"Fin de session",
|
||||
"oidcServiceMetaDataAuthnContext":"Contexte d'authentification",
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"Date",
|
||||
"dn":"DN",
|
||||
"domain":"Dominio",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"Scarica",
|
||||
"downloadIt":"Scaricalo",
|
||||
"duplicate":"Duplicato",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"Chiavi",
|
||||
"oidcServiceMetaDataRegistrationURI":"Registrazione",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Sicurezza",
|
||||
"oidcServiceMetaDataEndSessionURI":"Fine sessione",
|
||||
"oidcServiceMetaDataAuthnContext":"Contesto di autenticazione",
|
||||
|
@ -1050,4 +1052,4 @@
|
|||
"samlRelayStateTimeout":"Timeout di sessione di RelayState",
|
||||
"samlUseQueryStringSpecific":"Utilizza il metodo specifico query_string",
|
||||
"samlOverrideIDPEntityID":"Sostituisci l'ID entità quando agisce come IDP"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"Ngày",
|
||||
"dn":"DN",
|
||||
"domain":"Tên miền",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"Tải xuống",
|
||||
"downloadIt":"Tải xuống",
|
||||
"duplicate":"Sao y",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"Khóa",
|
||||
"oidcServiceMetaDataRegistrationURI":"Đăng ký",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Bảo mật",
|
||||
"oidcServiceMetaDataEndSessionURI":"Kết thúc phiên",
|
||||
"oidcServiceMetaDataAuthnContext":"Ngữ cảnh xác thực",
|
||||
|
@ -1050,4 +1052,4 @@
|
|||
"samlRelayStateTimeout":"Thời gian hết hạn phiên RelayState ",
|
||||
"samlUseQueryStringSpecific":"Sử dụng phương pháp query_string cụ thể",
|
||||
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
"dateTitle":"日期",
|
||||
"dn":"LDAP 唯一名称",
|
||||
"domain":"域",
|
||||
"dontCompactConf":"Don't compact configuration file",
|
||||
"download":"下载",
|
||||
"downloadIt":"下载它",
|
||||
"duplicate":"Duplicate",
|
||||
|
@ -569,6 +570,7 @@
|
|||
"oidcServiceMetaDataJWKSURI":"JWKS",
|
||||
"oidcServiceMetaDataKeys":"键值",
|
||||
"oidcServiceMetaDataRegistrationURI":"Registration",
|
||||
"oidcServiceMetaDataIntrospectionURI":"Introspection",
|
||||
"oidcServiceMetaDataSecurity":"Security",
|
||||
"oidcServiceMetaDataEndSessionURI":"End of session",
|
||||
"oidcServiceMetaDataAuthnContext":"Authentication context",
|
||||
|
@ -1050,4 +1052,4 @@
|
|||
"samlRelayStateTimeout":"RelayState session timeout",
|
||||
"samlUseQueryStringSpecific":"Use specific query_string method",
|
||||
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -38,7 +38,7 @@ while ( my $body = &body() ) {
|
|||
|
||||
#print STDERR Dumper($resBody);
|
||||
ok( $resBody->{result} == 1, "$desc: JSON response contains \"result:1\"" );
|
||||
ok( @{ $resBody->{details}->{__changes__} } eq 1,
|
||||
ok( @{ $resBody->{details}->{__changes__} } eq 2,
|
||||
"$desc: conf has changed" )
|
||||
or print STDERR Dumper($resBody);
|
||||
ok(
|
||||
|
|
|
@ -54,8 +54,8 @@ ok(
|
|||
) or print STDERR Dumper($resBody);
|
||||
|
||||
ok(
|
||||
@{ $resBody->{details}->{__changes__} } == 22,
|
||||
'JSON response contains 24 changes'
|
||||
@{ $resBody->{details}->{__changes__} } == 23,
|
||||
'JSON response contains 23 changes'
|
||||
) or print STDERR Dumper($resBody);
|
||||
|
||||
#print STDERR Dumper($resBody);
|
||||
|
@ -113,11 +113,10 @@ ok( $res = &client->jsonResponse('/diff/1/2'), 'Diff called' );
|
|||
my ( @c1, @c2 );
|
||||
ok( ( @c1 = sort keys %{ $res->[0] } ), 'diff() detects changes in conf 1' );
|
||||
ok( ( @c2 = sort keys %{ $res->[1] } ), 'diff() detects changes in conf 2' );
|
||||
ok( @c1 == 12, '11 keys changed in conf 1' )
|
||||
ok( @c1 == 12, '12 keys changed in conf 1' )
|
||||
or print STDERR "Expect: 12 keys, get: " . join( ', ', @c1 ) . "\n";
|
||||
ok( @c2 == 16, '14 keys changed or created in conf 2' )
|
||||
ok( @c2 == 16, '16 keys changed or created in conf 2' )
|
||||
or print STDERR "Expect: 16 keys, get: " . join( ',', @c2 ) . "\n";
|
||||
|
||||
count(5);
|
||||
|
||||
unlink $confFiles->[1];
|
||||
|
@ -246,6 +245,10 @@ sub changes {
|
|||
'new' => 0,
|
||||
'key' => 'captcha_mail_enabled',
|
||||
'old' => '1'
|
||||
},
|
||||
{
|
||||
'confCompacted' => '1',
|
||||
'removedKeys' => 'some; keys'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ foreach my $i ( 0 .. 1 ) {
|
|||
}
|
||||
|
||||
ok(
|
||||
@{ $resBody->{details}->{__changes__} } == 22,
|
||||
@{ $resBody->{details}->{__changes__} } == 23,
|
||||
'JSON response contains 22 changes'
|
||||
) or print STDERR Dumper($resBody);
|
||||
|
||||
|
@ -224,6 +224,10 @@ sub changes {
|
|||
{
|
||||
'key' => 'virtualHosts',
|
||||
'old' => 'test2.example.com'
|
||||
},
|
||||
{
|
||||
'confCompacted' => '1',
|
||||
'removedKeys' => 'some; keys'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,7 +34,7 @@ sub authenticate {
|
|||
{ user => $req->user, password => $req->data->{password} } );
|
||||
};
|
||||
if ($@) {
|
||||
$self->logger("Auth error: $@");
|
||||
$self->logger->error("Auth error: $@");
|
||||
$self->setSecurity($req);
|
||||
return PE_ERROR;
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ has iss => (
|
|||
# - userinfo : => userInfo() for unauth users (RP)
|
||||
# - jwks : => jwks() for unauth users (RP)
|
||||
# - register : => registration() for unauth users (RP)
|
||||
# - introspect : => introspection() for unauth users (RP)
|
||||
#
|
||||
# Other paths will be handle by run() and return PE_ERROR
|
||||
#
|
||||
|
@ -86,22 +87,24 @@ sub init {
|
|||
# Manage RP requests
|
||||
$self->addRouteFromConf(
|
||||
'Unauth',
|
||||
oidcServiceMetaDataEndSessionURI => 'endSessionDone',
|
||||
oidcServiceMetaDataCheckSessionURI => 'checkSession',
|
||||
oidcServiceMetaDataTokenURI => 'token',
|
||||
oidcServiceMetaDataUserInfoURI => 'userInfo',
|
||||
oidcServiceMetaDataJWKSURI => 'jwks',
|
||||
oidcServiceMetaDataRegistrationURI => 'registration',
|
||||
oidcServiceMetaDataEndSessionURI => 'endSessionDone',
|
||||
oidcServiceMetaDataCheckSessionURI => 'checkSession',
|
||||
oidcServiceMetaDataTokenURI => 'token',
|
||||
oidcServiceMetaDataUserInfoURI => 'userInfo',
|
||||
oidcServiceMetaDataJWKSURI => 'jwks',
|
||||
oidcServiceMetaDataRegistrationURI => 'registration',
|
||||
oidcServiceMetaDataIntrospectionURI => 'introspection',
|
||||
);
|
||||
|
||||
# Manage user requests
|
||||
$self->addRouteFromConf(
|
||||
'Auth',
|
||||
oidcServiceMetaDataCheckSessionURI => 'checkSession',
|
||||
oidcServiceMetaDataTokenURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataUserInfoURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataJWKSURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataRegistrationURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataCheckSessionURI => 'checkSession',
|
||||
oidcServiceMetaDataTokenURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataUserInfoURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataJWKSURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataRegistrationURI => 'badAuthRequest',
|
||||
oidcServiceMetaDataIntrospectionURI => 'badAuthRequest',
|
||||
);
|
||||
|
||||
# Metadata (.well-known/openid-configuration)
|
||||
|
@ -997,50 +1000,13 @@ sub token {
|
|||
my ( $self, $req ) = @_;
|
||||
$self->logger->debug("URL detected as an OpenID Connect TOKEN URL");
|
||||
|
||||
# Check authentication
|
||||
my ( $client_id, $client_secret ) =
|
||||
$self->getEndPointAuthenticationCredentials($req);
|
||||
|
||||
unless ($client_id) {
|
||||
$self->logger->error(
|
||||
"No authentication provided to get token, or authentication type not supported"
|
||||
);
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
|
||||
# Verify that client_id is registered in configuration
|
||||
my $rp = $self->getRP($client_id);
|
||||
my $rp = $self->checkEndPointAuthenticationCredentials($req);
|
||||
|
||||
unless ($rp) {
|
||||
$self->userLogger->error(
|
||||
"No registered Relying Party found with client_id $client_id");
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
else {
|
||||
$self->logger->debug("Client id $client_id match Relying Party $rp");
|
||||
}
|
||||
|
||||
# Check client_secret
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsPublic} )
|
||||
{
|
||||
$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( $req, 'invalid_request', 400 );
|
||||
}
|
||||
unless ( $client_secret eq $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsClientSecret} )
|
||||
{
|
||||
$self->logger->error("Wrong credentials for $rp");
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
}
|
||||
my $client_id = $self->oidcRPList->{$rp}->{oidcRPMetaDataOptionsClientID};
|
||||
|
||||
# Get code session
|
||||
my $code = $req->param('code');
|
||||
|
@ -1190,7 +1156,7 @@ sub token {
|
|||
return $self->p->sendJSONresponse( $req, $token_response );
|
||||
}
|
||||
|
||||
# Handle uerinfo endpoint
|
||||
# Handle userinfo endpoint
|
||||
sub userInfo {
|
||||
my ( $self, $req ) = @_;
|
||||
$self->logger->debug("URL detected as an OpenID Connect USERINFO URL");
|
||||
|
@ -1247,6 +1213,66 @@ sub userInfo {
|
|||
}
|
||||
}
|
||||
|
||||
sub introspection {
|
||||
my ( $self, $req ) = @_;
|
||||
$self->logger->debug("URL detected as an OpenID Connect INTROSPECTION URL");
|
||||
|
||||
my $rp = $self->checkEndPointAuthenticationCredentials($req);
|
||||
|
||||
unless ($rp) {
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsPublic} )
|
||||
{
|
||||
$self->logger->error(
|
||||
"Public clients are not allowed to acces the introspection endpoint"
|
||||
);
|
||||
return $self->p->sendError( $req, 'unauthorized_client', 401 );
|
||||
}
|
||||
|
||||
my $token = $req->param('token');
|
||||
unless ($token) {
|
||||
return $self->p->sendError( $req, 'invalid_request', 400 );
|
||||
}
|
||||
|
||||
my $response = { active => JSON::false };
|
||||
my $oidcSession = $self->getOpenIDConnectSession($token);
|
||||
if ($oidcSession) {
|
||||
if ( my $user_session_id = $oidcSession->{data}->{user_session_id} ) {
|
||||
|
||||
# Get user identifier
|
||||
my $apacheSession = $self->p->getApacheSession($user_session_id);
|
||||
if ($apacheSession) {
|
||||
|
||||
$response->{active} = JSON::true;
|
||||
|
||||
# The ID attribute we choose is the one of the calling webservice,
|
||||
# which might be different from the OIDC client the token was issued to.
|
||||
my $user_id_attribute =
|
||||
$self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsUserIDAttr}
|
||||
|| $self->conf->{whatToTrace};
|
||||
$response->{sub} = $apacheSession->data->{$user_id_attribute};
|
||||
$response->{scope} = $oidcSession->{data}->{scope}
|
||||
if $oidcSession->{data}->{scope};
|
||||
$response->{client_id} =
|
||||
$self->oidcRPList->{ $oidcSession->{data}->{rp} }
|
||||
->{oidcRPMetaDataOptionsClientID}
|
||||
if $oidcSession->{data}->{rp};
|
||||
$response->{exp} =
|
||||
$oidcSession->{data}->{_utime} + $self->conf->{timeout};
|
||||
}
|
||||
}
|
||||
else {
|
||||
$self->logger->error(
|
||||
"Could not find user session ID in access token object");
|
||||
}
|
||||
}
|
||||
return $self->p->sendJSONresponse( $req, $response );
|
||||
}
|
||||
|
||||
# Handle jwks endpoint
|
||||
sub jwks {
|
||||
my ( $self, $req ) = @_;
|
||||
|
@ -1485,13 +1511,14 @@ sub logout {
|
|||
sub metadata {
|
||||
my ( $self, $req ) = @_;
|
||||
my $issuerDBOpenIDConnectPath = $self->conf->{issuerDBOpenIDConnectPath};
|
||||
my $authorize_uri = $self->conf->{oidcServiceMetaDataAuthorizeURI};
|
||||
my $token_uri = $self->conf->{oidcServiceMetaDataTokenURI};
|
||||
my $userinfo_uri = $self->conf->{oidcServiceMetaDataUserInfoURI};
|
||||
my $jwks_uri = $self->conf->{oidcServiceMetaDataJWKSURI};
|
||||
my $registration_uri = $self->conf->{oidcServiceMetaDataRegistrationURI};
|
||||
my $endsession_uri = $self->conf->{oidcServiceMetaDataEndSessionURI};
|
||||
my $checksession_uri = $self->conf->{oidcServiceMetaDataCheckSessionURI};
|
||||
my $authorize_uri = $self->conf->{oidcServiceMetaDataAuthorizeURI};
|
||||
my $token_uri = $self->conf->{oidcServiceMetaDataTokenURI};
|
||||
my $userinfo_uri = $self->conf->{oidcServiceMetaDataUserInfoURI};
|
||||
my $jwks_uri = $self->conf->{oidcServiceMetaDataJWKSURI};
|
||||
my $registration_uri = $self->conf->{oidcServiceMetaDataRegistrationURI};
|
||||
my $endsession_uri = $self->conf->{oidcServiceMetaDataEndSessionURI};
|
||||
my $checksession_uri = $self->conf->{oidcServiceMetaDataCheckSessionURI};
|
||||
my $introspection_uri = $self->conf->{oidcServiceMetaDataIntrospectionURI};
|
||||
|
||||
my $path = $self->path . '/';
|
||||
my $issuer = $self->iss;
|
||||
|
@ -1531,6 +1558,7 @@ sub metadata {
|
|||
authorization_endpoint => $baseUrl . $authorize_uri,
|
||||
end_session_endpoint => $baseUrl . $endsession_uri,
|
||||
check_session_iframe => $baseUrl . $checksession_uri,
|
||||
introspection_endpoint => $baseUrl . $introspection_uri,
|
||||
|
||||
# Logout capabilities
|
||||
backchannel_logout_supported => JSON::true,
|
||||
|
@ -1551,6 +1579,8 @@ sub metadata {
|
|||
subject_types_supported => ["public"],
|
||||
token_endpoint_auth_methods_supported =>
|
||||
[qw/client_secret_post client_secret_basic/],
|
||||
introspection_endpoint_auth_methods_supported =>
|
||||
[qw/client_secret_post client_secret_basic/],
|
||||
claims_supported => [qw/sub iss auth_time acr/],
|
||||
request_parameter_supported => JSON::true,
|
||||
request_uri_parameter_supported => JSON::true,
|
||||
|
|
|
@ -728,7 +728,7 @@ sub getOpenIDConnectSession {
|
|||
return undef;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
if ( $id and $type ) {
|
||||
my $storedType = $oidcSession->{data}->{_type};
|
||||
|
||||
# Only check if a type is set in DB, for backward compatibility
|
||||
|
@ -1111,6 +1111,56 @@ sub returnBearerError {
|
|||
];
|
||||
}
|
||||
|
||||
sub checkEndPointAuthenticationCredentials {
|
||||
my ( $self, $req ) = @_;
|
||||
|
||||
# Check authentication
|
||||
my ( $client_id, $client_secret ) =
|
||||
$self->getEndPointAuthenticationCredentials($req);
|
||||
|
||||
unless ($client_id) {
|
||||
$self->logger->error(
|
||||
"No authentication provided to get token, or authentication type not supported"
|
||||
);
|
||||
return undef;
|
||||
}
|
||||
|
||||
# Verify that client_id is registered in configuration
|
||||
my $rp = $self->getRP($client_id);
|
||||
|
||||
unless ($rp) {
|
||||
$self->userLogger->error(
|
||||
"No registered Relying Party found with client_id $client_id");
|
||||
return undef;
|
||||
}
|
||||
else {
|
||||
$self->logger->debug("Client id $client_id match Relying Party $rp");
|
||||
}
|
||||
|
||||
# Check client_secret
|
||||
if ( $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsPublic} )
|
||||
{
|
||||
$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 undef;
|
||||
}
|
||||
unless ( $client_secret eq $self->conf->{oidcRPMetaDataOptions}->{$rp}
|
||||
->{oidcRPMetaDataOptionsClientSecret} )
|
||||
{
|
||||
$self->logger->error("Wrong credentials for $rp");
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
return $rp;
|
||||
}
|
||||
|
||||
# Get Client ID and Client Secret
|
||||
# @return array (client_id, client_secret)
|
||||
sub getEndPointAuthenticationCredentials {
|
||||
|
|
|
@ -189,6 +189,7 @@ sub display {
|
|||
elsif ( $req->{error} == PE_REDIRECT ) {
|
||||
$skinfile = "redirect";
|
||||
%templateParams = (
|
||||
MAIN_LOGO => $self->conf->{portalMainLogo},
|
||||
URL => $req->{urldc},
|
||||
HIDDEN_INPUTS => $self->buildHiddenForm($req),
|
||||
FORM_METHOD => $req->data->{redirectFormMethod} || 'get',
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
html,body{height:100%;background:radial-gradient(circle at 50% 0,#fff 0,#ddd 100%) no-repeat scroll 0 0 #ddd}#wrap{min-height:100%;height:auto;margin:0 auto -80px;padding:20px 0 80px}#footer{height:80px;background-color:#fff;background-color:rgba(255,255,255,0.9);text-align:center;padding-top:10px;overflow:hidden}#header img{background-color:#fff;background-color:rgba(255,255,255,0.8);margin-bottom:20px}.card,.navbar-light{background-color:#fff;background-color:rgba(255,255,255,0.9);background-image:none}.login,.password{text-align:center;padding:20px}div.form{margin:0 auto;max-width:330px}div.actions{margin:10px 0 0 0}div.actions a{margin-top:10px}.buttons{text-align:center;margin:10px 0 0 0;cursor:pointer}.btn{white-space:normal}.btn span.fa{padding-right:8px}li.ui-state-active{background-color:#fafafa;background-color:rgba(250,250,250,0.9)}#appslist,#password,#loginHistory,#logout,#oidcConsents{margin-top:20px}div.category{margin:10px 0;cursor:grab}div.application{margin:5px 0;overflow:hidden}div.application a,div.application a:hover{text-decoration:none}p.notifCheck label{margin-left:5px;margin-top:3px;display:inline-block}img.langicon{cursor:pointer}button.idploop{max-width:300px}button.idploop img{max-height:30px}div.oidc_consent_message>ul{text-align:left;list-style:circle}@media(min-width:768px){div.application{height:80px}div.application h4.appname{margin:0}#wrap{margin:0 auto -60px}#footer{height:60px}}.hiddenFrame{border:0;display:hidden;margin:0}.noborder{border:0}.max{width:100%}.link{cursor:pointer}.nodecor:hover,.nodecor:active.nodecor:focus{text-decoration:none}.fa.icon-blue{color:blue}.progress-bar-animated{width:100%}
|
212
lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
Normal file
212
lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
Normal file
|
@ -0,0 +1,212 @@
|
|||
use lib 'inc';
|
||||
use Test::More;
|
||||
use strict;
|
||||
use IO::String;
|
||||
use LWP::UserAgent;
|
||||
use LWP::Protocol::PSGI;
|
||||
use MIME::Base64;
|
||||
use JSON;
|
||||
|
||||
BEGIN {
|
||||
require 't/test-lib.pm';
|
||||
}
|
||||
|
||||
my $debug = 'error';
|
||||
|
||||
# Initialization
|
||||
my $op = LLNG::Manager::Test->new( {
|
||||
ini => {
|
||||
logLevel => $debug,
|
||||
domain => 'idp.com',
|
||||
portal => 'http://auth.op.com',
|
||||
authentication => 'Demo',
|
||||
userDB => 'Same',
|
||||
issuerDBOpenIDConnectActivation => 1,
|
||||
issuerDBOpenIDConnectRule => '$uid eq "french"',
|
||||
oidcRPMetaDataExportedVars => {
|
||||
rp => {
|
||||
email => "mail",
|
||||
family_name => "cn",
|
||||
name => "cn"
|
||||
},
|
||||
rp2 => {
|
||||
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",
|
||||
oidcRPMetaDataOptionsClientSecret => "rpsecret",
|
||||
oidcRPMetaDataOptionsUserIDAttr => "",
|
||||
oidcRPMetaDataOptionsAccessTokenExpiration => 1,
|
||||
oidcRPMetaDataOptionsBypassConsent => 1,
|
||||
},
|
||||
oauth => {
|
||||
oidcRPMetaDataOptionsDisplayName => "oauth",
|
||||
oidcRPMetaDataOptionsClientID => "oauth",
|
||||
oidcRPMetaDataOptionsClientSecret => "service",
|
||||
oidcRPMetaDataOptionsUserIDAttr => "",
|
||||
}
|
||||
},
|
||||
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-----
|
||||
",
|
||||
}
|
||||
}
|
||||
);
|
||||
my $res;
|
||||
|
||||
# Authenticate to LLNG
|
||||
my $url = "/";
|
||||
my $query = "user=french&password=french";
|
||||
ok(
|
||||
$res = $op->_post(
|
||||
"/",
|
||||
IO::String->new($query),
|
||||
accept => 'text/html',
|
||||
length => length($query),
|
||||
),
|
||||
"Post authentication"
|
||||
);
|
||||
my $idpId = expectCookie($res);
|
||||
|
||||
# Get code for RP1
|
||||
my $query =
|
||||
"response_type=code&scope=openid%20profile%20email&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp2.com%2F";
|
||||
ok(
|
||||
$res = $op->_get(
|
||||
"/oauth2/authorize",
|
||||
query => "$query",
|
||||
accept => 'text/html',
|
||||
cookie => "lemonldap=$idpId",
|
||||
),
|
||||
"Get authorization code"
|
||||
);
|
||||
|
||||
my ($code) = expectRedirection( $res, qr#http://rp2\.com/.*code=([^\&]*)# );
|
||||
|
||||
# Exchange code for AT
|
||||
$query =
|
||||
"grant_type=authorization_code&code=$code&redirect_uri=http%3A%2F%2Frp2.com%2F";
|
||||
|
||||
ok(
|
||||
$res = $op->_post(
|
||||
"/oauth2/token",
|
||||
IO::String->new($query),
|
||||
accept => 'text/html',
|
||||
length => length($query),
|
||||
custom => {
|
||||
HTTP_AUTHORIZATION => "Basic " . encode_base64("rpid:rpsecret"),
|
||||
},
|
||||
),
|
||||
"Post token"
|
||||
);
|
||||
my $json = from_json( $res->[2]->[0] );
|
||||
my $token = $json->{access_token};
|
||||
ok( $token, 'Access token present' );
|
||||
|
||||
my $query = "token=$token";
|
||||
ok(
|
||||
$res = $op->_post(
|
||||
"/oauth2/introspect",
|
||||
IO::String->new($query),
|
||||
accept => 'text/html',
|
||||
length => length $query,
|
||||
custom => {
|
||||
HTTP_AUTHORIZATION => "Basic " . encode_base64("oauth:service"),
|
||||
},
|
||||
),
|
||||
"Post introspection"
|
||||
);
|
||||
|
||||
expectOK($res);
|
||||
my $json = from_json( $res->[2]->[0] );
|
||||
ok( $json->{active}, "Token is valid" );
|
||||
is( $json->{sub}, "french", "Response contains the correct sub" );
|
||||
|
||||
# Check status after expiration
|
||||
sleep(2);
|
||||
|
||||
$query = "token=$token";
|
||||
ok(
|
||||
$res = $op->_post(
|
||||
"/oauth2/introspect",
|
||||
IO::String->new($query),
|
||||
accept => 'text/html',
|
||||
length => length $query,
|
||||
custom => {
|
||||
HTTP_AUTHORIZATION => "Basic " . encode_base64("oauth:service"),
|
||||
},
|
||||
),
|
||||
"Post introspection"
|
||||
);
|
||||
|
||||
expectOK($res);
|
||||
$json = from_json( $res->[2]->[0] );
|
||||
ok( !$json->{active}, "Token is no longer valid" );
|
||||
|
||||
clean_sessions();
|
||||
done_testing();
|
||||
|
|
@ -26,7 +26,7 @@ SKIP: {
|
|||
|
||||
$client = iniCmb('[Dm] and [DB]');
|
||||
expectCookie( try('rtyler') );
|
||||
expectReject( try('dwho'), 5 );
|
||||
expectReject( try('dwho'), 401, 5 );
|
||||
|
||||
$client = iniCmb('if($env->{HTTP_X} eq "dwho") then [Dm] else [DB]');
|
||||
expectCookie( try('dwho') );
|
||||
|
@ -37,7 +37,7 @@ SKIP: {
|
|||
);
|
||||
expectCookie( try('rtyler') );
|
||||
expectCookie( try('dvador') );
|
||||
expectReject( try('dwho'), 5 );
|
||||
expectReject( try('dwho'), 401, 5 );
|
||||
}
|
||||
count($maintests);
|
||||
clean_sessions();
|
||||
|
|
|
@ -344,14 +344,14 @@ Note that it works only for Ajax request (see below).
|
|||
=cut
|
||||
|
||||
sub expectReject {
|
||||
my ( $res, $code ) = @_;
|
||||
ok( $res->[0] == 401, ' Response is 401' ) or explain( $res->[0], 401 );
|
||||
my ( $res, $status, $code ) = @_;
|
||||
$status ||= 401;
|
||||
cmp_ok( $res->[0], '==', $status, " Response status is $status" );
|
||||
eval { $res = JSON::from_json( $res->[2]->[0] ) };
|
||||
ok( not($@), 'Content is JSON' )
|
||||
ok( not($@), ' Content is JSON' )
|
||||
or explain( $res->[2]->[0], 'JSON content' );
|
||||
if ( defined $code ) {
|
||||
ok( $res->{error} == $code, "Error code is $code" )
|
||||
or explain( $res->{error}, $code );
|
||||
is( $res->{error}, $code, " Error code is $code" );
|
||||
}
|
||||
else {
|
||||
pass("Error code is $res->{error}");
|
||||
|
@ -696,9 +696,8 @@ sub _get {
|
|||
$args{remote_user} ? ( 'REMOTE_USER' => $args{remote_user} )
|
||||
: ()
|
||||
),
|
||||
'REQUEST_METHOD' => $args{method}
|
||||
|| 'GET',
|
||||
'REQUEST_URI' => $path . ( $args{query} ? "?$args{query}" : '' ),
|
||||
'REQUEST_METHOD' => $args{method} || 'GET',
|
||||
'REQUEST_URI' => $path . ( $args{query} ? "?$args{query}" : '' ),
|
||||
( $args{query} ? ( QUERY_STRING => $args{query} ) : () ),
|
||||
'SCRIPT_NAME' => '',
|
||||
'SERVER_NAME' => 'auth.example.com',
|
||||
|
|
Loading…
Reference in New Issue
Block a user