This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Edge Proxy as the Relying Party

Running Stroom behind an authenticating reverse proxy, such as an AWS Application Load Balancer with Cognito, that completes the Open ID Connect flow itself.

Normally Stroom is its own Open ID Connect client, or Relying Party: it redirects the browser to the Identity Provider (IDP) Identity Provider (IDP) An Identity Provider is a system or service that can authenticate a user and assert their identity. Identity providers can support single sign on (SSO), which allows the user to sign in once to the Identity Provider so they are then authenticated to all systems using that IDP.Click to see more details..., exchanges the authorization code for tokens, and holds them in its session. That is the model described by Internal IDP and External IDP.

Some environments put an authenticating reverse proxy in front of Stroom instead. The proxy completes the OIDC flow before a request ever reaches Stroom, holds the tokens itself, and injects a verified credential into each request it forwards. Examples include:

  • An AWS Application Load Balancer with an authenticate-cognito or authenticate-oidc listener rule, which injects a signed x-amzn-oidc-data header.
  • NGINX with oauth2-proxy (or NGINX Plus’s native OIDC support, or lua-resty-openidc), which relays the IDP’s token as an Authorization: Bearer header.

This is common in cloud and government estates where a policy requires that unauthenticated traffic never reaches the application. Stroom supports it as a first class deployment model.

The One Rule: Exactly One Relying Party

For any given path, exactly one component runs the OIDC flow — the proxy, or Stroom, never both.

If Stroom is left in its normal configuration behind an authenticating proxy, both try to be the Relying Party. The browser is driven through a second, redundant OIDC flow stacked on the one the proxy already completed, which needs a second client registration at the IDP, doubles the cookies on every request, and typically fails with Stroom Loading, Authentication Error: Failed to Fetch.

Setting edgeAuthentication.enabled tells Stroom the proxy owns the flow. Stroom then:

  • Accepts the proxy’s injected credential, verified cryptographically on every request, as the user’s identity. No Stroom session is created; the identity is re-derived from the headers each time, which is also how the proxy’s own token refresh reaches Stroom.
  • Never starts an OIDC flow of its own, and disables its OIDC callback endpoint.
  • Treats the injected credential as needing Cross-Site Request Forgery (CSRF) protection on browser requests, because the browser attaches the proxy’s session cookie automatically, even to cross site requests.
  • Can end the proxy’s session on logout, not just its own.

Stroom Configuration

  security:
    authentication:
      edgeAuthentication:
        enabled: true
        logout:
          cookiesToExpire: [ "AWSELBAuthSessionCookie" ]
          signOutUrl: "https://MY_DOMAIN.auth.REGION.amazoncognito.com/logout?client_id=CLIENT_ID&logout_uri=POST_LOGOUT_URI"
      openId:
        identityProviderType: EXTERNAL_IDP
        # ... provider settings, see the worked examples ...

edgeAuthentication.enabled

Declares that the proxy is the Relying Party, with the effects described above. Requires identityProviderType: EXTERNAL_IDP; Stroom will refuse to start otherwise.

edgeAuthentication.logout.cookiesToExpire

Signing out of Stroom does not end the proxy’s session by itself; without help, the very next request would silently sign the user straight back in. This setting lists the proxy’s session cookie name prefixes, which Stroom expires when the user logs out.

They are prefixes because proxies shard large session cookies: an ALB’s AWSELBAuthSessionCookie arrives as AWSELBAuthSessionCookie-0, -1 and so on, and oauth2-proxy chunks _oauth2_proxy the same way.

edgeAuthentication.logout.signOutUrl

Where to send the browser after logging out of Stroom, normally the proxy’s or IDP’s own sign out endpoint, so the session ends everywhere. For Cognito this is the hosted UI’s /logout endpoint; for oauth2-proxy it is /oauth2/sign_out.

If it is not set, Stroom logs a warning at each logout: the proxy session survives, and the user may be signed straight back in.

csrf.protectBrowserOriginatedRequests

On by default, and independent of edgeAuthentication. It rejects a state changing request whose token arrived on a request the browser marked as cross site, unless the request carries the X-CSRF header. Browsers do not let a cross site page attach an Authorization header, so such a token can only have been injected by a proxy — this is the safety net for a proxy that nobody declared in the configuration.

Non browser clients are unaffected, as they send none of the browser fetch metadata this check relies on.

What the Proxy Must and Must Not Authenticate

Stroom is not only a web application; it ingests data, serves health checks and its nodes talk to each other. None of that traffic can complete an interactive sign in, so the proxy’s authenticate rule must cover the browser facing paths only.

