diff --git a/doc/sources/admin/hooks.rst b/doc/sources/admin/hooks.rst new file mode 100644 index 0000000000000000000000000000000000000000..c89f9ecc3ec1610ef735a511949aab0233bbd233 --- /dev/null +++ b/doc/sources/admin/hooks.rst @@ -0,0 +1,181 @@ +Available plugin hooks +====================== + +OpenID Connect Issuer hooks +--------------------------- + +oidcGotRequest +~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG received an authorization request on the `/oauth2/authorize` endpoint. + +The hook's parameter is a hash containing the authorization request parameters. + +Sample code:: + + use constant hook => { + oidcGotRequest => 'addScopeToRequest', + }; + + sub addScopeToRequest { + my ( $self, $req, $oidc_request ) = @_; + $oidc_request->{scope} = $oidc_request->{scope} . " my_hooked_scope"; + + return PE_OK; + } + + +oidcGenerateUserInfoResponse +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG is about to send a UserInfo response to a relying party on the `/oauth2/userinfo` endpoint. + +The hook's parameter is a hash containing all the claims that are about to be released. + +Sample code:: + + use constant hook => { + oidcGenerateUserInfoResponse => 'addClaimToUserInfo', + }; + + sub addClaimToUserInfo { + my ( $self, $req, $userinfo ) = @_; + $userinfo->{"userinfo_hook"} = 1; + return PE_OK; + } + +oidcGenerateIDToken +~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG is generating an ID Token. + +The hook's parameters are: + +* A hash of the claims to be contained in the ID Token +* the configuration key of the relying party which will receive the token + +Sample code:: + + use constant hook => { + oidcGenerateIDToken => 'addClaimToIDToken', + }; + + sub addClaimToIDToken { + my ( $self, $req, $payload, $rp ) = @_; + $payload->{"id_token_hook"} = 1; + return PE_OK; + } + +SAML Issuer hooks +----------------- + +samlGotAuthnRequest +~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG has received a SAML login request + +The hook's parameter is the Lasso::Login object + +Sample code:: + + use constant hook => { + samlGotAuthnRequest => 'gotRequest', + }; + + sub gotRequest { + my ( $self, $res, $login ) = @_; + + # Your code here + } + +samlBuildAuthnResponse +~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG is about to build a response to the SAML login request + +The hook's parameter is the Lasso::Login object + +Sample code:: + + use constant hook => { + samlBuildAuthnResponse => 'buildResponse', + }; + + sub buildResponse { + my ( $self, $res, $login ) = @_; + + # Your code here + } + +samlGotLogoutRequest +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG has received a SAML logout request + +The hook's parameter is the Lasso::Logout object + +Sample code:: + + use constant hook => { + samlGotLogoutRequest => 'gotLogout', + }; + + sub gotLogout { + my ( $self, $res, $logout ) = @_; + + # Your code here + } + +samlGotLogoutResponse +~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG has received a SAML logout response + +The hook's parameter is the Lasso::Logout object + +Sample code:: + + use constant hook => { + samlGotLogoutResponse => 'gotLogoutResponse', + }; + + sub gotLogoutResponse { + my ( $self, $res, $logout ) = @_; + + # Your code here + } + +samlBuildLogoutResponse +~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.0.10 + +This hook is triggered when LemonLDAP::NG is about to generate a SAML logout response + +The hook's parameter is the Lasso::Logout object + +Sample code:: + + use constant hook => { + samlBuildLogoutResponse => 'buildLogoutResponse', + }; + + sub buildLogoutResponse { + my ( $self, $res, $logout ) = @_; + + # Your code here + } diff --git a/doc/sources/admin/plugincustom.rst b/doc/sources/admin/plugincustom.rst index 4924eb45c0807c68e725c49a32549f761d8d7c4b..a7226254124a290627d4ba989391ed7e5f703d3c 100644 --- a/doc/sources/admin/plugincustom.rst +++ b/doc/sources/admin/plugincustom.rst @@ -4,6 +4,9 @@ Write a custom plugin Presentation ------------ +Standard entry points +~~~~~~~~~~~~~~~~~~~~~ + You can now write a custom portal plugin that will hook in the authentication process: @@ -18,6 +21,9 @@ authentication process: - ``forAuthUser``: method called for already authenticated users - ``beforeLogout``: method called before logout +Extended entry points +~~~~~~~~~~~~~~~~~~~~~ + If you need to call a method just after any standard method in authentication process, then use ``afterSub``, for example: @@ -48,6 +54,23 @@ authentication process, then use ``aroundSub``, for example: return $ret; } + +Hooks +~~~~~ + +.. versionadded:: 2.0.10 + +Your plugin can also register itself to be called at some points of interest +within the main LemonLDAP::NG code. + +.. toctree:: + :maxdepth: 1 + + hooks + +Routes +~~~~~~ + The plugin can also define new routes and call actions on them. See also ``Lemonldap::NG::Portal::Main::Plugin`` man page. diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm index 4f436c68dbdc0700d5dc7e63a118ad1b4bd0e30b..8165e79c72667ed08ceb15d34599603b1675dede 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm @@ -178,6 +178,10 @@ sub run { } } + my $h = + $self->p->processHook( $req, 'oidcGotRequest', $oidc_request ); + return PE_ERROR if ( $h != PE_OK ); + # Detect requested flow my $response_type = $oidc_request->{'response_type'}; my $flow = $self->getFlowType($response_type); @@ -1364,7 +1368,7 @@ sub _handleAuthorizationCodeGrant { } # Create ID Token - my $id_token = $self->createIDToken( $id_token_payload_hash, $rp ); + my $id_token = $self->createIDToken( $req, $id_token_payload_hash, $rp ); unless ($id_token) { $self->logger->error( @@ -1582,7 +1586,7 @@ sub _handleRefreshTokenGrant { } # Create ID Token - my $id_token = $self->createIDToken( $id_token_payload_hash, $rp ); + my $id_token = $self->createIDToken( $req, $id_token_payload_hash, $rp ); unless ($id_token) { $self->logger->error( @@ -2298,7 +2302,7 @@ sub _generateIDToken { } # Create ID Token - return $self->createIDToken( $id_token_payload_hash, $rp ); + return $self->createIDToken( $req, $id_token_payload_hash, $rp ); } sub _redirectToUrl { diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm index 3683e9c85f8c53045e82e0284809dd1b785185fd..db05279f3eff73beb8f1ea003359f4077023adcb 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/SAML.pm @@ -385,6 +385,10 @@ sub run { return PE_SAML_SSO_ERROR; } + my $h = + $self->p->processHook( $req, 'samlGotAuthnRequest', $login ); + return $h if ( $h != PE_OK ); + # Get SP entityID my $sp = $request ? $login->remote_providerID() : $idp_initiated_sp; @@ -847,6 +851,10 @@ sub run { "SAML authentication response sent to SAML SP $spConfKey for $user$nameIDLog" ); + $h = + $self->p->processHook( $req, 'samlBuildAuthnResponse', $login ); + return $h if ( $h != PE_OK ); + # Build SAML response $protocolProfile = $login->protocolProfile(); @@ -1124,6 +1132,12 @@ sub soapSloServer { $self->logger->debug("SLO: Logout request is valid"); + my $h = $self->p->processHook( $req, 'samlGotLogoutRequest', $logout ); + if ( $h != PE_OK ) { + return $self->p->sendError( $req, + "SLO: samlGotLogoutRequest hook returned error", 400 ); + } + # We accept only SOAP here unless ( $method eq $self->getHttpMethod('soap') ) { return $self->p->sendError( $req, @@ -1283,6 +1297,13 @@ sub soapSloServer { "SLO response signature according to metadata"); } + $h = + $self->p->processHook( $req, 'samlBuildLogoutResponse', $logout ); + if ( $h != PE_OK ) { + return $self->p->sendError( $req, + "SLO: samlBuildLogoutResponse hook returned error", 400 ); + } + # Send logout response unless ( $self->buildLogoutResponseMsg($logout) ) { $self->logger->error("Unable to build SLO response"); @@ -1614,8 +1635,8 @@ sub sloResume { $req->setInfo( $logoutContextSession->data->{info} ) if $logoutContextSession->data->{info}; - return $self->_finishSlo( $req, $logout, $method, $spConfKey, $provider_nb, - $relayID ); + return $self->_finishSlo( $req, $logout, $method, $spConfKey, + $provider_nb, $relayID ); } sub _finishSlo { @@ -1679,6 +1700,12 @@ sub sloServer { $self->logger->debug("SLO: Logout request is valid"); + my $h = $self->p->processHook( $req, 'samlGotLogoutRequest', $logout ); + if ( $h != PE_OK ) { + return $self->p->sendError( $req, + "SLO: samlGotLogoutRequest hook returned error", 400 ); + } + # Get SP entityID my $sp = $logout->remote_providerID(); $req->env->{llng_saml_sp} = $sp; @@ -1901,6 +1928,9 @@ sub sloServer { $self->logger->debug("Logout response is valid"); + my $h = $self->p->processHook( $req, 'samlGotLogoutResponse', $logout ); + $self->imgnok($req) if ( $h != PE_OK ); + # Check Destination $self->imgnok($req) unless ( $self->checkDestination( $logout->response, $url ) ); @@ -2018,8 +2048,8 @@ sub attributeServer { my $name_id = $query->nameIdentifier(); unless ($name_id) { - $self->p->sendError( $req, "Fail to get NameID from attribute request", - 400 ); + $self->p->sendError( $req, + "Fail to get NameID from attribute request", 400 ); } my $user = $name_id->content(); @@ -2077,8 +2107,8 @@ sub attributeServer { eval { @requested_attributes = $query->request()->Attribute(); }; if ($@) { $self->checkLassoError($@); - return $self->p->sendError( $req, "Unable to get requested attributes", - 400 ); + return $self->p->sendError( $req, + "Unable to get requested attributes", 400 ); } # Returned attributes diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm index 65208ea97ca96a615a0b403af6ca39e7c333bb40..98f451d97c152628537d082220db827098e2850e 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/OpenIDConnect.pm @@ -1394,6 +1394,10 @@ sub buildUserInfoResponse { } } + my $h = $self->p->processHook( $req, 'oidcGenerateUserInfoResponse', + $userinfo_response ); + return {} if ( $h != PE_OK ); + return $userinfo_response; } @@ -1577,13 +1581,16 @@ sub createJWT { # @param rp Internal Relying Party identifier # @return String id_token ID Token as JWT sub createIDToken { - my ( $self, $payload, $rp ) = @_; + my ( $self, $req, $payload, $rp ) = @_; # Get signature algorithm my $alg = $self->conf->{oidcRPMetaDataOptions}->{$rp} ->{oidcRPMetaDataOptionsIDTokenSignAlg}; $self->logger->debug("ID Token signature algorithm: $alg"); + my $h = $self->p->processHook( $req, 'oidcGenerateIDToken', $payload, $rp ); + return undef if ( $h != PE_OK ); + return $self->createJWT( $payload, $alg, $rp ); } diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm index 67571b77072c4dc2eabd56ba4b90a4a1bb2c6928..1329ed010a9e3ef915c072ee86fc1421352281de 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm @@ -74,6 +74,9 @@ BEGIN { has 'afterSub' => ( is => 'rw', default => sub { {} } ); has 'aroundSub' => ( is => 'rw', default => sub { {} } ); +# Issuer hooks +has 'hook' => ( is => 'rw', default => sub { {} } ); + has spRules => ( is => 'rw', default => sub { {} } @@ -493,6 +496,25 @@ sub findEP { } } } + if ( $obj->can('hook') ) { + $self->logger->debug("Found hook in $plugin"); + my $h = $obj->hook; + unless ( ref $h and ref($h) eq 'HASH' ) { + $self->logger->error('"hook" endpoint must be a hashref, skipped'); + } + else { + foreach my $hookname ( keys %$h ) { + my $callback = $h->{$hookname}; + push @{ $self->hook->{$hookname} }, sub { + eval { + $obj->logger->debug( + "Launching ${plugin}::$callback on hook $hookname"); + }; + $obj->$callback(@_); + }; + } + } + } $self->logger->debug("Plugin $plugin initializated"); # Rules for menu diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm index b20ad9ffe66ebc66a34ca25f942d239365b33087..fffddd769171b05b0b68bfb4f48e1421dabe81aa 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Plugin.pm @@ -320,6 +320,19 @@ method. Example: Do not launch "getUser" but use the given C<$sub>. This permits multiple plugins to use "aroundSub" in the same time. +=item C: hash ref that gives methods to call when a hook is triggered in the +LemonLDAP::NG code. Example: + + use constant hook => { + oidcGenerateIDToken => 'addClaimToIDToken' + }; + + sub addClaimToIDToken { + my ( $self, $req, $payload, $rp ) = @_; + $payload->{"id_token_hook"} = 1; + return PE_OK; + } + =back =head1 LOGGING diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm index 7745b707e4241e9e1f53a2219ce27446dd3e59cc..cbb30f3725af96bb6a3ace071cd6b771188ad7a2 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Process.pm @@ -44,6 +44,22 @@ sub process { return $err; } +sub processHook { + my ( $self, $req, $hookName, @args ) = @_; + + $self->logger->debug("Calling hook $hookName"); + my $err = PE_OK; + for my $sub ( @{ $self->hook->{$hookName} } ) { + if ( ref $sub eq 'CODE' ) { + last if ( $err = $sub->( $req, @args ) ); + } + else { + $self->logger->debug("Not a code ref: $sub"); + } + } + return $err; +} + sub _formatProcessResult { my ( $self, $err ) = @_; return ( ( $err > 0 ? "error" : "status" ) diff --git a/lemonldap-ng-portal/t/30-Auth-and-issuer-SAML-POST-Hook.t b/lemonldap-ng-portal/t/30-Auth-and-issuer-SAML-POST-Hook.t new file mode 100644 index 0000000000000000000000000000000000000000..3a30b57aaee466fc56a6f6fd50bc2da57e963162 --- /dev/null +++ b/lemonldap-ng-portal/t/30-Auth-and-issuer-SAML-POST-Hook.t @@ -0,0 +1,185 @@ +use lib 'inc'; +use Test::More; +use strict; +use IO::String; +use LWP::UserAgent; +use LWP::Protocol::PSGI; +use MIME::Base64; + +BEGIN { + require 't/test-lib.pm'; + require 't/saml-lib.pm'; +} + +my $maintests = 3; +my $debug = 'error'; +my ( $issuer, $sp, $res ); + +# Redefine LWP methods for tests +LWP::Protocol::PSGI->register( + sub { + my $req = Plack::Request->new(@_); + fail('POST should not launch SOAP requests'); + count(1); + return [ 500, [], [] ]; + } +); + +SKIP: { + eval "use Lasso"; + if ($@) { + skip 'Lasso not found', $maintests; + } + + # Initialization + $issuer = register( 'issuer', \&issuer ); + $sp = register( 'sp', \&sp ); + + my ( $url, $s, $pdata, $host ); + + # Simple SP access + ok( + $res = $sp->_get( + '/', accept => 'text/html', + ), + 'Unauth SP request' + ); + expectOK($res); + ( $host, $url, $s ) = + expectAutoPost( $res, 'auth.idp.com', '/saml/singleSignOn', + 'SAMLRequest' ); + + # Push SAML request to IdP + ok( + $res = $issuer->_post( + $url, + IO::String->new($s), + accept => 'text/html', + length => length($s) + ), + 'Post SAML request to IdP' + ); + expectOK($res); + $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' ); + + # Try to authenticate with an authorized user to IdP + $s = "user=french&password=french&$s"; + ok( + $res = $issuer->_post( + $url, + IO::String->new($s), + accept => 'text/html', + cookie => $pdata, + length => length($s), + ), + 'Post authentication' + ); + my $idpId = expectCookie($res); + + # Expect failure triggered by the hook + expectPortalError( $res, -999 ); +} + +count($maintests); +clean_sessions(); +done_testing( count() ); + +sub issuer { + return LLNG::Manager::Test->new( { + ini => { + logLevel => $debug, + domain => 'idp.com', + portal => 'http://auth.idp.com', + authentication => 'Demo', + userDB => 'Same', + issuerDBSAMLActivation => 1, + issuerDBSAMLRule => '$uid eq "french"', + samlSPMetaDataOptions => { + 'sp.com' => { + samlSPMetaDataOptionsEncryptionMode => 'none', + samlSPMetaDataOptionsSignSSOMessage => 1, + samlSPMetaDataOptionsSignSLOMessage => 1, + samlSPMetaDataOptionsCheckSSOMessageSignature => 1, + samlSPMetaDataOptionsCheckSLOMessageSignature => 1, + } + }, + samlSPMetaDataExportedAttributes => { + 'sp.com' => { + cn => +'1;cn;urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + uid => +'1;uid;urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + } + }, + samlOrganizationDisplayName => "IDP", + samlOrganizationName => "IDP", + samlOrganizationURL => "http://www.idp.com/", + samlServicePrivateKeyEnc => saml_key_idp_private_enc, + samlServicePrivateKeySig => saml_key_idp_private_sig, + samlServicePublicKeyEnc => saml_key_idp_public_enc, + samlServicePublicKeySig => saml_key_idp_public_sig, + samlSPMetaDataXML => { + "sp.com" => { + samlSPMetaDataXML => + samlSPMetaDataXML( 'sp', 'HTTP-POST' ) + }, + }, + customPlugins => 't::SamlHookPlugin', + } + } + ); +} + +sub sp { + return LLNG::Manager::Test->new( { + ini => { + logLevel => $debug, + domain => 'sp.com', + portal => 'http://auth.sp.com', + authentication => 'SAML', + userDB => 'Same', + issuerDBSAMLActivation => 0, + restSessionServer => 1, + samlIDPMetaDataExportedAttributes => { + idp => { + mail => "0;mail;;", + uid => "1;uid", + cn => "0;cn" + } + }, + samlIDPMetaDataOptions => { + idp => { + samlIDPMetaDataOptionsEncryptionMode => 'none', + samlIDPMetaDataOptionsSSOBinding => 'post', + samlIDPMetaDataOptionsSLOBinding => 'post', + samlIDPMetaDataOptionsSignSSOMessage => 1, + samlIDPMetaDataOptionsSignSLOMessage => 1, + samlIDPMetaDataOptionsCheckSSOMessageSignature => 1, + samlIDPMetaDataOptionsCheckSLOMessageSignature => 1, + samlIDPMetaDataOptionsForceUTF8 => 1, + } + }, + samlIDPMetaDataExportedAttributes => { + idp => { + "uid" => "0;uid;;", + "cn" => "1;cn;;", + }, + }, + samlIDPMetaDataXML => { + idp => { + samlIDPMetaDataXML => + samlIDPMetaDataXML( 'idp', 'HTTP-POST' ) + } + }, + samlOrganizationDisplayName => "SP", + samlOrganizationName => "SP", + samlOrganizationURL => "http://www.sp.com", + samlServicePublicKeySig => saml_key_sp_public_sig, + samlServicePrivateKeyEnc => saml_key_sp_private_enc, + samlServicePrivateKeySig => saml_key_sp_private_sig, + samlServicePublicKeyEnc => saml_key_sp_public_enc, + samlSPSSODescriptorAuthnRequestsSigned => 1, + }, + } + ); +} diff --git a/lemonldap-ng-portal/t/32-OIDC-Hooks.t b/lemonldap-ng-portal/t/32-OIDC-Hooks.t new file mode 100644 index 0000000000000000000000000000000000000000..ead6125c45f39bb0aa61ce4ad8760ffe041d7fff --- /dev/null +++ b/lemonldap-ng-portal/t/32-OIDC-Hooks.t @@ -0,0 +1,175 @@ +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'; + require 't/oidc-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" + } + }, + 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 => 3600, + 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 => oidc_key_op_private_sig, + oidcServicePublicKeySig => oidc_key_op_public_sig, + customPlugins => 't::OidcHookPlugin', + } + } +); +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 +$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 $id_token = $json->{id_token}; +ok( $id_token, 'ID token present' ); +my $id_token_payload = id_token_payload($id_token); +is ($id_token_payload->{id_token_hook}, 1, "Found hooked claim in ID token"); + +# Get userinfo +$res = $op->_post( + "/oauth2/userinfo", + IO::String->new(''), + accept => 'application/json', + length => 0, + custom => { + HTTP_AUTHORIZATION => "Bearer " . $token, + }, +); + +$json = expectJSON($res); +is ($json->{userinfo_hook}, 1, "Found hooked claim in Userinfo token"); + + +# Introspect to find scopes +$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] ); +like($json->{scope}, qr/\bmy_hooked_scope\b/, "Found hook defined scope"); + +clean_sessions(); +done_testing(); + diff --git a/lemonldap-ng-portal/t/OidcHookPlugin.pm b/lemonldap-ng-portal/t/OidcHookPlugin.pm new file mode 100644 index 0000000000000000000000000000000000000000..e0e830a66d89551c560f6cd1520bc0cfd0b6295d --- /dev/null +++ b/lemonldap-ng-portal/t/OidcHookPlugin.pm @@ -0,0 +1,41 @@ +package t::OidcHookPlugin; + +use Mouse; +extends 'Lemonldap::NG::Portal::Main::Plugin'; + +use Lemonldap::NG::Portal::Main::Constants qw(PE_OK); +use Data::Dumper; +use Test::More; + +use constant hook => { + oidcGenerateIDToken => 'addClaimToIDToken', + oidcGenerateUserInfoResponse => 'addClaimToUserInfo', + oidcGotRequest => 'addScopeToRequest', +}; + +sub init { + my ($self) = @_; + return 1; +} + +sub addClaimToIDToken { + my ( $self, $req, $payload, $rp ) = @_; + $payload->{"id_token_hook"} = 1; + return PE_OK; +} + +sub addClaimToUserInfo { + my ( $self, $req, $userinfo ) = @_; + $userinfo->{"userinfo_hook"} = 1; + return PE_OK; +} + +sub addScopeToRequest { + my ( $self, $req, $oidc_request ) = @_; + $oidc_request->{scope} = $oidc_request->{scope} . " my_hooked_scope"; + + return PE_OK; +} + +1; + diff --git a/lemonldap-ng-portal/t/SamlHookPlugin.pm b/lemonldap-ng-portal/t/SamlHookPlugin.pm new file mode 100644 index 0000000000000000000000000000000000000000..f46618217dcc7ef74b0fd031ed415ef618b49808 --- /dev/null +++ b/lemonldap-ng-portal/t/SamlHookPlugin.pm @@ -0,0 +1,21 @@ +package t::SamlHookPlugin; + +use Mouse; +extends 'Lemonldap::NG::Portal::Main::Plugin'; + +use constant hook => { samlGotAuthnRequest => 'gotRequest', }; + +sub init { + my ($self) = @_; + return 1; +} + +sub gotRequest { + my ( $self, $res, $login ) = @_; + + # Return a weird + return -999; +} + +1; +