Add pre-authenticated (trusted reverse-proxy) mode to the REST app - #4256
Add pre-authenticated (trusted reverse-proxy) mode to the REST app#4256arief-hidayat wants to merge 2 commits into
Conversation
The REST app's Spring Security chain only supports HTTP Basic, and its SecurityConfiguration is a plain @configuration with no way to switch that off. That makes flowable-app-rest impossible to run unchanged behind a reverse proxy that has already authenticated the caller and owns the Authorization header (oauth2-proxy, nginx auth_request, AWS ALB OIDC, Databricks Apps, ...): the proxy's bearer token collides with Flowable's Basic prompt and every request is rejected. Add a third flowable.rest.app.authentication-mode value, 'pre-auth'. In this mode the chain reads the already-authenticated user id from a request header (flowable.rest.app.pre-auth.principal-header, default X-Forwarded-User) via Spring Security's RequestHeaderAuthenticationFilter instead of HTTP Basic. No password is checked; privileges are still loaded from the IDM engine, so the access-rest-api / access-admin authorization rules behave exactly as in 'verify-privilege'. Identity flows to the engine unchanged, because SpringSecurityAuthenticationContext reads the principal name generically. This mirrors the pluggable security the UI apps already have via flowable.common.app.security.type. It is off by default and changes nothing for existing HTTP Basic deployments. Security note documented on the new classes: 'pre-auth' trusts the principal header, so it is only safe when the app is unreachable except through a proxy that strips any client-supplied copy of that header.
nikhiln64
left a comment
There was a problem hiding this comment.
The mode is cleanly built and I like that the trust assumption is stated plainly in the Javadoc, that this is only safe when the app cannot be reached except through a proxy that strips a client supplied copy of the header. The privilege loading mirrors BasicAuthenticationProvider so authorization behaves the same, and failing a missing header down to an anonymous request that the rules deny with 403 rather than a 500 is the right call.
What I would raise is that the whole security boundary rests on that one deployment assumption with no backstop inside the app. If the app ever becomes reachable off the proxy, or the proxy is misconfigured to pass an inbound X-Forwarded-User through, any caller sending X-Forwarded-User rest-admin is authenticated as rest-admin with full privileges, and the default header being an X-Forwarded name is exactly the family many proxies forward unless told to strip. Since this ships as a framework option enabled from a property, a defence in depth layer would cut the blast radius of a single misconfiguration. Spring Security's preauth stack lets you reject the request unless the peer address is a configured trusted proxy, through an AuthenticationDetailsSource that checks the remote address. An optional trusted proxy allowlist, off by default, would let a careful operator bind identity to the proxy it came from rather than to the header alone.
On the tests, they pin the header present and header absent behaviour well, but the case that decides whether this feature is safe, a request arriving with a spoofed principal header, is the same request the passing tests already make, so the suite cannot tell a trusted header from a forged one. If you add the IP guard, a test that a request carrying the header from an untrusted source address is rejected would pin the actual contract.
Defence in depth for the pre-auth authentication mode, addressing review feedback: the mode's safety otherwise rests entirely on the deployment assumption that the proxy strips client-supplied copies of the principal header, with no backstop in the app. If the app becomes reachable off the proxy, or the proxy forwards an inbound X-Forwarded-* header, any caller could assert an arbitrary identity. New optional property flowable.rest.app.pre-auth.trusted-proxies takes a list of IPs / CIDR ranges. When set, the principal header is only honoured if the request's transport peer address (ServletRequest#getRemoteAddr(), not an X-Forwarded-For value) matches the allowlist; otherwise the header is ignored and the request is denied exactly like a missing header. Empty by default, so existing deployments are unchanged. This binds the trusted identity to the proxy it came from rather than to the header alone, and complements — does not replace — the header-stripping requirement. Implemented by wrapping RequestHeaderAuthenticationFilter with an IpAddressMatcher check in getPreAuthenticatedPrincipal. Tests: FlowableRestApplicationPreAuthUntrustedProxyTest asserts a valid, privileged principal header from a source outside the allowlist is rejected with 403 — the case the earlier header-present/absent suite could not distinguish from a legitimate request. FlowableRestApplicationPreAuthTrustedProxyTest asserts a header from an allowlisted (loopback) source is honoured with 200.
|
Thanks — this is a fair call, and I've pushed a change ( New optional property Implementation is an On the tests: agreed the earlier suite couldn't tell a forged header from a trusted one. Added two cases — |
Add a pre-authenticated (trusted reverse-proxy) mode to the REST app
Problem
The REST app's Spring Security chain only supports HTTP Basic, and its SecurityConfiguration is a plain @configuration with no way to switch that off. That makes flowable-app-rest impossible to run unchanged behind a reverse proxy that has already authenticated the caller and owns the Authorization header (oauth2-proxy, nginx auth_request, AWS ALB OIDC, Databricks Apps, …): the proxy's bearer token collides with Flowable's Basic prompt and every request is rejected with 401.
Change
Add a third flowable.rest.app.authentication-mode value, pre-auth. In this mode the chain reads the already-authenticated user id from a request header (flowable.rest.app.pre-auth.principal-header, default X-Forwarded-User) via Spring Security's RequestHeaderAuthenticationFilter instead of HTTP Basic. No password is checked; privileges are still loaded from the IDM engine, so the access-rest-api / access-admin authorization rules behave exactly as in verify-privilege. Identity flows to the engine unchanged, because SpringSecurityAuthenticationContext reads the principal name generically.
This mirrors the pluggable security the UI apps already have via flowable.common.app.security.type. It is off by default and changes nothing for existing HTTP Basic deployments.
A request that arrives without the principal header is denied 403 (not 401): in pre-auth mode there is no Basic challenge to issue, so an identity-less request is an authorization failure rather than a prompt. A trusted proxy is expected to always set the header.
Security note
pre-auth trusts the principal header, so it is only safe when the app is unreachable except through a proxy that strips any client-supplied copy of that header. This is documented on the new classes.
Configuration example
flowable.rest.app.authentication-mode=pre-auth
flowable.rest.app.pre-auth.principal-header=X-Forwarded-User
How it was verified
header → 403, header for a user without access-rest-api → 403, admin actuator → 403.
Check List: