Skip to content

feat: emit jti/origin_jti and support RevokeToken#1705

Open
AbhigyaKrishna wants to merge 2 commits into
floci-io:mainfrom
AbhigyaKrishna:feat/origin-jti
Open

feat: emit jti/origin_jti and support RevokeToken#1705
AbhigyaKrishna wants to merge 2 commits into
floci-io:mainfrom
AbhigyaKrishna:feat/origin-jti

Conversation

@AbhigyaKrishna

Copy link
Copy Markdown
Contributor

Summary

Brings Floci's Cognito token generation to parity with real AWS Cognito by emitting the two native token-revocation claims and honoring the RevokeToken API. Implements the Sapphire Terminal team's patch request (floci-patch-origin-jti-request.md) that unblocks end-to-end dev testing of "remote device logout" (instant session revocation).

  • jti — a unique UUID stamped on every access and ID token, regenerated on each issuance/refresh.
  • origin_jti — a per-refresh-token-family UUID: minted at initial authentication, embedded in the refresh token, and kept identical for every access/ID token later minted from that refresh token (stable across REFRESH_TOKEN_AUTH / GetTokensFromRefreshToken), while differing between independent logins.
  • Both claims are gated on the app client's EnableTokenRevocation flag (default true), matching AWS, which omits them when revocation is disabled.
  • RevokeToken API wired: revoking a refresh token invalidates its whole origin_jti family, so subsequent refreshes fail with NotAuthorizedException.
    origin_jti and the refresh token's embedded id was unrelated — breaking the stable-across-refresh contract downstream authorizers rely on.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

AWS Compatibility

  • Protocol: Cognito AWS JSON 1.1 (X-Amz-Target: AWSCognitoIdentityProviderService.*). No wire-format or endpoint-shape changes.
  • Claims: jti / origin_jti match AWS's documented token revocation semantics — origin_jti is the stable revocation identifier for the refresh-token family; jti is unique per JWT.
  • RevokeToken errors modeled after AWS: UnsupportedOperationException when the client has EnableTokenRevocation=false, UnsupportedTokenTypeException for a non-refresh token, NotAuthorizedException for a client mismatch / bad client secret, InvalidParameterException for missing Token/ClientId; empty 200 on success.
  • DescribeUserPoolClient already reports EnableTokenRevocation (defaults to true on client creation) — verified by the new test.
  • Verification: RestAssured integration tests exercise the real JSON 1.1 wire path (InitiateAuth USER_PASSWORD_AUTH / REFRESH_TOKEN_AUTH, GetTokensFromRefreshToken, RevokeToken, DescribeUserPoolClient) and decode the issued JWTs to assert claim presence, family stability, per-token jti uniqueness, and revocation behavior.

Checklist

  • ./mvnw test passes locally — full Cognito suite 234 + 10 new = green (run under JDK 26 with -Denforcer.skip=true, as no JDK 25 is installed locally; CI on JDK 25 is authoritative)
  • New or updated integration test added — OriginJtiIntegrationTest (10 tests)
  • Commit messages follow Conventional Commits — feat: emit jti/origin_jti and support RevokeToken

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds AWS-compatible jti/origin_jti JWT claims to Floci's Cognito token generation and implements the RevokeToken API, enabling end-to-end testing of remote device logout (instant session revocation). The implementation correctly scopes origin_jti to clients with EnableTokenRevocation=true and keeps the family identifier stable across refresh cycles.

  • jti/origin_jti emission: A fresh originJti UUID is minted at initial authentication, embedded in the refresh token, and propagated as origin_jti into every access/ID token issued from that refresh token; jti is a unique UUID per individual token issuance. Only origin_jti is gated on EnableTokenRevocation (not jti), which the accompanying test confirms but the PR description contradicts.
  • RevokeToken API: Validates caller identity (client secret) before checking the revocation-enabled flag, matching AWS ordering; stores the revocation record keyed on the refresh token's embedded familyId (= originJti), which the existing REFRESH_TOKEN_AUTH and GetTokensFromRefreshToken revocation checks already query by the same key.
  • 10 new integration tests cover initial auth claim presence, family stability across refresh paths, EnableTokenRevocation=false omission, revocation invalidation, and error-ordering for confidential clients.

Confidence Score: 5/5

Safe to merge — the revocation key is consistent across all code paths, auth ordering matches AWS, and the token family semantics are correctly implemented end-to-end.

The revokeToken implementation correctly validates the caller identity before checking client configuration, embeds originJti as the stable family identifier in the refresh token, and keys revocation records identically to how the existing REFRESH_TOKEN_AUTH and GetTokensFromRefreshToken paths look them up. The isExpired() comparison uses seconds throughout the new code. Ten new integration tests exercise the full session lifecycle.

