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
- Sign in on any platform.
- Call
signOut() (or tap Sign out in any Clerk UI).
- Note the app returns to the signed-out state and reports success.
- 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.
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._deleteclears the token cache before building the request:_headersattachesAuthorizationonlyif (_tokenCache.hasClientToken), and_queryParamsreadssessionIdfrom the same cache. SoDELETE /v1/clientidentifies neither client nor session. The back end answers 200 to it — there is nothing to revoke for an unidentified client — andsignOutreturnsClient.emptyregardless of the outcome, so the caller sees success.deleteUser()goes through the same helper viaDELETE /meand is worse off:requiresSessionId: truemeans it also depends on thesessionIdcleared on that same line. It reports deleting an account that has not been deleted.Steps to reproduce
signOut()(or tap Sign out in any Clerk UI).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 originalabandon_at(a month out, in our case). Observed on a release build on a physical iPhone and again on the Simulator.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
Logs
Logs
Captured with an
HttpServicedecorator. Noteauth=NONEon the DELETE:Every other request in the session carries
auth=yes(518). Only the DELETE does not.Flutter Doctor output
Doctor output
Suggested fix
Build the headers before clearing the cache, and move the clear into a
finallyso 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-requestsExpected: <true> Actual: <null>)clerk_authsuite green: 647 passingPossibly 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.