Package: release.debian.org Severity: normal Tags: bookworm User: release.debian....@packages.debian.org Usertags: pu X-Debbugs-Cc: lemonldap...@packages.debian.org, y...@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
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 <y...@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 <y...@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.bes...@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 <y...@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 <y...@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