No files require special attention.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java Core logic: adds revokeToken(), originJti generation, buildRefreshToken overload, and isTokenRevocationEnabled helper. Auth/revocation ordering is correct; expiry uses the right seconds unit; revocation key is consistent with existing validation checks.
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoJsonHandler.java Wires RevokeToken action to handleRevokeToken(), which correctly returns an empty 200 JSON object on success, matching AWS wire behavior.
src/test/java/io/github/hectorvent/floci/services/cognito/OriginJtiIntegrationTest.java 10 ordered integration tests covering claim emission, family stability across REFRESH_TOKEN_AUTH and GetTokensFromRefreshToken, revocation invalidation, and error-ordering for confidential clients.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Floci
    participant Svc as CognitoService
    participant Store as RevokedTokenStore
    Note over Client,Store: Initial Authentication
    Client->>Floci: InitiateAuth USER_PASSWORD_AUTH
    Floci->>Svc: generateAuthResult
    Svc->>Svc: "originJti = UUID.randomUUID()"
    Svc->>Svc: "AccessToken jti=new, origin_jti=originJti"
    Svc->>Svc: "IdToken jti=new, origin_jti=originJti"
    Svc->>Svc: "RefreshToken familyId=originJti"
    Floci-->>Client: tokens
    Note over Client,Store: Refresh
    Client->>Floci: REFRESH_TOKEN_AUTH
    Floci->>Svc: "parse token parts[4]=familyId"
    Svc->>Store: validateTokenNotRevoked(familyId)
    Store-->>Svc: ok
    Svc->>Svc: "new jti, same origin_jti=familyId"
    Floci-->>Client: new tokens
    Note over Client,Store: RevokeToken
    Client->>Floci: RevokeToken
    Floci->>Svc: revokeToken
    Svc->>Svc: check secret then revocation flag
    Svc->>Svc: "parse token parts[4]=familyId"
    Svc->>Store: put revokedTokenKey(poolId,familyId)
    Floci-->>Client: 200 OK
    Note over Client,Store: Post-revocation refresh
    Client->>Floci: REFRESH_TOKEN_AUTH
    Svc->>Store: validateTokenNotRevoked(familyId)
    Store-->>Svc: REVOKED
    Floci-->>Client: 400 NotAuthorizedException
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Floci
    participant Svc as CognitoService
    participant Store as RevokedTokenStore
    Note over Client,Store: Initial Authentication
    Client->>Floci: InitiateAuth USER_PASSWORD_AUTH
    Floci->>Svc: generateAuthResult
    Svc->>Svc: "originJti = UUID.randomUUID()"
    Svc->>Svc: "AccessToken jti=new, origin_jti=originJti"
    Svc->>Svc: "IdToken jti=new, origin_jti=originJti"
    Svc->>Svc: "RefreshToken familyId=originJti"
    Floci-->>Client: tokens
    Note over Client,Store: Refresh
    Client->>Floci: REFRESH_TOKEN_AUTH
    Floci->>Svc: "parse token parts[4]=familyId"
    Svc->>Store: validateTokenNotRevoked(familyId)
    Store-->>Svc: ok
    Svc->>Svc: "new jti, same origin_jti=familyId"
    Floci-->>Client: new tokens
    Note over Client,Store: RevokeToken
    Client->>Floci: RevokeToken
    Floci->>Svc: revokeToken
    Svc->>Svc: check secret then revocation flag
    Svc->>Svc: "parse token parts[4]=familyId"
    Svc->>Store: put revokedTokenKey(poolId,familyId)
    Floci-->>Client: 200 OK
    Note over Client,Store: Post-revocation refresh
    Client->>Floci: REFRESH_TOKEN_AUTH
    Svc->>Store: validateTokenNotRevoked(familyId)
    Store-->>Svc: REVOKED
    Floci-->>Client: 400 NotAuthorizedException
Loading

Reviews (2): Last reviewed commit: "fix(cognito): authenticate RevokeToken c..." | Re-trigger Greptile

Comment thread src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java Outdated
…g check

Address PR review feedback on the jti/origin_jti + RevokeToken change:

- RevokeToken now validates the client secret before the
  EnableTokenRevocation check, so a confidential client with revocation
  disabled returns NotAuthorizedException (not UnsupportedOperationException)
  for a wrong/missing secret, matching AWS's identity-first ordering and
  avoiding leaking revocation state to unauthenticated callers.
- OriginJtiIntegrationTest guards the shared ordered-test state via
  requireSetup()/requireInitialAuthState() so a setup or initial-auth
  failure reports a clear cause instead of misleading downstream
  assertions, and adds a regression test for the auth-ordering fix.
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