Skip to content

feat: Implement Cognito GlobalSignOut#1701

Open
AbhigyaKrishna wants to merge 2 commits into
floci-io:mainfrom
AbhigyaKrishna:feat/cognito-signout
Open

feat: Implement Cognito GlobalSignOut#1701
AbhigyaKrishna wants to merge 2 commits into
floci-io:mainfrom
AbhigyaKrishna:feat/cognito-signout

Conversation

@AbhigyaKrishna

Copy link
Copy Markdown
Contributor

Summary

Implements the Cognito GlobalSignOut action, which was previously an
unsupported operation (it fell through to the UnsupportedOperation default in
the JSON dispatcher).

GlobalSignOut is the self-service counterpart to the already-supported
AdminUserGlobalSignOut: instead of admin credentials, it authenticates with the
caller's own access token and invalidates all tokens (access, ID, refresh)
that Cognito issued to that user.

  • CognitoService.globalSignOut(accessToken) — extracts the username/pool/jti
    from the access token, rejects invalid or already-revoked/signed-out tokens with
    NotAuthorizedException, then revokes every issued token for the user via the
    existing revokeAllUserTokens(...).
  • CognitoJsonHandler — adds handleGlobalSignOut(...) and wires
    case "GlobalSignOut" into the action dispatch (returns HTTP 200 with an empty
    body, matching AWS).

Type of change

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

AWS Compatibility

New action GlobalSignOut on the AWSCognitoIdentityProviderService target
(AWS JSON 1.1). Wire contract:

  • Request: { "AccessToken": "<jwt>" }
  • Success: 200 with empty JSON body {}
  • Failure: NotAuthorizedException for an invalid, revoked, or
    already-signed-out access token.

Behavior mirrors the existing, already-verified AdminUserGlobalSignOut path and
was validated end-to-end through the project's integration-test harness
(GlobalSignOutIntegrationTest), which exercises the real request/response
envelope: sign-in → GlobalSignOut → old access token rejected → old refresh
token rejected → a fresh login still succeeds.

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added (GlobalSignOutIntegrationTest, 9 cases)
  • Commit messages follow Conventional Commits

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements GlobalSignOut, the self-service counterpart to the existing AdminUserGlobalSignOut, wiring it through the JSON dispatcher and adding comprehensive integration tests. The implementation correctly authenticates via the caller's access token and delegates to the existing revokeAllUserTokens path.

  • CognitoService.globalSignOut extracts and validates username, poolId, and jti from the access token, checks per-token and global revocation state, confirms the pool and user exist via adminGetUser, then revokes all issued tokens — matching the order used by getUser and changePassword.
  • CognitoJsonHandler adds handleGlobalSignOut and wires case \"GlobalSignOut\" into the dispatcher, returning HTTP 200 with an empty JSON body as AWS specifies.
  • GlobalSignOutIntegrationTest adds 10 ordered integration-test cases covering sign-out success, post-sign-out rejection of both access and refresh tokens, re-login validity, and rejection of forged tokens for nonexistent users.

Confidence Score: 5/5

Safe to merge — the change is a straightforward, well-tested addition of a previously missing Cognito action that delegates entirely to existing, already-exercised helpers.

The globalSignOut method correctly validates the access token (null checks for username, poolId, and jti), checks per-token and global revocation state, confirms both pool and user existence via adminGetUser before mutating the revocation store, and then revokes all tokens. The ordering and guard set exactly mirrors getUser and addresses both issues raised in the prior review round. The extractIatFromToken helper has a Jackson-based fallback that correctly handles numeric iat values, so the global revocation timestamp comparison works for real emulator-issued tokens. Ten ordered integration tests cover the full happy path, both token-type rejections after sign-out, fresh login, and forged/nonexistent-user tokens.