Path Proxy rule Why
/, /stroom/*, /ui/* Authenticate The UI
/api/* Authenticate Browser API calls
/datafeed (and its legacy aliases) Bypass Data receipt from Stroom-Proxies and clients, authenticated by certificate, token or API key
/remoting/remotefeedservice.rpc Bypass Feed status RPC
/status Bypass Health checks
Admin port (/stroomAdmin) Bypass Should not be publicly exposed at all

Stroom still authenticates the bypassed paths itself — bypassing the proxy does not bypass Stroom’s own checks.

Node to node traffic inside a cluster does not go through the proxy and needs no special handling.

Trust Prerequisites

Stroom verifies the signature of whatever credential the proxy injects, so a forged header does not authenticate. Two things must still be true of the deployment, and Stroom cannot verify them from the inside:

  1. Stroom is unreachable except through the proxy — a security group, firewall rule or network policy allowing traffic to Stroom’s application port only from the proxy.
  2. The proxy overwrites the headers it injects, so a client cannot supply its own. The ALB does this for its x-amzn-oidc-* headers; with NGINX make sure proxy_set_header is used for the Authorization header, which overwrites, and nothing upstream re-adds it.

Request Header Sizes

Authenticating proxies make requests big. An ALB’s session cookie is sharded at 4KB per shard, and the injected token headers come on top, so an ordinary authenticated request can exceed the 8KB per request default that Jetty applies when nothing is configured. The failure looks like a network error, not an authentication error.

Set a larger limit on every Stroom node:

server:
  applicationConnectors:
    - type: http
      port: 8080
      useForwardedHeaders: true
      maxRequestHeaderSize: 32KiB

User Accounts and Permissions

Exactly as with any external IDP, the proxy establishes who the user is; Stroom still decides what they may do. A Stroom user record is created automatically the first time a verified identity is seen, with no permissions. Anyone the IDP will authenticate can therefore reach an empty Stroom UI, so if that is not wanted, restrict who can authenticate at the IDP or proxy (for example, limit the Cognito app client or the ALB rule to a group).

Worked Examples

  • AWS ALB and Cognito - the load balancer authenticates against a Cognito user pool and injects a signed x-amzn-oidc-data header.
  • NGINX, oauth2-proxy and KeyCloak - the proxy authenticates against KeyCloak (or any OIDC provider) and relays the IDP’s token as a bearer header.

Troubleshooting

Symptom Likely cause
Authentication Error: Failed to Fetch at the loading screen edgeAuthentication.enabled not set, so Stroom started a second flow of its own; or the proxy session lapsed (Stroom reloads the page once to let the proxy re-authenticate, then shows this)
Browser bounces between Stroom and the IDP forever Two Relying Parties: Stroom is running its own flow behind the proxy. Set edgeAuthentication.enabled
HTTP 403 with Authenticated user is not permitted to use stroom The proxy’s credential verified, but the user is unknown or disabled in Stroom, or the token could not be validated - check the issuer and (for an ALB) expectedSignerPrefixes
Requests fail with what looks like a network error Header size - set maxRequestHeaderSize
Data feeds or health checks broken The proxy’s authenticate rule covers a machine path - see the path table above
Signing out signs the user straight back in logout.cookiesToExpire / logout.signOutUrl not set, or the post logout page is behind the proxy’s authenticate rule

1 - AWS ALB and Cognito

Running Stroom behind an AWS Application Load Balancer that authenticates users against an Amazon Cognito user pool.

In this deployment the Application Load Balancer is the Open ID Connect Relying Party. Its listener rule sends unauthenticated browsers to Cognito, completes the code flow, holds the session in AWSELBAuthSessionCookie cookies, and forwards each authenticated request to Stroom with three extra headers:

Header Contents
x-amzn-oidc-data The user’s claims as a JWT, signed by the ALB with a regional AWS key (ES256)
x-amzn-oidc-accesstoken The access token from Cognito, in plain text
x-amzn-oidc-identity The sub claim, in plain text

Stroom authenticates the request by verifying the x-amzn-oidc-data signature against AWS’s regional public key endpoint, checking the token’s issuer against the configured one, and checking that the signing load balancer is one of yours.

Cognito Setup

Create a user pool, hosted UI domain and app client as described on the Cognito page, with these differences:

  • The app client belongs to the ALB, not to Stroom, so its allowed callback URL is the ALB’s own: https://STROOM_FQDN/oauth2/idpresponse (this fixed path is handled by the load balancer itself and never reaches Stroom).
  • The client must have a client secret and use the code grant; the ALB requires both.
  • Register the post logout landing page as an allowed sign out URL for the client (see Logout).

No second app client for Stroom is needed. The ALB is the only OIDC client in this topology.

Load Balancer Setup

Order the listener rules so machine traffic is forwarded without authentication, then authenticate everything else:

  1. Paths /datafeed*, /stroom/datafeed*, /remoting/*, /statusforward to the Stroom target group.
  2. Default → authenticate-cognito (your user pool, app client and hosted UI domain) then forward to the Stroom target group.

Points worth knowing:

  • SessionCookieName defaults to AWSELBAuthSessionCookie; if you change it, change edgeAuthentication.logout.cookiesToExpire to match.
  • The session cookie is sharded at 4KB per shard (-0, -1, …), which is why the header size limit matters.
  • If the total claims and access token exceed 11KB the ALB itself returns HTTP 500 and increments its ELBAuthUserClaimsSizeExceeded metric — trim what the IDP puts in the token if you hit this.
  • Restrict the Stroom target’s security group to accept traffic only from the ALB’s security group; this is trust prerequisite one.

Stroom Configuration

server:
  applicationConnectors:
    - type: http
      port: 8080
      useForwardedHeaders: true
      maxRequestHeaderSize: 32KiB

appConfig:
  publicUri: "https://STROOM_FQDN"    # the ALB's public address
  security:
    authentication:
      edgeAuthentication:
        enabled: true
        logout:
          cookiesToExpire: [ "AWSELBAuthSessionCookie" ]
          signOutUrl: "https://MY_DOMAIN.auth.REGION.amazoncognito.com/logout?\
client_id=ALB_CLIENT_ID&logout_uri=https://STROOM_FQDN/loggedOut"
      openId:
        identityProviderType: EXTERNAL_IDP
        # Cognito's discovery document; supplies the issuer that x-amzn-oidc-data is
        # checked against. Stroom runs no flow of its own, so no clientSecret is needed.
        openIdConfigurationEndpoint: "https://cognito-idp.REGION.amazonaws.com/\
POOL_ID/.well-known/openid-configuration"
        clientId: "ALB_CLIENT_ID"
        # MANDATORY - pins the JWT's 'signer' header to your load balancer(s).
        # Without it, every x-amzn-oidc-data token is rejected. Each value must reach at
        # least the account id; use the full ALB ARN where you know it.
        expectedSignerPrefixes:
          - "arn:aws:elasticloadbalancing:REGION:ACCOUNT_ID:"

expectedSignerPrefixes

The regional AWS endpoint that Stroom fetches verification keys from serves the keys of every load balancer in that region, so the signature alone proves a token came from an ALB, not from your ALB. This setting closes that gap: the signer field in the token’s header, which is the signing load balancer’s ARN, must start with one of the configured values.

It is required — with it unset, every ALB token is rejected, and the log message names this property.

publicKeyUriPattern

The default value fetches keys from https://public-keys.auth.elb.${awsRegion}.amazonaws.com/${keyId}, which is correct for the commercial AWS regions. AWS GovCloud serves the keys from different, S3 hosted endpoints, so GovCloud deployments must override it, e.g.:

        publicKeyUriPattern: "https://s3-us-gov-west-1.amazonaws.com/\
aws-elb-public-keys-prod-us-gov-west-1/${keyId}"

Identity Claims

The claims in x-amzn-oidc-data come from Cognito’s user info endpoint, not from an ID token. The default uniqueIdentityClaim of sub is correct and stable; set userDisplayNameClaim to taste (username and email are usually available).

Logout

AWS documents ending an ALB session as the application’s job: expire the session cookies and send the browser to the IDP’s logout endpoint. The configuration above does exactly that — cookiesToExpire removes the AWSELBAuthSessionCookie shards and signOutUrl sends the browser to Cognito’s /logout.

Two registration details make it work:

  • The logout_uri value must be registered in the Cognito app client as an allowed sign out URL.
  • The page it points at must be matched by a forward rule, not the authenticate rule, or the sign in flow simply restarts and the user never appears to sign out.

Verifying it Works

After deploying, load Stroom in a browser and check, in the developer tools network tab:

  1. You are redirected to the Cognito hosted UI, sign in, and land back at Stroom.
  2. The request to /api/auth/flow/v1/status returns 200 with "authenticated": true and the UI loads.
  3. There is no navigation to .../oauth2/authorize on the Cognito domain after that first sign in — if there is, Stroom is running a second flow and edgeAuthentication.enabled is not set.

On the Stroom side, the log should not contain Redirecting with an AuthenticationRequest to: during normal browsing.

2 - NGINX, oauth2-proxy and KeyCloak

Running Stroom behind NGINX with oauth2-proxy authenticating users against KeyCloak (or any Open ID Connect provider).

In this deployment oauth2-proxy is the Open ID Connect Relying Party. NGINX asks it to authorise each request (auth_request); oauth2-proxy completes the code flow against the Identity Provider (IDP) Identity Provider (IDP) An Identity Provider is a system or service that can authenticate a user and assert their identity. Identity providers can support single sign on (SSO), which allows the user to sign in once to the Identity Provider so they are then authenticated to all systems using that IDP.Click to see more details..., holds its session in _oauth2_proxy cookies, and hands back the IDP’s token, which NGINX forwards to Stroom as an Authorization: Bearer header.

Unlike the ALB, nothing here mints its own token: Stroom receives the IDP’s own token and verifies it against the IDP’s published keys, exactly as it would verify a token presented by an API client. That is why this pattern works unchanged with KeyCloak, Cognito or Entra ID behind the proxy.

KeyCloak Setup

Create a realm and a confidential client as described on the KeyCloak page, with one difference: the client’s redirect URI is oauth2-proxy’s callback, https://STROOM_FQDN/oauth2/callback, not Stroom’s.

No second client for Stroom is needed.

Oauth2-proxy Setup

provider = "keycloak-oidc"
oidc_issuer_url = "https://IDP_HOST/realms/REALM"
client_id = "stroom-proxy-client"
client_secret = "THE_CLIENT_SECRET"
redirect_url = "https://STROOM_FQDN/oauth2/callback"
cookie_secret = "RANDOM_32_BYTES_BASE64"

# Hand the IDP's token to NGINX so it can be forwarded to Stroom.
set_authorization_header = true

# Refresh the session before the access token expires, so the forwarded
# token is always live.
cookie_refresh = "4m"

NGINX Setup

The essential shape — authenticate the browser paths, forward the machine paths untouched, and overwrite the Authorization header on everything proxied:

server {
    listen 443 ssl;
    server_name STROOM_FQDN;

    # oauth2-proxy's own endpoints (sign in, callback, sign out)
    location /oauth2/ {
        proxy_pass       http://oauth2-proxy:4180;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Subrequest endpoint used by auth_request
    location = /oauth2/auth {
        internal;
        proxy_pass              http://oauth2-proxy:4180;
        proxy_set_header        Content-Length "";
        proxy_pass_request_body off;
    }

    # Browser facing paths - authenticated
    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/sign_in;

        # Take the token oauth2-proxy returned and forward it to Stroom.
        # proxy_set_header OVERWRITES any client supplied Authorization header,
        # which is one of the trust prerequisites.
        auth_request_set $auth_token $upstream_http_authorization;
        proxy_set_header Authorization $auth_token;

        proxy_pass       https://stroom-backend:8080/;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
    }

    # Machine paths - no auth_request, Stroom authenticates these itself
    location /datafeed  { proxy_pass https://stroom-backend:8080/datafeed; }
    location /remoting/ { proxy_pass https://stroom-backend:8080/remoting/; }
    location /status    { proxy_pass https://stroom-backend:8080/status; }
}

Stroom Configuration

server:
  applicationConnectors:
    - type: http
      port: 8080
      useForwardedHeaders: true
      maxRequestHeaderSize: 32KiB    # oauth2-proxy chunks its session cookie

appConfig:
  publicUri: "https://STROOM_FQDN"
  security:
    authentication:
      edgeAuthentication:
        enabled: true
        logout:
          cookiesToExpire: [ "_oauth2_proxy" ]
          signOutUrl: "https://STROOM_FQDN/oauth2/sign_out"
      openId:
        identityProviderType: EXTERNAL_IDP
        # The real IDP's discovery document - Stroom verifies the forwarded token
        # against the keys it advertises.
        openIdConfigurationEndpoint: "https://IDP_HOST/realms/REALM/\
.well-known/openid-configuration"
        # oauth2-proxy's client - the forwarded token's audience is this client.
        clientId: "stroom-proxy-client"
        # Leave requiredAccessTokenType unset: oauth2-proxy forwards the ID token.

No clientSecret is needed; Stroom runs no flow of its own.

Logout

The configuration above expires oauth2-proxy’s (chunked) session cookies and sends the browser to /oauth2/sign_out, which ends the proxy session. To also end the KeyCloak session, give oauth2-proxy’s sign out a redirect to KeyCloak’s end session endpoint:

          signOutUrl: "https://STROOM_FQDN/oauth2/sign_out?rd=https%3A%2F%2FIDP_HOST%2Frealms%2FREALM%2Fprotocol%2Fopenid-connect%2Flogout"

(The rd value must be URL encoded and allowed by oauth2-proxy’s whitelist_domains.)

Verifying it Works

  1. Loading Stroom redirects via oauth2-proxy to KeyCloak; after signing in, the UI loads.
  2. The request to /api/auth/flow/v1/status returns 200 with "authenticated": true, and there is no subsequent navigation to KeyCloak’s /auth endpoint.
  3. curl -H "Authorization: Bearer $TOKEN" https://stroom-backend:8080/api/... from inside the network still works — machine access does not traverse the proxy.