Bug#1053219: bookworm-pu: package lemonldap-ng/2.16.1+ds-deb12u2
Yadd
yadd at debian.org
Fri Sep 29 14:37:25 BST 2023
Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian.org at packages.debian.org
Usertags: pu
X-Debbugs-Cc: lemonldap-ng at packages.debian.org, yadd at debian.org
Control: affects -1 + src:lemonldap-ng
[ Reason ]
Two new vulnerabilities have been dicovered and fixed in lemonldap-ng:
- an open redirection only when configuration is edited by hand and
doesn't follow OIDC specifications
- a server-side-request-forgery (CVE-2023-44469) in OIDC protocol:
A little-know feature of OIDC allows the OpenID Provider to fetch the
Authorization request parameters itself by indicating a request_uri
parameter. This feature is now restricted to a white list using this
patch
[ Impact ]
One low and one medium security issue.
[ Tests ]
Patches includes test updates
[ Risks ]
Outside of test changes, patches are not so big and the test coverage
provided by upstream is good, so risk is moderate.
[ Checklist ]
[X] *all* changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in (old)stable
[X] the issue is verified as fixed in unstable
[ Changes ]
- open redirection patch: just rejects requests with `redirect_uri` if
relying party configuration has no declared redirect URIs.
- SSRF patch:
* add new configuration parameter to list authorized "request_uris"
* change the algorithm that manage request_uri parameter
Cheers,
Xavier
-------------- next part --------------
diff --git a/debian/NEWS b/debian/NEWS
index b8955920b..5295a3cbb 100644
--- a/debian/NEWS
+++ b/debian/NEWS
@@ -1,3 +1,13 @@
+lemonldap-ng (2.16.1+ds-deb12u2) bullseye; urgency=medium
+
+ A little-know feature of OIDC allows the OpenID Provider to fetch the
+ Authorization request parameters itself by indicating a request_uri
+ parameter.
+ By default, this feature is now restricted to a white list. See
+ Relying-Party security option to fill this field.
+
+ -- Yadd <yadd at debian.org> Fri, 29 Sep 2023 17:15:03 +0400
+
lemonldap-ng (2.0.9+ds-1) unstable; urgency=medium
CVE-2020-24660
diff --git a/debian/changelog b/debian/changelog
index cd4c8a023..148164a94 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+lemonldap-ng (2.16.1+ds-deb12u2) bookworm; urgency=medium
+
+ * Fix open redirection when OIDC RP has no redirect uris
+ * Fix Server-Side-Request-Forgery issue in OIDC (CVE-2023-44469)
+
+ -- Yadd <yadd at debian.org> Fri, 29 Sep 2023 17:18:12 +0400
+
lemonldap-ng (2.16.1+ds-deb12u1) bookworm; urgency=medium
* Apply login control to auth-slave requests
diff --git a/debian/patches/SSRF-issue.patch b/debian/patches/SSRF-issue.patch
new file mode 100644
index 000000000..3c6ca8b51
--- /dev/null
+++ b/debian/patches/SSRF-issue.patch
@@ -0,0 +1,795 @@
+Description: fix SSRF vulnerability
+ Issue described here: https://security.lauritz-holtmann.de/post/sso-security-ssrf/
+Author: Maxime Besson <maxime.besson at worteks.com>
+Origin: upstream, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/383/diffs
+Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2998
+Forwarded: not-needed
+Applied-Upstream: 2.17.1, https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/merge_requests/383/diffs
+Reviewed-By: Yadd <yadd at debian.org>
+Last-Update: 2023-09-22
+
+--- a/doc/sources/admin/idpopenidconnect.rst
++++ b/doc/sources/admin/idpopenidconnect.rst
+@@ -247,6 +247,11 @@
+ This feature only works if you have configured a form-based authentication module.
+ - **Allow OAuth2.0 Client Credentials Grant** (since version ``2.0.11``): Allow the use of the
+ :ref:`Client Credentials Grant <client-credentials-grant>` by this client.
++ - **Allowed URLs for fetching Request Object**: (since version ``2.17.1``):
++ which URLs may be called by the portal to fetch the request object (see
++ `request_uri
++ <https://openid.net/specs/openid-connect-core-1_0.html#RequestUriParameter>`__
++ in OIDC specifications). These URLs may use wildcards (``https://app.example.com/*``).
+ - **Authentication level**: Required authentication level to access this application
+ - **Access rule**: Lets you specify a :doc:`Perl rule<rules_examples>` to restrict access to this client
+
+--- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm
++++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm
+@@ -4656,6 +4656,7 @@
+ oidcRPMetaDataOptionsComment => { type => 'longtext' },
+ oidcRPMetaDataOptionsOfflineSessionExpiration => { type => 'int' },
+ oidcRPMetaDataOptionsRedirectUris => { type => 'text', },
++ oidcRPMetaDataOptionsRequestUris => { type => 'text', },
+ oidcRPMetaDataOptionsExtraClaims => {
+ type => 'keyTextContainer',
+ keyTest => qr/^[\x21\x23-\x5B\x5D-\x7E]+$/,
+--- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm
++++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/CTrees.pm
+@@ -255,6 +255,7 @@
+ 'oidcRPMetaDataOptionsAllowOffline',
+ 'oidcRPMetaDataOptionsAllowPasswordGrant',
+ 'oidcRPMetaDataOptionsAllowClientCredentialsGrant',
++ 'oidcRPMetaDataOptionsRequestUris',
+ 'oidcRPMetaDataOptionsAuthnLevel',
+ 'oidcRPMetaDataOptionsRule',
+ ]
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/ar.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/ar.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Public client",
+ "oidcRPMetaDataOptionsRedirectUris":"?????? ????? ??????? ??????? ??? ?????? ??????",
+ "oidcRPMetaDataOptionsRefreshToken":"Use refresh tokens",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsRule":"????? ??????",
+ "oidcRPMetaDataOptionsScopes":"????",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/en.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/en.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Public client",
+ "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
+ "oidcRPMetaDataOptionsRefreshToken":"Use refresh tokens",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Require PKCE",
+ "oidcRPMetaDataOptionsRule":"Access rule",
+ "oidcRPMetaDataOptionsScopes":"Scope",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/es.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/es.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Cliente p?blico",
+ "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
+ "oidcRPMetaDataOptionsRefreshToken":"Use refresh tokens",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Se requiere PKCE",
+ "oidcRPMetaDataOptionsRule":"Regla de acceso",
+ "oidcRPMetaDataOptionsScopes":"?mbito",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/fr.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/fr.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Client public",
+ "oidcRPMetaDataOptionsRedirectUris":"Adresses de redirection autoris?es pour la connexion",
+ "oidcRPMetaDataOptionsRefreshToken":"Utiliser les jetons de renouvellement",
++"oidcRPMetaDataOptionsRequestUris":"URLs autoris?es pour r?cup?rer les param?tres de la requ?te",
+ "oidcRPMetaDataOptionsRequirePKCE":"PKCE requis",
+ "oidcRPMetaDataOptionsRule":"R?gle d'acc?s",
+ "oidcRPMetaDataOptionsScopes":"Scope",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/he.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/he.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"???? ??????",
+ "oidcRPMetaDataOptionsRedirectUris":"Allowed redirection addresses for login",
+ "oidcRPMetaDataOptionsRefreshToken":"?????? ???????? ?????",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"????? PKCE",
+ "oidcRPMetaDataOptionsRule":"??? ????",
+ "oidcRPMetaDataOptionsScopes":"????",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/it.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/it.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Cliente pubblico",
+ "oidcRPMetaDataOptionsRedirectUris":"Indirizzi di reindirizzazione consentiti per l'accesso",
+ "oidcRPMetaDataOptionsRefreshToken":"Use refresh tokens",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Richiedi PKCE",
+ "oidcRPMetaDataOptionsRule":"Regola di accesso",
+ "oidcRPMetaDataOptionsScopes":"Scopo",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/pl.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/pl.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Klient publiczny",
+ "oidcRPMetaDataOptionsRedirectUris":"Dozwolone adresy przekierowa? dla logowania",
+ "oidcRPMetaDataOptionsRefreshToken":"U?yj token?w od?wie?aj?cych",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Wymagaj PKCE",
+ "oidcRPMetaDataOptionsRule":"Regu?a dost?pu",
+ "oidcRPMetaDataOptionsScopes":"Zakres",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/pt.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/pt.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Cliente p?blico",
+ "oidcRPMetaDataOptionsRedirectUris":"Endere?os de redirecionamento permitidos para o login",
+ "oidcRPMetaDataOptionsRefreshToken":"Usar tokens renovados",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Exigir PKCE",
+ "oidcRPMetaDataOptionsRule":"Regra de acesso",
+ "oidcRPMetaDataOptionsScopes":"Escopo",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/pt_BR.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/pt_BR.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Cliente p?blico",
+ "oidcRPMetaDataOptionsRedirectUris":"Endere?os de redirecionamento permitidos para o login",
+ "oidcRPMetaDataOptionsRefreshToken":"Usar tokens renovados",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Exigir PKCE",
+ "oidcRPMetaDataOptionsRule":"Regra de acesso",
+ "oidcRPMetaDataOptionsScopes":"Escopo",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/tr.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/tr.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"A??k istemci",
+ "oidcRPMetaDataOptionsRedirectUris":"Giri? i?in izin verilen y?nlendirme adresleri",
+ "oidcRPMetaDataOptionsRefreshToken":"Yeni jetonlar? kullan",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"PKCE gerektir",
+ "oidcRPMetaDataOptionsRule":"Eri?im kural?",
+ "oidcRPMetaDataOptionsScopes":"Kapsam",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/vi.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/vi.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"Kh?ch h?ng c?ng khai",
+ "oidcRPMetaDataOptionsRedirectUris":"C?c ??a ch? chuy?n h??ng ???c ph?p ?? ??ng nh?p",
+ "oidcRPMetaDataOptionsRefreshToken":"S? d?ng m? th?ng b?o l?m m?i",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"Y?u c?u PKCE",
+ "oidcRPMetaDataOptionsRule":"Quy t?c truy c?p",
+ "oidcRPMetaDataOptionsScopes":"Ph?m vi",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/zh.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/zh.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"?????",
+ "oidcRPMetaDataOptionsRedirectUris":"???????????",
+ "oidcRPMetaDataOptionsRefreshToken":"????????",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"?? PKCE",
+ "oidcRPMetaDataOptionsRule":"????",
+ "oidcRPMetaDataOptionsScopes":"??",
+--- a/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json
++++ b/lemonldap-ng-manager/site/htdocs/static/languages/zh_TW.json
+@@ -726,6 +726,7 @@
+ "oidcRPMetaDataOptionsPublic":"?????",
+ "oidcRPMetaDataOptionsRedirectUris":"???????????",
+ "oidcRPMetaDataOptionsRefreshToken":"????????",
++"oidcRPMetaDataOptionsRequestUris":"Allowed URLs for fetching Request Object",
+ "oidcRPMetaDataOptionsRequirePKCE":"?? PKCE",
+ "oidcRPMetaDataOptionsRule":"????",
+ "oidcRPMetaDataOptionsScopes":"??",
+--- a/lemonldap-ng-portal/MANIFEST
++++ b/lemonldap-ng-portal/MANIFEST
+@@ -656,6 +656,7 @@
+ t/32-OIDC-Password-Grant.t
+ t/32-OIDC-Refresh-Token.t
+ t/32-OIDC-Register.t
++t/32-OIDC-Request-Uri.t
+ t/32-OIDC-Response-Modes.t
+ t/32-OIDC-RP-rule.t
+ t/32-OIDC-Token-Exchange.t
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
+@@ -208,31 +208,97 @@
+ return PE_ERROR;
+ }
+
+- # Extract request_uri/request parameter
+- if ( $oidc_request->{'request_uri'} ) {
+- my $request =
+- $self->getRequestJWT( $oidc_request->{'request_uri'} );
++ # Client ID must be provided and cannot come from
++ # request or request_uri
++ unless ( $oidc_request->{'client_id'} ) {
++ $self->logger->error("Client ID is required");
++ return PE_ERROR;
++ }
++
++ # Check client_id
++ my $client_id = $oidc_request->{'client_id'};
++ $self->logger->debug("Request from client id $client_id");
++
++ # Verify that client_id is registered in configuration
++ my $rp = $self->getRP($client_id);
++
++ unless ($rp) {
++ $self->logger->error(
++ "No registered Relying Party found with"
++ . " client_id $client_id" );
++ return PE_UNKNOWNPARTNER;
++ }
++ else {
++ $self->logger->debug("Client id $client_id matches RP $rp");
++ }
++
++ # Scope must be provided and cannot come from request or request_uri
++ unless ( $oidc_request->{'scope'} ) {
++ $self->logger->error("Scope is required");
++ return PE_ERROR;
++ }
+
+- if ($request) {
+- $oidc_request->{'request'} = $request;
++ # Extract request_uri/request parameter
++ if ( my $request_uri = $oidc_request->{'request_uri'} ) {
++ if (
++ $self->isUriAllowedForRP(
++ $request_uri, $rp,
++ "oidcRPMetaDataOptionsRequestUris", 1
++ )
++ )
++ {
++ my $request = $self->getRequestJWT($request_uri);
++ if ($request) {
++ $oidc_request->{'request'} = $request;
++ }
++ else {
++ $self->logger->error(
++ "Error with Request URI resolution");
++ return PE_ERROR;
++ }
+ }
+ else {
+- $self->logger->error("Error with Request URI resolution");
++ $self->logger->error(
++ "Request URI $request_uri is not allowed for $rp");
+ return PE_ERROR;
+ }
+ }
+
+ if ( $oidc_request->{'request'} ) {
+- my $request = getJWTPayload( $oidc_request->{'request'} );
++ if (
++ $self->verifyJWTSignature(
++ $oidc_request->{'request'},
++ undef, $rp
++ )
++ )
++ {
++ $self->logger->debug("JWT signature request verified");
++ my $request = getJWTPayload( $oidc_request->{'request'} );
+
+- # Override OIDC parameters by request content
+- foreach ( keys %$request ) {
+- $self->logger->debug(
+-"Override $_ OIDC param by value present in request parameter"
+- );
+- $oidc_request->{$_} = $request->{$_};
+- $self->p->setHiddenFormValue( $req, $_, $request->{$_}, '',
+- 0 );
++ # Override OIDC parameters by request content
++ foreach ( keys %$request ) {
++ $self->logger->debug( "Override $_ OIDC param"
++ . " by value present in request parameter" );
++
++ if ( $_ eq "client_id" or $_ eq "response_type" ) {
++ if ( $oidc_request->{$_} ne $request->{$_} ) {
++ $self->logger->error( "$_ from request JWT ("
++ . $oidc_request->{$_}
++ . ") does not match $_ from request URI ("
++ . $request->{$_}
++ . ")" );
++ return PE_ERROR;
++ }
++ }
++ $oidc_request->{$_} = $request->{$_};
++ $self->p->setHiddenFormValue( $req, $_, $request->{$_},
++ '', 0 );
++ }
++ }
++ else {
++ $self->logger->error(
++ "JWT signature request can not be verified");
++ return PE_ERROR;
+ }
+ }
+
+@@ -241,37 +307,12 @@
+ $self->logger->error("Redirect URI is required");
+ return PE_ERROR;
+ }
+- unless ( $oidc_request->{'scope'} ) {
+- $self->logger->error("Scope is required");
+- return PE_ERROR;
+- }
+- unless ( $oidc_request->{'client_id'} ) {
+- $self->logger->error("Client ID is required");
+- return PE_ERROR;
+- }
+ if ( $flow eq "implicit" and not defined $oidc_request->{'nonce'} )
+ {
+ $self->logger->error("Nonce is required for implicit flow");
+ return PE_ERROR;
+ }
+
+- # Check client_id
+- my $client_id = $oidc_request->{'client_id'};
+- $self->logger->debug("Request from client id $client_id");
+-
+- # Verify that client_id is registered in configuration
+- my $rp = $self->getRP($client_id);
+-
+- unless ($rp) {
+- $self->logger->error(
+- "No registered Relying Party found with"
+- . " client_id $client_id" );
+- return PE_UNKNOWNPARTNER;
+- }
+- else {
+- $self->logger->debug("Client id $client_id matches RP $rp");
+- }
+-
+ # Check if this RP is authorized
+ if ( my $rule = $self->spRules->{$rp} ) {
+ my $ruleVariables =
+@@ -290,24 +331,14 @@
+
+ # Check redirect_uri
+ my $redirect_uri = $oidc_request->{'redirect_uri'};
+- my $redirect_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}
+- ->{oidcRPMetaDataOptionsRedirectUris};
+-
+- if ($redirect_uris) {
+- my $redirect_uri_allowed = 0;
+- foreach ( split( /\s+/, $redirect_uris ) ) {
+- $redirect_uri_allowed = 1 if $redirect_uri eq $_;
+- }
+- unless ($redirect_uri_allowed) {
+- $self->userLogger->error(
+- "Redirect URI $redirect_uri not allowed");
+- return PE_UNAUTHORIZEDURL;
+- }
+- }
+- elsif ($redirect_uri) {
+- $self->logger->error(
+-"RP $rp has no RedirectUris, unable to handle accept redirect_uri=$redirect_uri"
+- );
++ if (
++ !$self->isUriAllowedForRP(
++ $redirect_uri, $rp, 'oidcRPMetaDataOptionsRedirectUris'
++ )
++ )
++ {
++ $self->userLogger->error(
++ "Redirect URI $redirect_uri not allowed");
+ return PE_UNAUTHORIZEDURL;
+ }
+
+@@ -411,24 +442,6 @@
+ return PE_ERROR;
+ }
+
+- # Check Request JWT signature
+- if ( $oidc_request->{'request'} ) {
+- unless (
+- $self->verifyJWTSignature(
+- $oidc_request->{'request'},
+- undef, $rp
+- )
+- )
+- {
+- $self->logger->error(
+- "JWT signature request can not be verified");
+- return PE_ERROR;
+- }
+- else {
+- $self->logger->debug("JWT signature request verified");
+- }
+- }
+-
+ # Check id_token_hint
+ my $id_token_hint = $oidc_request->{'id_token_hint'};
+ if ($id_token_hint) {
+@@ -1067,26 +1080,13 @@
+
+ if ($post_logout_redirect_uri) {
+
+- # Check redirect URI is allowed
+- my $redirect_uri_allowed = 0;
+- foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
+- my $logout_rp = $_;
+- if ( my $redirect_uris =
+- $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
+- ->{oidcRPMetaDataOptionsPostLogoutRedirectUris} )
+- {
+- foreach ( split( /\s+/, $redirect_uris ) ) {
+- if ( $post_logout_redirect_uri eq $_ ) {
+- $self->logger->debug(
+-"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
+- );
+- $redirect_uri_allowed = 1;
+- }
+- }
+- }
+- }
+-
+- unless ($redirect_uri_allowed) {
++ unless (
++ $self->findRPFromUri(
++ $post_logout_redirect_uri,
++ 'oidcRPMetaDataOptionsPostLogoutRedirectUris'
++ )
++ )
++ {
+ $self->logger->error(
+ "$post_logout_redirect_uri is not allowed");
+ return PE_UNAUTHORIZEDURL;
+@@ -1118,6 +1118,43 @@
+ return PE_ERROR;
+ }
+
++sub findRPFromUri {
++ my ( $self, $uri, $option ) = @_;
++
++ my $found_rp;
++ foreach my $rp ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
++ $found_rp = $rp if $self->isUriAllowedForRP( $uri, $rp, $option );
++ }
++ return $found_rp;
++}
++
++sub isUriAllowedForRP {
++ my ( $self, $uri, $rp, $option, $wildcard_allowed ) = @_;
++ my $allowed_uris = $self->conf->{oidcRPMetaDataOptions}->{$rp}->{$option} // "";
++
++ my $is_uri_allowed;
++ if ($wildcard_allowed) {
++ $is_uri_allowed =
++ grep { _wildcard_match( $_, $uri ) } split( /\s+/, $allowed_uris );
++ }
++ else {
++ $is_uri_allowed = grep { $_ eq $uri } split( /\s+/, $allowed_uris );
++ }
++ return $is_uri_allowed;
++}
++
++sub _wildcard_match {
++ my ( $config_url, $candidate ) = @_;
++
++ # Quote everything
++ my $config_re = $config_url =~ s/(.)/\Q$1/gr;
++
++ # Replace \* by .*
++ $config_re =~ s/\\\*/.*/g;
++
++ return ( $candidate =~ qr/^$config_re$/ ? 1 : 0 );
++}
++
+ # Handle token endpoint
+ sub token {
+ my ( $self, $req ) = @_;
+@@ -2037,6 +2074,7 @@
+ my $userinfo_signed_response_alg =
+ $client_metadata->{userinfo_signed_response_alg};
+ my $redirect_uris = $client_metadata->{redirect_uris};
++ my $request_uris = $client_metadata->{request_uris};
+
+ # Register RP in global configuration
+ my $conf = $self->confAcc->getConf( { raw => 1, noCache => 1 } );
+@@ -2058,6 +2096,9 @@
+ = $id_token_signed_response_alg;
+ $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsRedirectUris}
+ = join( ' ', @$redirect_uris );
++ $conf->{oidcRPMetaDataOptions}->{$rp}->{oidcRPMetaDataOptionsRequestUris} =
++ join( ' ', @$request_uris )
++ if $request_uris and @$request_uris;
+ $conf->{oidcRPMetaDataOptions}->{$rp}
+ ->{oidcRPMetaDataOptionsUserInfoSignAlg} = $userinfo_signed_response_alg
+ if defined $userinfo_signed_response_alg;
+@@ -2095,6 +2136,8 @@
+ $registration_response->{'id_token_signed_response_alg'} =
+ $id_token_signed_response_alg;
+ $registration_response->{'redirect_uris'} = $redirect_uris;
++ $registration_response->{'request_uris'} = $request_uris
++ if $request_uris and @$request_uris;
+ $registration_response->{'userinfo_signed_response_alg'} =
+ $userinfo_signed_response_alg
+ if defined $userinfo_signed_response_alg;
+@@ -2121,25 +2164,13 @@
+
+ if ($post_logout_redirect_uri) {
+
+- # Check redirect URI is allowed
+- my $redirect_uri_allowed = 0;
+- foreach ( keys %{ $self->conf->{oidcRPMetaDataOptions} } ) {
+- my $logout_rp = $_;
+- my $redirect_uris =
+- $self->conf->{oidcRPMetaDataOptions}->{$logout_rp}
+- ->{oidcRPMetaDataOptionsPostLogoutRedirectUris};
+-
+- foreach ( split( /\s+/, $redirect_uris ) ) {
+- if ( $post_logout_redirect_uri eq $_ ) {
+- $self->logger->debug(
+-"$post_logout_redirect_uri is an allowed logout redirect URI for RP $logout_rp"
+- );
+- $redirect_uri_allowed = 1;
+- }
+- }
+- }
+-
+- unless ($redirect_uri_allowed) {
++ unless (
++ $self->findRPFromUri(
++ $post_logout_redirect_uri,
++ 'oidcRPMetaDataOptionsPostLogoutRedirectUris'
++ )
++ )
++ {
+ $self->logger->error("$post_logout_redirect_uri is not allowed");
+ return $self->p->login($req);
+ }
+@@ -2324,7 +2355,7 @@
+ claims_supported => [qw/sub iss auth_time acr/],
+ request_parameter_supported => JSON::true,
+ request_uri_parameter_supported => JSON::true,
+- require_request_uri_registration => JSON::false,
++ require_request_uri_registration => JSON::true,
+ response_modes_supported => [ "query", "fragment", "form_post", ],
+
+ # Algorithms
+@@ -2375,19 +2406,7 @@
+ }
+ }
+
+- # Extract request_uri/request parameter
+- my $request = $req->param('request');
+- if ( $req->param('request_uri') ) {
+- $request = $self->getRequestJWT( $req->param('request_uri') );
+- }
+-
+- if ($request) {
+- my $request_data = getJWTPayload($request);
+- foreach ( keys %$request_data ) {
+- $req->env->{ "llng_oidc_" . $_ } = $request_data->{$_};
+- }
+- }
+-
++ my $rp;
+ if ( $req->param('client_id') ) {
+ my $rp = $self->getRP( $req->param('client_id') );
+ $req->env->{"llng_oidc_rp"} = $rp if $rp;
+@@ -2399,6 +2418,27 @@
+ if $targetAuthnLevel;
+ }
+
++ # Extract request_uri/request parameter
++ my $request = $req->param('request');
++ if ( my $request_uri = $req->param('request_uri') ) {
++ if (
++ $rp
++ and $self->isUriAllowedForRP(
++ $request_uri, $rp, 'oidcRPMetaDataOptionsRequestUris', 1
++ )
++ )
++ {
++ $request = $self->getRequestJWT($request_uri);
++ }
++ }
++
++ if ($request) {
++ my $request_data = getJWTPayload($request);
++ foreach ( keys %$request_data ) {
++ $req->env->{ "llng_oidc_" . $_ } = $request_data->{$_};
++ }
++ }
++
+ return PE_OK;
+ }
+
+--- /dev/null
++++ b/lemonldap-ng-portal/t/32-OIDC-Request-Uri.t
+@@ -0,0 +1,200 @@
++use warnings;
++use lib 'inc';
++use Test::More;
++use strict;
++use IO::String;
++use LWP::UserAgent;
++use LWP::Protocol::PSGI;
++use Plack::Request;
++use Plack::Response;
++use MIME::Base64;
++
++# Initialization
++my ( $op, $res );
++ok( $op = op(), 'OP portal' );
++
++my $i = $op->p->loadedModules->{'Lemonldap::NG::Portal::Issuer::OpenIDConnect'};
++
++# Lazy load client
++#$i->getRP("rpid");
++
++our $call_allowed = 1;
++
++LWP::Protocol::PSGI->register(
++ sub {
++ my $req = Plack::Request->new(@_);
++ my $payload = {
++ client_id => "rpid",
++ redirect_uri => "http://redirect.uri/"
++ };
++
++ is( $req->uri->host, "request.uri", "only authorized URI is called" );
++ ok( $call_allowed, "Call is expected in this scenario" );
++
++ if ( $req->path_info eq "/baduri" ) {
++ $payload->{redirect_uri} = "http://invalid/";
++ }
++ if ( $req->path_info eq "/badclientid" ) {
++ $payload->{client_id} = "otherid";
++ }
++ my $res = Plack::Response->new(200);
++ $res->content_type('application/json');
++ $res->body( $i->createJWT( $payload, "HS256", "rp" ) );
++ return $res->finalize;
++ }
++);
++
++BEGIN {
++ require 't/test-lib.pm';
++ require 't/oidc-lib.pm';
++}
++
++my $debug = 'error';
++
++subtest "Successful request" => sub {
++ my $idpId = login( $op, "french" );
++ $res = authorize(
++ $op, $idpId,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ state => "xxyy",
++ request_uri => "http://request.uri/valid"
++ }
++ );
++ expectRedirection( $res, qr,http://redirect.uri/.*state=xxyy.*, );
++};
++
++subtest "Successful request, override of bad redirect_uri" => sub {
++ my $idpId = login( $op, "french" );
++ $res = authorize(
++ $op, $idpId,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ redirect_uri => "http://bad.uri/",
++ request_uri => "http://request.uri/valid"
++ }
++ );
++ expectRedirection( $res, qr,http://redirect.uri/.*, );
++};
++
++subtest "unauthorized Request URI" => sub {
++ my $idpId = login( $op, "french" );
++ local $call_allowed = 0;
++ $res = authorize(
++ $op, $idpId,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ request_uri => "http://bad.uri/"
++ }
++ );
++ expectPortalError( $res, 24 );
++};
++
++subtest "Allowed request URI, bad redirect URI" => sub {
++ my $idpId = login( $op, "french" );
++ $res = authorize(
++ $op, $idpId,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ request_uri => "http://request.uri/baduri"
++ }
++ );
++ expectPortalError( $res, 108 );
++};
++
++subtest "Allowed request URI, bad redirect URI override" => sub {
++ my $idpId = login( $op, "french" );
++ $res = authorize(
++ $op, $idpId,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ redirect_uri => "http://redirect.uri/",
++ request_uri => "http://request.uri/baduri"
++ }
++ );
++ expectPortalError( $res, 108 );
++};
++
++subtest "Undeclared request_uri is not called before auth" => sub {
++ local $call_allowed = 0;
++ $res = authorize(
++ $op, undef,
++ {
++ response_type => "code",
++ client_id => "rpid",
++ scope => "openid",
++ request_uri => "http://bad.uri/valid"
++ }
++ );
++
++ # LWP PSGI handler above will fail the test if a call is performed
++ ok(1);
++};
++
++clean_sessions();
++done_testing();
++
++sub op {
++ return LLNG::Manager::Test->new( {
++ ini => {
++ logLevel => $debug,
++ domain => 'idp.com',
++ portal => 'http://auth.op.com/',
++ authentication => 'Demo',
++ userDB => 'Same',
++ issuerDBOpenIDConnectActivation => 1,
++ oidcRPMetaDataExportedVars => {
++ rp => {
++ email => "mail",
++ family_name => "cn",
++ name => "cn"
++ }
++ },
++ oidcServiceAllowHybridFlow => 1,
++ oidcServiceAllowImplicitFlow => 1,
++ oidcServiceAllowAuthorizationCodeFlow => 1,
++ oidcRPMetaDataOptions => {
++ rp => {
++ oidcRPMetaDataOptionsDisplayName => "RP",
++ oidcRPMetaDataOptionsIDTokenExpiration => 3600,
++ oidcRPMetaDataOptionsClientID => "rpid",
++ oidcRPMetaDataOptionsClientSecret => "rpid",
++ oidcRPMetaDataOptionsIDTokenSignAlg => "RS256",
++ oidcRPMetaDataOptionsBypassConsent => 0,
++ oidcRPMetaDataOptionsUserIDAttr => "",
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsBypassConsent => 1,
++ oidcRPMetaDataOptionsRequestUris =>
++ "http://request.uri/*",
++ oidcRPMetaDataOptionsRedirectUris =>
++ "http://redirect.uri/",
++ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
++ "http://auth.rp.com/?logout=1"
++ }
++ },
++ oidcOPMetaDataOptions => {},
++ oidcOPMetaDataJSON => {},
++ oidcOPMetaDataJWKS => {},
++ oidcServiceMetaDataAuthnContext => {
++ 'loa-4' => 4,
++ 'loa-1' => 1,
++ 'loa-5' => 5,
++ 'loa-2' => 2,
++ 'loa-3' => 3
++ },
++ oidcServicePrivateKeySig => oidc_key_op_private_sig,
++ oidcServicePublicKeySig => oidc_cert_op_public_sig,
++ }
++ }
++ );
++}
diff --git a/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch b/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch
new file mode 100644
index 000000000..a5c0c60ab
--- /dev/null
+++ b/debian/patches/fix-open-redirection-without-OIDC-redirect-uris.patch
@@ -0,0 +1,729 @@
+Description: Fix open redirection when OIDC RP has no oidcRPMetaDataOptionsRedirectUris
+ This issue concerns only people that modify config by hand. The manager
+ refuses already a relying party without redirect URIs.
+Author: Yadd <yadd at debian.org>
+Origin: upstream, commit:c1de35ad
+Bug: https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/3003
+Forwarded: not-needed
+Applied-Upstream: v2.17.1, commit:c1de35ad
+Reviewed-By: <name and email of someone who approved/reviewed the patch>
+Last-Update: 2023-09-20
+
+--- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
++++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm
+@@ -19,6 +19,7 @@
+ PE_OIDC_SERVICE_NOT_ALLOWED
+ PE_FIRSTACCESS
+ PE_SENDRESPONSE
++ URIRE
+ );
+ use String::Random qw/random_string/;
+
+@@ -303,6 +304,12 @@
+ return PE_UNAUTHORIZEDURL;
+ }
+ }
++ elsif ($redirect_uri) {
++ $self->logger->error(
++"RP $rp has no RedirectUris, unable to handle accept redirect_uri=$redirect_uri"
++ );
++ return PE_UNAUTHORIZEDURL;
++ }
+
+ # Check if flow is allowed
+ if ( $flow eq "authorizationcode"
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-OP-logout.t
+@@ -222,6 +222,8 @@
+ 'http://auth.rp.com/oidc/logout',
+ oidcRPMetaDataOptionsLogoutType => 'front',
+ oidcRPMetaDataOptionsLogoutSessionRequired => 0,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-jwt-userinfo.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-jwt-userinfo.t
+@@ -333,7 +333,9 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsUserInfoSignAlg => "HS512",
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-public_client.t
+@@ -332,7 +332,9 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-authchoice.t
+@@ -286,7 +286,9 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-info.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-info.t
+@@ -336,7 +336,9 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-none-alg.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code-with-none-alg.t
+@@ -328,7 +328,9 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-authorization_code.t
+@@ -447,7 +447,9 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+ "http://auth.rp.com/?logout=1",
+- oidcRPMetaDataOptionsRule => '$uid eq "french"',
++ oidcRPMetaDataOptionsRule => '$uid eq "french"',
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-hybrid.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-hybrid.t
+@@ -248,7 +248,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit-no-token.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit-no-token.t
+@@ -231,7 +231,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit.t
++++ b/lemonldap-ng-portal/t/32-Auth-and-issuer-OIDC-implicit.t
+@@ -249,7 +249,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t
+@@ -356,7 +356,9 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsAuthnLevel => 5,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Code-Flow-with-2F.t
+@@ -356,7 +356,9 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com/?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Hooks.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Hooks.t
+@@ -51,6 +51,7 @@
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsRefreshToken => 1,
+ oidcRPMetaDataOptionsAllowOffline => 1,
++ oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+ },
+ oauth => {
+ oidcRPMetaDataOptionsDisplayName => "oauth",
+--- a/lemonldap-ng-portal/t/32-OIDC-Logout-from-RP-bypass-confirm.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Logout-from-RP-bypass-confirm.t
+@@ -254,6 +254,8 @@
+ oidcRPMetaDataOptionsLogoutBypassConfirm => 1,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+ "http://auth.rp.com?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Logout-redirect-uri-not-allowed.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Logout-redirect-uri-not-allowed.t
+@@ -240,6 +240,8 @@
+ oidcRPMetaDataOptionsLogoutBypassConfirm => 0,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+ "http://auth.rpother.com?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Macro.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Macro.t
+@@ -129,6 +129,7 @@
+ oidcRPMetaDataOptionsClientSecret => "rpid",
+ oidcRPMetaDataOptionsUserIDAttr => "custom_sub",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris => 'http://rp.com/',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/32-OIDC-Offline-Session.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Offline-Session.t
+@@ -201,7 +201,7 @@
+ oidcRPMetaDataOptionsIDTokenForceClaims => 1,
+ oidcRPMetaDataOptionsAdditionalAudiences =>
+ "http://my.extra.audience/test urn:extra2",
+-
++ oidcRPMetaDataOptionsRedirectUris => 'http://test/',
+ }
+ },
+ oidcServicePrivateKeySig => oidc_key_op_private_sig,
+--- a/lemonldap-ng-portal/t/32-OIDC-Refresh-Token.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Refresh-Token.t
+@@ -158,6 +158,7 @@
+ oidcRPMetaDataOptionsIDTokenForceClaims => 1,
+ oidcRPMetaDataOptionsAdditionalAudiences =>
+ "http://my.extra.audience/test urn:extra2",
++ oidcRPMetaDataOptionsRedirectUris => 'http://test/',
+ }
+ },
+ oidcServicePrivateKeySig => oidc_key_op_private_sig,
+--- a/lemonldap-ng-portal/t/32-OIDC-Token-Exchange.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Token-Exchange.t
+@@ -50,6 +50,7 @@
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsRefreshToken => 1,
+ oidcRPMetaDataOptionsIDTokenForceClaims => 1,
++ oidcRPMetaDataOptionsRedirectUris => 'http://test',
+ },
+ },
+ oidcRPMetaDataScopeRules => {
+--- a/lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Token-Introspection.t
+@@ -61,6 +61,7 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsBypassConsent => 1,
++ oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+ },
+ oauth => {
+ oidcRPMetaDataOptionsDisplayName => "oauth",
+--- a/lemonldap-ng-portal/t/32-OIDC-Token-Security.t
++++ b/lemonldap-ng-portal/t/32-OIDC-Token-Security.t
+@@ -51,6 +51,7 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsBypassConsent => 1,
++ oidcRPMetaDataOptionsRedirectUris => 'http://rp.com/',
+ },
+ rp2 => {
+ oidcRPMetaDataOptionsDisplayName => "RP2",
+@@ -61,7 +62,8 @@
+ oidcRPMetaDataOptionsUserIDAttr => "",
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsBypassConsent => 1,
+- oidcRPMetaDataOptionsRule => '$uid eq "dwho"',
++ oidcRPMetaDataOptionsRule => '$uid eq "dwho"',
++ oidcRPMetaDataOptionsRedirectUris => 'http://rp2.com/',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+@@ -98,7 +100,7 @@
+
+ # Try to get code for RP1 with invalide scope name
+ $query =
+-"response_type=code&scope=openid%20profile%20email%22&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp2.com%2F";
++"response_type=code&scope=openid%20profile%20email%22&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp.com%2F";
+ ok(
+ $res = $op->_get(
+ "/oauth2/authorize",
+@@ -113,7 +115,7 @@
+ #
+ # 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";
++"response_type=code&scope=openid%20profile%20email&client_id=rpid&state=af0ifjsldkj&redirect_uri=http%3A%2F%2Frp.com%2F";
+ ok(
+ $res = $op->_get(
+ "/oauth2/authorize",
+@@ -125,7 +127,7 @@
+ );
+ count(1);
+
+-my ($code) = expectRedirection( $res, qr#http://rp2\.com/.*code=([^\&]*)# );
++my ($code) = expectRedirection( $res, qr#http://rp\.com/.*code=([^\&]*)# );
+
+ # Play code on RP2
+ $query = buildForm(
+--- /dev/null
++++ b/lemonldap-ng-portal/t/32-OIDC-redirect_uri-filter.t
+@@ -0,0 +1,252 @@
++use warnings;
++use lib 'inc';
++use Test::More;
++use strict;
++use IO::String;
++use Lemonldap::NG::Common::FormEncode;
++use LWP::UserAgent;
++use LWP::Protocol::PSGI;
++use MIME::Base64;
++use URI::QueryParam;
++
++BEGIN {
++ require 't/test-lib.pm';
++ require 't/oidc-lib.pm';
++}
++
++my @badUrls = ( 'http://attacker_url.com/requesturi.jwt', );
++
++my $debug = 'error';
++my ( $op, $rp, $res );
++
++my $access_token;
++
++LWP::Protocol::PSGI->register(
++ sub {
++ my $req = Plack::Request->new(@_);
++ ok( $req->uri =~ m#http://auth.((?:o|r)p).com(.*)#, ' REST request' );
++ my $host = $1;
++ my $url = $2;
++ my ( $res, $client );
++ count(1);
++ if ( $host eq 'op' ) {
++ pass(" Request from RP to OP, endpoint $url");
++ $client = $op;
++ }
++ elsif ( $host eq 'rp' ) {
++ pass(' Request from OP to RP');
++ $client = $rp;
++ }
++ else {
++ fail(' Aborting REST request (external)');
++ return [ 500, [], [] ];
++ }
++ if ( $req->method =~ /^post$/i ) {
++ my $s = $req->content;
++ if ( $req->uri eq '/token/oauth2' ) {
++ is( $req->param("my_param"),
++ "my value", "oidcGenerateTokenRequest called" );
++ count(1);
++ }
++ 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);
++
++# Authentication
++switch ('op');
++my $query = "user=french&password=french";
++ok(
++ $res = $op->_post(
++ '/',,
++ IO::String->new($query),
++ accept => 'text/html',
++ length => length($query),
++ ),
++ "Post authentication"
++);
++count(1);
++my $idpId = expectCookie($res);
++
++# Query RP for auth
++switch ('rp');
++ok( $res = $rp->_get( '/', accept => 'text/html' ), 'Unauth SP request' );
++count(1);
++my $url;
++( $url, $query ) =
++ expectRedirection( $res, qr#http://auth.op.com(/oauth2/authorize)\?(.*)$# );
++
++# MAIN PART OF TEST
++switch ('op');
++foreach my $badUrl (@badUrls) {
++ my $badArg = build_urlencoded( redirect_uri => $badUrl );
++ my $forged = $query;
++ $forged =~ s#redirect_uri=(?:[^&]*)#$badArg#;
++
++ ok(
++ $res = $op->_get(
++ $url,
++ query => $forged,
++ accept => 'text/html',
++ cookie => "lemonldap=$idpId",
++ ),
++ "Push bad request to OP"
++ );
++ expectOK($res);
++ ok( $res->[2][0] =~ /trmsg="108"/, 'Get unauthorized redirect_uri' );
++ count(2);
++}
++
++clean_sessions();
++done_testing( count() );
++
++sub op {
++ return LLNG::Manager::Test->new( {
++ ini => {
++ logLevel => $debug,
++ domain => 'idp.com',
++ portal => 'http://auth.op.com/',
++ authentication => 'Demo',
++ userDB => 'Same',
++ issuerDBOpenIDConnectActivation => "1",
++ restSessionServer => 1,
++ restExportSecretKeys => 1,
++ oidcRPMetaDataExportedVars => {
++ rp => {
++ email => "mail",
++ family_name => "cn",
++ name => "cn"
++ }
++ },
++ oidcServiceAllowHybridFlow => 1,
++ oidcServiceAllowImplicitFlow => 1,
++ oidcServiceAllowAuthorizationCodeFlow => 1,
++ oidcRPMetaDataOptions => {
++ rp => {
++ oidcRPMetaDataOptionsDisplayName => "RP",
++ oidcRPMetaDataOptionsIDTokenExpiration => 3600,
++ oidcRPMetaDataOptionsClientID => "rpid",
++ oidcRPMetaDataOptionsIDTokenSignAlg => "HS512",
++ oidcRPMetaDataOptionsBypassConsent => 0,
++ oidcRPMetaDataOptionsClientSecret => "rpsecret",
++ oidcRPMetaDataOptionsRefreshToken => 1,
++ oidcRPMetaDataOptionsUserIDAttr => "",
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRule => '$uid eq "french"',
++ }
++ },
++ oidcOPMetaDataOptions => {},
++ oidcOPMetaDataJSON => {},
++ oidcOPMetaDataJWKS => {},
++ oidcServiceMetaDataAuthnContext => {
++ 'loa-4' => 4,
++ 'customacr-1' => 1,
++ 'loa-5' => 5,
++ 'loa-2' => 2,
++ 'loa-3' => 3
++ },
++ oidcServicePrivateKeySig => oidc_key_op_private_sig,
++ oidcServicePublicKeySig => oidc_cert_op_public_sig,
++ }
++ }
++ );
++}
++
++sub rp {
++ my ( $jwks, $metadata ) = @_;
++ return LLNG::Manager::Test->new( {
++ ini => {
++ logLevel => $debug,
++ domain => 'rp.com',
++ portal => 'http://auth.rp.com/',
++ authentication => 'OpenIDConnect',
++ userDB => 'Same',
++ restSessionServer => 1,
++ restExportSecretKeys => 1,
++ oidcOPMetaDataExportedVars => {
++ op => {
++ cn => "name",
++ uid => "sub",
++ sn => "family_name",
++ mail => "email"
++ }
++ },
++ oidcOPMetaDataOptions => {
++ op => {
++ oidcOPMetaDataOptionsCheckJWTSignature => 1,
++ oidcOPMetaDataOptionsJWKSTimeout => 0,
++ oidcOPMetaDataOptionsAcrValues => "loa-32 customacr-1",
++ oidcOPMetaDataOptionsClientSecret => "rpsecret",
++ oidcOPMetaDataOptionsScope => "openid profile email",
++ oidcOPMetaDataOptionsStoreIDToken => 0,
++ oidcOPMetaDataOptionsMaxAge => 30,
++ oidcOPMetaDataOptionsDisplay => "",
++ oidcOPMetaDataOptionsClientID => "rpid",
++ oidcOPMetaDataOptionsStoreIDToken => 1,
++ oidcOPMetaDataOptionsUseNonce => 1,
++ oidcOPMetaDataOptionsConfigurationURI =>
++ "https://auth.op.com/.well-known/openid-configuration"
++ }
++ },
++ oidcOPMetaDataJWKS => {
++ op => $jwks,
++ },
++ oidcOPMetaDataJSON => {
++ op => $metadata,
++ },
++ customPlugins => 't::OidcHookPlugin',
++ }
++ }
++ );
++}
+--- a/lemonldap-ng-portal/t/37-Issuer-Timeout.t
++++ b/lemonldap-ng-portal/t/37-Issuer-Timeout.t
+@@ -178,7 +178,9 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp.com/?logout=1"
++ "http://auth.rp.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://rp.example.com/',
+ },
+ rp2 => {
+ oidcRPMetaDataOptionsDisplayName => "RP",
+@@ -191,7 +193,9 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+- "http://auth.rp2.com/?logout=1"
++ "http://auth.rp2.com/?logout=1",
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://rp2.example.com/',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-IDP-Redirect.t
++++ b/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-IDP-Redirect.t
+@@ -335,7 +335,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 1,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-IDP-SOAP.t
++++ b/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-IDP-SOAP.t
+@@ -339,6 +339,8 @@
+ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
+ oidcRPMetaDataOptionsPostLogoutRedirectUris =>
+ 'http://auth.rp.com?logout=1',
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-SP.t
++++ b/lemonldap-ng-portal/t/37-Logout-from-OIDC-RP-to-SAML-SP.t
+@@ -350,7 +350,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET-with-WAYF.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET-with-WAYF.t
+@@ -373,7 +373,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-GET.t
+@@ -353,7 +353,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-POST.t
++++ b/lemonldap-ng-portal/t/37-OIDC-RP-to-SAML-IdP-POST.t
+@@ -355,7 +355,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.rp.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-SAML-SP-GET-to-OIDC-OP.t
++++ b/lemonldap-ng-portal/t/37-SAML-SP-GET-to-OIDC-OP.t
+@@ -289,7 +289,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.proxy.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+--- a/lemonldap-ng-portal/t/37-SAML-SP-POST-to-OIDC-OP.t
++++ b/lemonldap-ng-portal/t/37-SAML-SP-POST-to-OIDC-OP.t
+@@ -255,8 +255,7 @@
+ done_testing( count() );
+
+ sub op {
+- return LLNG::Manager::Test->new(
+- {
++ return LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => $debug,
+ domain => 'op.com',
+@@ -288,7 +287,9 @@
+ oidcRPMetaDataOptionsBypassConsent => 0,
+ oidcRPMetaDataOptionsClientSecret => "rpsecret",
+ oidcRPMetaDataOptionsUserIDAttr => "",
+- oidcRPMetaDataOptionsAccessTokenExpiration => 3600
++ oidcRPMetaDataOptionsAccessTokenExpiration => 3600,
++ oidcRPMetaDataOptionsRedirectUris =>
++ 'http://auth.proxy.com?openidconnectcallback=1',
+ }
+ },
+ oidcOPMetaDataOptions => {},
+@@ -310,8 +311,7 @@
+
+ sub proxy {
+ my ( $jwks, $metadata ) = @_;
+- return LLNG::Manager::Test->new(
+- {
++ return LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => $debug,
+ domain => 'proxy.com',
+@@ -382,8 +382,7 @@
+ }
+
+ sub sp {
+- return LLNG::Manager::Test->new(
+- {
++ return LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => $debug,
+ domain => 'sp.com',
diff --git a/debian/patches/series b/debian/patches/series
index 14369dfd8..e4acf948c 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -7,3 +7,5 @@ fix-OP-acr-parsing.patch
fix-viewer-endpoint.patch
apply-user-control-to-authslave.patch
fix-open-redirection.patch
+fix-open-redirection-without-OIDC-redirect-uris.patch
+SSRF-issue.patch
More information about the pkg-perl-maintainers
mailing list