[Security][Hardening] JWT validation deviates from secure defaults: RequireHttpsMetadata=false, ValidateIssuer=false #690

Closed
opened 2026-07-17 22:54:24 +00:00 by spikerj · 2 comments
Owner

Finding

SpikerSoft.Api/Infrastructure/Authentication.cs (ConfigureKeycloakJwtBearer) drives two validation controls from config, and the effective production values (from appsettings.json; appsettings.Production.json does not override them, and no Keycloak__* env var does either) are:

  • Keycloak:require-https = False -> RequireHttpsMetadata = false
  • Keycloak:validate-issuer = False -> ValidateIssuer = false

Exploitability (evaluated — currently defense-in-depth, NOT an active bypass)

  • RequireHttpsMetadata=false: the OIDC metadata/JWKS is fetched from MetadataAddress = https://ids.spikersoft.com/realms/spikersoft/.well-known/openid-configuration. Because that URL is already HTTPS, the key material is fetched over TLS regardless of the flag — there is no MITM window today. The flag only fails to reject a hypothetical future http metadata URL.
  • ValidateIssuer=false: signing keys come from that single realm's JWKS and ValidateIssuerSigningKey defaults to true, so a token must be signed by the realm's private key to pass. An attacker cannot forge one, and there is no second trusted realm, so skipping the iss check does not enable a bypass today.

Both are, however, secure-default deviations that a security review flags and that become exploitable under config drift (e.g. someone points server-url at an http/internal Keycloak).

Recommended fix (NOT applied here — auth-config change with regression risk)

Enable both in the production layer only (leave dev/base as-is, where Keycloak may be http):

  • Keycloak:require-https = true — safe; prod server-url is already HTTPS, so no behaviour change, just fail-secure.
  • Keycloak:validate-issuer = trueverify first: confirm the realm's issued token iss equals the metadata issuer (https://ids.spikersoft.com/realms/spikersoft). If Keycloak's frontend URL (KC_HOSTNAME) ever differs from server-url, enabling this would reject valid tokens and cause an auth outage. Roll out behind a canary.

Not fixed blind here specifically to avoid a production auth regression.

## Finding `SpikerSoft.Api/Infrastructure/Authentication.cs` (`ConfigureKeycloakJwtBearer`) drives two validation controls from config, and the effective **production** values (from `appsettings.json`; `appsettings.Production.json` does not override them, and no `Keycloak__*` env var does either) are: - `Keycloak:require-https = False` -> `RequireHttpsMetadata = false` - `Keycloak:validate-issuer = False` -> `ValidateIssuer = false` ## Exploitability (evaluated — currently defense-in-depth, NOT an active bypass) - **RequireHttpsMetadata=false:** the OIDC metadata/JWKS is fetched from `MetadataAddress = https://ids.spikersoft.com/realms/spikersoft/.well-known/openid-configuration`. Because that URL is already HTTPS, the key material is fetched over TLS regardless of the flag — there is no MITM window today. The flag only fails to *reject* a hypothetical future http metadata URL. - **ValidateIssuer=false:** signing keys come from that single realm's JWKS and `ValidateIssuerSigningKey` defaults to true, so a token must be signed by the realm's private key to pass. An attacker cannot forge one, and there is no second trusted realm, so skipping the `iss` check does not enable a bypass today. Both are, however, secure-default deviations that a security review flags and that become exploitable under config drift (e.g. someone points `server-url` at an http/internal Keycloak). ## Recommended fix (NOT applied here — auth-config change with regression risk) Enable both in the **production** layer only (leave dev/base as-is, where Keycloak may be http): - `Keycloak:require-https = true` — safe; prod `server-url` is already HTTPS, so no behaviour change, just fail-secure. - `Keycloak:validate-issuer = true` — **verify first**: confirm the realm's issued token `iss` equals the metadata issuer (`https://ids.spikersoft.com/realms/spikersoft`). If Keycloak's frontend URL (KC_HOSTNAME) ever differs from `server-url`, enabling this would reject valid tokens and cause an auth outage. Roll out behind a canary. Not fixed blind here specifically to avoid a production auth regression.
Author
Owner

Verify-first step completed; hardening up as backend PR #417.

Verification performed (the gate this ticket set before enabling validate-issuer):

  1. Live discovery document: issuer = https://ids.spikersoft.com/realms/spikersoft — and the JwtBearer handler validates iss against exactly the issuer it fetched from metadata, so expected==actual by construction.
  2. Repo-wide sweep: the public HTTPS host is the only Keycloak:server-url configured anywhere (Api base + Development; no worker or service uses an internal http://keycloak URL for token acquisition). Every token presented to the API therefore carries the matching iss. The KC_HOSTNAME-mismatch outage scenario cannot occur under current config.

PR scope: appsettings.Production.json gains a Keycloak section with require-https=true + validate-issuer=true (dev/base untouched, per this ticket's recommendation), documented in-file with the verification and the do-not-point-at-internal-URL caveat. Plus JwtValidationProductionConfigTests — loads the real base+Production layering and asserts both flags and the https server-url precondition, so the config drift this ticket flags as the actual risk now fails the build.

Merge is the deploy gate (nothing hot-patched). Will close on merge.

**Verify-first step completed; hardening up as backend PR [#417](https://git.spikersoft.com/spikerj/spikersoft-backend/pulls/417).** Verification performed (the gate this ticket set before enabling `validate-issuer`): 1. Live discovery document: `issuer` = `https://ids.spikersoft.com/realms/spikersoft` — and the JwtBearer handler validates `iss` against exactly the issuer it fetched from metadata, so expected==actual by construction. 2. Repo-wide sweep: the public HTTPS host is the **only** `Keycloak:server-url` configured anywhere (Api base + Development; no worker or service uses an internal `http://keycloak` URL for token acquisition). Every token presented to the API therefore carries the matching `iss`. The KC_HOSTNAME-mismatch outage scenario cannot occur under current config. PR scope: `appsettings.Production.json` gains a `Keycloak` section with `require-https=true` + `validate-issuer=true` (dev/base untouched, per this ticket's recommendation), documented in-file with the verification and the do-not-point-at-internal-URL caveat. Plus `JwtValidationProductionConfigTests` — loads the real base+Production layering and asserts both flags and the https `server-url` precondition, so the config drift this ticket flags as the actual risk now fails the build. Merge is the deploy gate (nothing hot-patched). Will close on merge.
Author
Owner

Resolved in backend PR #417 (merged to master 2026-07-18). Production now runs require-https=true + validate-issuer=true (verified safe: discovery issuer matches, no internal Keycloak URL exists anywhere), with JwtValidationProductionConfigTests failing the build on config drift. Closing.

Resolved in backend PR [#417](https://git.spikersoft.com/spikerj/spikersoft-backend/pulls/417) (merged to `master` 2026-07-18). Production now runs `require-https=true` + `validate-issuer=true` (verified safe: discovery issuer matches, no internal Keycloak URL exists anywhere), with `JwtValidationProductionConfigTests` failing the build on config drift. Closing.
Sign in to join this conversation.