Skip to content

Sign out does not revoke the session: _delete clears the token cache before building the request, so the DELETE is unauthenticated #445

Description

@dbeattie71

Calling signOut() clears local state and reports success, but revokes nothing. The session stays active on the back end until it expires naturally, and any token already minted from it stays valid.

Api._delete clears the token cache before building the request:

Future<bool> _delete(String path, {bool requiresSessionId = false}) async {
  _tokenCache.clear();                                   // wipes _clientToken and _sessionId
  final headers = _headers(method: HttpMethod.delete);   // reads hasClientToken -> false

_headers attaches Authorization only if (_tokenCache.hasClientToken), and _queryParams reads sessionId from the same cache. So DELETE /v1/client identifies neither client nor session. The back end answers 200 to it — there is nothing to revoke for an unidentified client — and signOut returns Client.empty regardless of the outcome, so the caller sees success.

deleteUser() goes through the same helper via DELETE /me and is worse off: requiresSessionId: true means it also depends on the sessionId cleared on that same line. It reports deleting an account that has not been deleted.

Steps to reproduce

  1. Sign in on any platform.
  2. Call signOut() (or tap Sign out in any Clerk UI).
  3. Note the app returns to the signed-out state and reports success.
  4. Query the session through the Backend API: GET https://api.clerk.com/v1/sessions/<session_id>.

Expected results

The session is revoked. The Backend API reports it ended / revoked, and tokens minted from it stop working.

Actual results

The session is still active, with its original abandon_at (a month out, in our case). Observed on a release build on a physical iPhone and again on the Simulator.

session id : sess_… (the session just "signed out" of)
status     : active
abandon_at : <unchanged, ~30 days away>

Sign-out is therefore a local forget rather than a revocation. That matters for the case sign-out exists to serve — a shared, lost, or stolen device, or signing out because something is suspected compromised. The user performs the revocation, is told it worked, and nothing is revoked. It is not remotely exploitable on its own: an attacker needs the client token, which lives in the app's container. But the control silently does not do what it says.

Code sample

Code sample
// Any sign-out reproduces it; this is the whole surface.
await authState.signOut();

// then, out of band:
//   GET https://api.clerk.com/v1/sessions/<session_id>
//   -> {"status": "active", ...}

Logs

Logs

Captured with an HttpService decorator. Note auth=NONE on the DELETE:

CLERKHTTP #39 --> GET    /v1/client                          auth=yes(518)
CLERKHTTP #39 <-- 200    /v1/client   user=1sessions
CLERKHTTP #40 --> DELETE /v1/client                          auth=NONE
CLERKHTTP #40 <-- 200    /v1/client
CLERKHTTP #41 --> GET    /v1/client                          auth=NONE
CLERKHTTP #41 <-- 200    /v1/client   user=0sessions      <- a NEW, empty client

Every other request in the session carries auth=yes(518). Only the DELETE does not.

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.6 25G72 darwin-arm64, locale en-US) [461ms]
    • Flutter version 3.44.8 on channel stable at /opt/homebrew/share/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 058e0af2c2 (5 weeks ago), 2026-07-23 10:56:21 -0700
    • Engine revision 0cd610717b
    • Dart version 3.12.2
    • DevTools version 2.57.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✗] Android toolchain - develop for Android devices [411ms]
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/to/macos-android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, please use
      `flutter config --android-sdk` to update to that location.
[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [952ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17F113
    • CocoaPods version 1.17.0
[✓] Chrome - develop for the web [4ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (4 available) [6.3s]
    • My iPhone (mobile)    • <redacted>            • ios            • iOS 26.6 23G71
    • iPhone 17 Pro Max (mobile) • <redacted> • ios            • com.apple.CoreSimulator.SimRuntime.iOS-26-5 (simulator)
    • macOS (desktop)            • macos                                • darwin-arm64   • macOS 26.6 25G72 darwin-arm64
    • Chrome (web)               • chrome                               • web-javascript • Google Chrome 151.0.7922.71
[✓] Network resources [403ms]
    • All expected network resources are available.
! Doctor found issues in 1 category.

Suggested fix

Build the headers before clearing the cache, and move the clear into a finally so local credentials are still dropped when the request fails — a sign-out that cannot reach the network must still sign the user out of that device.

Implemented with tests against 0.0.18-beta: https://github.com/bytefoo/clerk-sdk-flutter/tree/fix/authenticate-delete-requests

  • a regression test asserting the sign-out DELETE carries the client token (confirmed failing before the fix: Expected: <true> Actual: <null>)
  • a test that local state is still cleared when the request fails
  • clerk_auth suite green: 647 passing

Possibly related

#427 reports that on web the session remains active after Sign out, "verified by continuing to call our backend with fresh session tokens". That symptom is what this bug produces on every platform, so the two may be separate causes with a shared outcome — the hit-testing problem described there would be distinct from this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions