Skip to content

fix(auth): bind internal JWTs to principal secret generation#5053

Open
vigneshio wants to merge 2 commits into
apache:mainfrom
vigneshio:fix/jwt-bind-credentials-epoch
Open

fix(auth): bind internal JWTs to principal secret generation#5053
vigneshio wants to merge 2 commits into
apache:mainfrom
vigneshio:fix/jwt-bind-credentials-epoch

Conversation

@vigneshio

Copy link
Copy Markdown
Contributor

Internal JWTs are bound to the principal's current main secret hash (cv claim). After credential rotate or reset, previously issued access tokens are rejected on verify so sessions cannot outlive a secret compromise response.

Tokens minted before this change (missing cv) are also rejected and must be reissued via client credentials.

No schema migration: the fingerprint is the existing mainSecretHash.

@github-project-automation github-project-automation Bot moved this to PRs In Progress in Basic Kanban Board Jul 13, 2026
@vigneshio
vigneshio force-pushed the fix/jwt-bind-credentials-epoch branch from 5925d16 to 1a98a23 Compare July 16, 2026 13:35

@dimas-b dimas-b left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your diligence, @vigneshio ! The general idea is very reasonable. Some implementation comments below.

* secrets are rotated or reset, {@code mainSecretHash} changes and tokens carrying a prior value
* are rejected on verify.
*/
@VisibleForTesting static final String CLAIM_KEY_CREDENTIALS_VERSION = "cv";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per RFC 7519 this has to be a "collision-resistant" name.

How about polaris-cv?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.. renamed to polaris-cv.

principal.entity().getId(),
clientId,
scope,
principal.mainSecretHash());

@dimas-b dimas-b Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exposing secret hashes in JWTs is a good idea. These JWTs are not encrypted.

I believe a more robust approach would be to generate a separate number for this. Perhaps it can be handled inside PolarisPrincipalSecrets by adjusting the processing of existing fields in a backward-compatible way.

throw new NotAuthorizedException("Failed to verify the token");
}
if (credentialsVersion == null || credentialsVersion.isBlank()) {
throw new NotAuthorizedException("Failed to verify the token");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be too strong. This will break all clients on Polaris upgrade.

I believe a more robust option would be to enforce "cv" after the first credential reset/rotation that happens in the new Polaris version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, 👀 that was too aggressive 😅 fixed - tokens without the claim are just accepted now (treated as pre-binding legacy), so nothing breaks on upgrade. everything minted after the upgrade carries the claim and dies on the next rotate/reset.

on the "enforce after first rotation" idea - that needs per-principal state to tell "rotated after upgrade" from "never rotated", and jdbc keeps secrets in plain columns so that's a schema change. trade-off i took instead: old claim-less tokens stay valid till expiry (max 1h by default), all new tokens are fully bound. can do the strict per-principal version as a follow-up if you think the window matters. WDYT ? @dimas-b

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to re-mint without a forced rotation. The old token does not have a long lifespan and the new token will have "cv" data.

@dimas-b
dimas-b requested review from adutra and snazy July 16, 2026 22:47
Embed a credentials-generation fingerprint (polaris-cv claim) in minted
access tokens. The version is derived from the principal's current main
secret hash (hash of the hash), so the secret-verification artifact
itself is never exposed in the unencrypted token and no schema change
is needed.

On verify (and thus token exchange), tokens whose version no longer
matches are rejected, so credential rotate/reset invalidates
outstanding sessions without waiting for token expiry.

Tokens minted before this change carry no claim and remain valid until
expiry for upgrade compatibility; they become bound on the next
rotate/reset.
Access tokens are bound to per-realm principal secret hashes. Reusing a
POLARIS admin token against POLARIS2 correctly returns 401. Bootstrap
both realms with known credentials and obtain a POLARIS2 token for the
cross-realm rate-limit isolation assertion.
@vigneshio
vigneshio force-pushed the fix/jwt-bind-credentials-epoch branch from 1a98a23 to 083f99e Compare July 17, 2026 08:02
*/
@Nullable
public String getCredentialsVersion() {
return mainSecretHash == null ? null : DigestUtils.sha256Hex(mainSecretHash);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should salt this hash for good measure.

Also, "version" processing should respect the "secondary" hash too.

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.

2 participants