No files require special attention.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java Adds globalSignOut: validates token fields, checks revocation, confirms user/pool exist via adminGetUser, then calls revokeAllUserTokens. Correct ordering matches getUser/changePassword; jti null-guard and user-existence check address prior review comments.
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoJsonHandler.java Wires case "GlobalSignOut" into the dispatcher and adds handleGlobalSignOut; correctly returns HTTP 200 with an empty JSON object, matching AWS behavior.
src/test/java/io/github/hectorvent/floci/services/cognito/GlobalSignOutIntegrationTest.java 10 ordered integration tests covering the full sign-in → GlobalSignOut → token rejection → re-login cycle, plus invalid-token and nonexistent-user edge cases. Uses shared static state via @Order — consistent with existing Cognito integration test patterns.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant CognitoJsonHandler
    participant CognitoService
    participant RevokedTokenStore
    participant UserStore

    Client->>CognitoJsonHandler: "POST GlobalSignOut { AccessToken }"
    CognitoJsonHandler->>CognitoService: globalSignOut(accessToken)
    CognitoService->>CognitoService: "extractUsernameFromToken()<br/>extractPoolIdFromToken()<br/>extractJtiFromToken()"
    alt "username / poolId / jti == null"
        CognitoService-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>RevokedTokenStore: validateTokenNotRevoked(jti, poolId)
    alt jti revoked
        RevokedTokenStore-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>CognitoService: extractIatFromToken()
    CognitoService->>RevokedTokenStore: validateUserNotGloballySignedOut(username, poolId, iat)
    alt token issued before revocation
        RevokedTokenStore-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>UserStore: adminGetUser(poolId, username)
    alt pool not found
        UserStore-->>Client: 400 ResourceNotFoundException
    end
    alt user not found
        UserStore-->>Client: 400 UserNotFoundException
    end
    CognitoService->>RevokedTokenStore: revokeAllUserTokens(poolId, username)
    Note over RevokedTokenStore: Writes global:{username} revocation record (1-year expiry)
    CognitoService-->>CognitoJsonHandler: void
    CognitoJsonHandler-->>Client: "200 {}"
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 CognitoJsonHandler
    participant CognitoService
    participant RevokedTokenStore
    participant UserStore

    Client->>CognitoJsonHandler: "POST GlobalSignOut { AccessToken }"
    CognitoJsonHandler->>CognitoService: globalSignOut(accessToken)
    CognitoService->>CognitoService: "extractUsernameFromToken()<br/>extractPoolIdFromToken()<br/>extractJtiFromToken()"
    alt "username / poolId / jti == null"
        CognitoService-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>RevokedTokenStore: validateTokenNotRevoked(jti, poolId)
    alt jti revoked
        RevokedTokenStore-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>CognitoService: extractIatFromToken()
    CognitoService->>RevokedTokenStore: validateUserNotGloballySignedOut(username, poolId, iat)
    alt token issued before revocation
        RevokedTokenStore-->>Client: 400 NotAuthorizedException
    end
    CognitoService->>UserStore: adminGetUser(poolId, username)
    alt pool not found
        UserStore-->>Client: 400 ResourceNotFoundException
    end
    alt user not found
        UserStore-->>Client: 400 UserNotFoundException
    end
    CognitoService->>RevokedTokenStore: revokeAllUserTokens(poolId, username)
    Note over RevokedTokenStore: Writes global:{username} revocation record (1-year expiry)
    CognitoService-->>CognitoJsonHandler: void
    CognitoJsonHandler-->>Client: "200 {}"
Loading

Reviews (2): Last reviewed commit: "fix(cognito): validate user existence an..." | Re-trigger Greptile

Comment thread src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java Outdated
Address PR floci-io#1701 review:
- Add adminGetUser existence check before revoking tokens so a crafted
  token referencing a nonexistent pool/user returns UserNotFoundException/
  ResourceNotFoundException instead of silently writing revocation records
  and returning 200, matching AdminUserGlobalSignOut and getUser.
- Include jti in the invalid-token guard so legacy tokens without a jti
  claim cannot be used as a sign-out credential, consistent with getUser.
- Cover the nonexistent-user path with an integration test.
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