I’m trying to configure the OpenID Connect extension to enable single sign-on, but I’m having problems with the interactions between the Redirect Base URI setting and the Global Proxy Base URL.
For our configuration I have configured the following for the Proxy Base URL in the Global settings:
https://${X-Forwarded-Host}/${X-Forwarded-Path}/
The use of the X-Forwarded-* headers is required because of how user requests are handled depending on if they are internal or external users. This all works as expected when accessing GeoServer via the Apache reverse proxies.
However, I am having problems when trying to configure the OpenID Connect authentication extension. When setting up the extension I am configuring the following Proxy Base URI to something like:
https://geoserver.example.com/geoserver/
When saving the configuration it seems to apply fine - I can see correct values in the <datadir>/security/filter/keycloak/config.xml file. However when viewing the extension configuration again via the web interface the Redirect Base URI is replaced with the following:
https://${X-Forwarded-Host}/${X-Forwarded-Path}/
even though the contents of the config.xml file is correct. Trying to save this “changed” value with the header variables results is an error (“org.geoserver.security.oauth2.config.OAuth2FilterConfigException: OAUTH2_REDIRECT_URI_MALFORMED”).
In addition to this I get a 500 Internal Server Error when trying to click on the OpenID Connect button to log in (ie. https://geoserver.example.com/geoserver/web/oauth2/authorization/keycloak__oidc) and the following error shows up in the GeoServer logs:
18 Sept 16:10:22 ERROR [web.OAuth2AuthorizationRequestRedirectFilter] - Authorization Request failed: java.lang.IllegalArgumentException: Map has no value for 'X-Forwarded-Host'
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter$OAuth2AuthorizationRequestException: Map has no value for 'X-Forwarded-Host'
at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:190)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:71)
at org.geoserver.security.filter.GeoServerCompositeFilter.doFilter(GeoServerCompositeFilter.java:88)
at org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter.doFilter(GeoServerOAuth2LoginAuthenticationFilter.java:78)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:385)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:67)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:239)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:71)
at org.geoserver.security.filter.GeoServerCompositeFilter.doFilter(GeoServerCompositeFilter.java:88)
at org.geoserver.security.filter.GeoServerUserNamePasswordAuthenticationFilter.doFilter(GeoServerUserNamePasswordAuthenticationFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:385)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:67)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:115)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:106)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:71)
at org.geoserver.security.filter.GeoServerCompositeFilter.doFilter(GeoServerCompositeFilter.java:88)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:385)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:67)
at org.geoserver.security.filter.GeoServerSecurityContextPersistenceFilter$1.doFilterInternal(GeoServerSecurityContextPersistenceFilter.java:66)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.geoserver.security.filter.GeoServerCompositeFilter$NestedFilterChain.doFilter(GeoServerCompositeFilter.java:71)
at org.geoserver.security.filter.GeoServerCompositeFilter.doFilter(GeoServerCompositeFilter.java:88)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:385)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:237)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:195)
at org.geoserver.security.GeoServerSecurityFilterChainProxy.doFilter(GeoServerSecurityFilterChainProxy.java:139)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:355)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:272)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.geoserver.filters.SecurityHeadersFilter.doFilter(SecurityHeadersFilter.java:116)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.geoserver.filters.GZIPFilter.doFilter(GZIPFilter.java:46)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.geoserver.filters.SessionDebugFilter.doFilter(SessionDebugFilter.java:47)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.geoserver.filters.FlushSafeFilter.doFilter(FlushSafeFilter.java:41)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:205)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:199)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.eclipse.jetty.ee11.servlet.FilterHolder.doFilter(FilterHolder.java:208)
at org.eclipse.jetty.ee11.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1647)
at org.eclipse.jetty.ee11.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1602)
at org.eclipse.jetty.ee11.servlet.ServletChannel.dispatch(ServletChannel.java:871)
at org.eclipse.jetty.ee11.servlet.ServletChannel.handle(ServletChannel.java:449)
at org.eclipse.jetty.ee11.servlet.ServletHandler.handle(ServletHandler.java:469)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:546)
at org.eclipse.jetty.ee11.servlet.SessionHandler.handle(SessionHandler.java:763)
at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1253)
at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:148)
at org.eclipse.jetty.server.Server.handle(Server.java:197)
at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:804)
at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:420)
at org.eclipse.jetty.server.internal.HttpConnection$FillableCallback.succeeded(HttpConnection.java:1790)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:54)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:492)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.epcRunTask(AdaptiveExecutionStrategy.java:428)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:401)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:255)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:204)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:317)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:1009)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1240)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1194)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.IllegalArgumentException: Map has no value for 'X-Forwarded-Host'
at org.springframework.web.util.UriComponents$MapTemplateVariables.getValue(UriComponents.java:336)
at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:253)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:430)
at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:54)
at org.springframework.web.util.UriComponents.expand(UriComponents.java:153)
at org.springframework.web.util.UriComponentsBuilder.buildAndExpand(UriComponentsBuilder.java:309)
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver.expandRedirectUri(DefaultOAuth2AuthorizationRequestResolver.java:271)
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver.resolve(DefaultOAuth2AuthorizationRequestResolver.java:174)
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver.resolve(DefaultOAuth2AuthorizationRequestResolver.java:129)
at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:183)
... 71 more
It seems that the Redirect Base URI is being overridden by Proxy Base URI, causing the OpenID Connect extension to throw an exception because it doesn’t know how to handle the X-Forwarded-* header substitutions.
Has anyone else hit this issue? or know of any work-arounds?
I have the exact same setup with another GeoServer instance that has a “hard-coded” Proxy Base URI and it works fine, so I’m pretty confident this isn’t an issue with the extension configuration.
This would seem to be a bug, and I would create an issue, but it seems that write access to the issue tracker is restricted