Skip to content

Support RS256/JWKS signed JSON Web Token for SSO login - #613

Open
gilfixefy wants to merge 2 commits into
joeferner:masterfrom
Fixefy:feature/sso-jwt-public-key-jwks
Open

Support RS256/JWKS signed JSON Web Token for SSO login#613
gilfixefy wants to merge 2 commits into
joeferner:masterfrom
Fixefy:feature/sso-jwt-public-key-jwks

Conversation

@gilfixefy

Copy link
Copy Markdown

Summary

External SSO tokens (/sso route) can currently only be validated with a shared secret, so identity
providers signing their tokens with a private key (Keycloak, Auth0, Entra ID, Google, Okta, ...) cannot
be used. This PR adds validation of tokens signed with asymmetric algorithms (RS256, PS256, ES256, ...).

Beside the existing sso.jwtSharedSecret the key to validate the signature with can now be configured as:

  • sso.jwtPublicKey (env SSO_JWT_PUBLIC_KEY) - PEM encoded public key or the name of a file containing
    it, so it can be mounted as Docker/Kubernetes secret and referenced by name
  • sso.jwksUri (env SSO_JWKS_URI) - url of the JSON Web Key Set endpoint of the identity provider

Exactly one of the three key sources must be set, everything else is rejected on startup.

Additional config keys: sso.jwksCacheMaxAge (env SSO_JWKS_CACHE_MAX_AGE, default 600 seconds) and an
env var for the already existing algorithm list (SSO_JWT_ALGORITHMS, comma separated).

No new dependency

The keys are downloaded with the node core https module and imported with crypto.createPublicKey(),
therefore no additional npm package (like jwks-rsa and its transitive dependencies) is pulled into the
image. All new code lives in lib/ssoKeys.js, lib/app.js only passes the key resolver to jwt.verify().

Keys downloaded from the JWKS endpoint are cached in memory, refreshed after sso.jwksCacheMaxAge seconds
and whenever a token references an unknown key id. The latter is rate limited to one download every 30
seconds so unknown key ids cannot be used to flood the identity provider. If the endpoint is temporarily
unreachable the keys downloaded before are used as long as possible.

Security

HMAC algorithms are rejected on startup whenever a public key or a JWKS endpoint is configured - a public
key is known to everybody and could be used as shared secret to sign forged tokens otherwise. oct keys
(shared secrets) published at a JWKS endpoint are ignored for the same reason, as are keys marked for
encryption only.

Included bugfix (separate commit)

sso.subject was assigned to the audience verify option instead of subject, so a configured subject
silently replaced the audience check instead of validating the sub claim. Kept as its own commit to be
able to drop or cherry-pick it independently.

Test plan

  • npm test - 30 new tests in test/testSsoKeys.js covering key loading from PEM string and file,
    JWKS download, caching, refresh, rate limiting for unknown key ids, fallback to cached keys on endpoint
    errors, malformed key sets, RS256 end-to-end verification and the new config validation rules
  • manual test against a local JWKS endpoint: login with a valid RS256 token succeeds, token signed
    with another key is rejected ("invalid signature"), replayed token hits the existing single-use check,
    JWKS endpoint is called only once due to caching
  • manual test with sso.jwtPublicKey pointing to a key file, including subject mismatch being rejected
  • redis-commander --test for the new validation errors and for the unchanged shared secret setup

Remarks

  • existing configurations are not affected, defaults are unchanged
  • importing JWK formatted keys needs Node.js >= 15.12, the engines field still says >=12 and was left
    untouched - happy to bump it if wanted
  • no changes needed for the Helm chart, the new env vars can be set via its generic env list

The value of config key "sso.subject" was assigned to the "audience" verify
option instead of "subject". A configured subject therefore silently replaced
the audience check instead of validating the "sub" claim of the SSO token.
External SSO tokens could only be validated with a shared secret so far, which
does not work with identity providers signing their tokens with a private key.

Beside "sso.jwtSharedSecret" the key to validate the signature can now be set as
PEM encoded public key ("sso.jwtPublicKey", either the key itself or the name of
a file containing it) or downloaded from the JSON Web Key Set endpoint of the
identity provider ("sso.jwksUri"). Downloaded keys are cached in memory and
refreshed after "sso.jwksCacheMaxAge" seconds as well as on unknown key ids,
the latter rate limited to not flood the identity provider with requests.
No additional npm dependency is needed, the keys are fetched and imported with
the node core modules https and crypto.

Exactly one of the three key sources must be configured. HMAC algorithms are
rejected on startup whenever a public key is used - the public key is known to
everybody and could be used as shared secret to sign forged tokens otherwise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant