@supabase/gotrue-js 2.119.0-canary.3 → 2.119.0-canary.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ # `signOut` keeps the session when the server logout fails
2
+
3
+ **Since:** the first release after v2.118.0
4
+ **Action required by:** none in v2.x. See [Planned for v3](#planned-for-v3).
5
+
6
+ `signOut({ scope: 'global' })` and `signOut({ scope: 'others' })` now report a server or network failure without touching the session they were asked to revoke. `signOut({ scope: 'local' })` always signs out this device.
7
+
8
+ ## What changed
9
+
10
+ - **`global` (the default scope):** the local session is cleared, and `SIGNED_OUT` fires, only after the server accepts the logout. If the request fails, for example because the device is offline, the local session is kept, no `SIGNED_OUT` event fires and the error is returned.
11
+ - **`local`:** the local session is removed and `SIGNED_OUT` fires before the request to revoke the refresh token is sent, so signing out of this device never depends on the network. The request's error, if any, is still returned.
12
+ - **`others`:** the local session is never changed. A failed request returns the error.
13
+
14
+ Previously, a failed `global` sign out still cleared the local session. The refresh token then stayed valid on the server and the client no longer had the credentials to retry the revoke.
15
+
16
+ ## Who is affected
17
+
18
+ **Callers that use the default scope** (`signOut()` or `signOut({ scope: 'global' })`) and assume it always ends the local session. If the device is offline or the auth server returns an error, `signOut()` now returns `{ error }` and the user stays signed in.
19
+
20
+ To always sign out this device, either use the `local` scope, or retry with it when `global` fails:
21
+
22
+ ```ts
23
+ const { error } = await supabase.auth.signOut()
24
+ if (error) {
25
+ // The server could not revoke the sessions and the user is still signed in locally.
26
+ // Sign out this device regardless:
27
+ await supabase.auth.signOut({ scope: 'local' })
28
+ }
29
+ ```
30
+
31
+ If you only want to sign out the current tab or device, pass `{ scope: 'local' }` directly. This is usually what a "Sign out" button wants.
32
+
33
+ ## Responses that still count as a successful sign out
34
+
35
+ A `401`, `403` or `404` response from the auth server is treated as "already signed out": the local session is cleared and `error` is `null`. Auth servers from v2.145.0 answer `403` with `user_not_found` (and from v2.150.0 `session_not_found`) when the user or session no longer exists, and servers before v2.145.0 answered `401` or `404`.
36
+
37
+ The status is also `403` for `bad_jwt` and `user_banned`, where nothing was revoked, so these are treated as a successful sign out too. This is known, and changing it would be a breaking change.
38
+
39
+ ## Planned for v3
40
+
41
+ v3 is expected to match on the error code instead of the status: only `session_not_found` and `user_not_found` will count as already signed out, and every other error will be returned with the session kept. Callers that handle the `error` from `signOut()` as shown above need no change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/gotrue-js",
3
- "version": "2.119.0-canary.3",
3
+ "version": "2.119.0-canary.4",
4
4
  "private": false,
5
5
  "description": "Official SDK for Supabase Auth",
6
6
  "keywords": [
@@ -4026,12 +4026,31 @@ export default class GoTrueClient {
4026
4026
  }
4027
4027
 
4028
4028
  /**
4029
- * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
4029
+ * Inside a browser context, `signOut()` will remove the logged in user from the browser session
4030
+ * and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"`
4031
+ * event.
4030
4032
  *
4031
- * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
4032
- * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
4033
+ * For server-side management, you can revoke all refresh tokens for a user by passing a user's
4034
+ * JWT through to `auth.api.signOut(JWT: string)`.
4033
4035
  *
4034
- * If using `others` scope, no `SIGNED_OUT` event is fired!
4036
+ * There is no way to revoke a user's access token jwt until it expires. It is recommended to set
4037
+ * a shorter expiry on the jwt for this reason.
4038
+ *
4039
+ * `signOut` behavior depends on the selected scope:
4040
+ *
4041
+ * - `local`: The local session is removed first and a `SIGNED_OUT` event is fired, then the client
4042
+ * makes a best-effort request to revoke the refresh token on the server. If the request fails,
4043
+ * the client is still signed out locally and the error is returned, but the server may retain
4044
+ * the refresh token until it expires.
4045
+ *
4046
+ * - `global`: All of the user's sessions are first invalidated on the server. Once the server
4047
+ * acknowledges the request, the local session is cleared. If server logout fails, the local
4048
+ * session is retained, no `SIGNED_OUT` event is triggered, and the server or network error is
4049
+ * returned. A 401, 403, or 404 response counts as a successful sign-out.
4050
+ *
4051
+ * - `others`: All other sessions are invalidated when the server accepts the request; the
4052
+ * current local session remains unchanged. In case of server or network failures, the error is
4053
+ * returned. A `SIGNED_OUT` event is never triggered, regardless of the outcome.
4035
4054
  *
4036
4055
  * **Warning:** the default `scope` is `'global'`. This signs the user out of
4037
4056
  * **every device they are currently signed in on**, not just the current
@@ -4049,7 +4068,13 @@ export default class GoTrueClient {
4049
4068
  * usually what apps want on a "Sign out" button, especially when users
4050
4069
  * sign in from multiple devices and do not expect signing out of one to
4051
4070
  * terminate the others.
4052
- * - Since Supabase Auth uses JWTs for authentication, the access token JWT will be valid until it's expired. When the user signs out, Supabase revokes the refresh token and deletes the JWT from the client-side. This does not revoke the JWT and it will still be valid until it expires.
4071
+ * - With the default `global` scope, `signOut()` while offline returns an error and keeps the user
4072
+ * signed in. To sign out regardless, fall back to `signOut({ scope: 'local' })` when the call
4073
+ * returns an error.
4074
+ * - Since Supabase Auth uses JWTs for authentication, the access token JWT will be valid until
4075
+ * it's expired. When the user signs out, Supabase revokes the refresh token and deletes the
4076
+ * JWT from the client-side. This does not revoke the JWT and it will still be valid until it
4077
+ * expires.
4053
4078
  *
4054
4079
  * @example Sign out of every device (global – default)
4055
4080
  * ```js
@@ -4079,41 +4104,78 @@ export default class GoTrueClient {
4079
4104
  return await this._signOut(options)
4080
4105
  }
4081
4106
 
4082
- protected async _signOut(
4083
- { scope }: SignOut = { scope: 'global' }
4084
- ): Promise<{ error: AuthError | null }> {
4107
+ protected async _signOut({ scope = 'global' }: SignOut = {}): Promise<{
4108
+ error: AuthError | null
4109
+ }> {
4085
4110
  return await this._useSession(async (result) => {
4086
4111
  const removeCurrentSession = async () => {
4087
4112
  await this._removeSession()
4088
4113
  }
4089
4114
  const { data, error: sessionError } = result
4090
4115
  if (sessionError && !isAuthSessionMissingError(sessionError)) {
4116
+ // A retryable refresh failure (offline, 5xx) leaves the session in storage and no access
4117
+ // token to revoke with, so `local` still has to sign out this device. A rejected refresh
4118
+ // has already removed the session and fired `SIGNED_OUT`.
4119
+ if (scope === 'local' && isAuthRetryableFetchError(sessionError)) {
4120
+ await removeCurrentSession()
4121
+ }
4091
4122
  return this._returnResult({ error: sessionError })
4092
4123
  }
4093
4124
  const accessToken = data.session?.access_token
4125
+ let removeError: { thrown: unknown } | null = null
4126
+ const finish = <T>(value: T): T => {
4127
+ if (removeError) {
4128
+ throw removeError.thrown
4129
+ }
4130
+ return value
4131
+ }
4132
+ if (scope === 'local') {
4133
+ // The local session is removed before the request is sent, so signing out of this device
4134
+ // never depends on the network. The request only revokes the refresh token on the server.
4135
+ // A throwing `SIGNED_OUT` subscriber must not skip that request, so its error is rethrown
4136
+ // once the request has been attempted.
4137
+ try {
4138
+ await removeCurrentSession()
4139
+ } catch (thrown) {
4140
+ removeError = { thrown }
4141
+ }
4142
+ }
4094
4143
  if (accessToken) {
4095
4144
  const { error } = await this.admin.signOut(accessToken, scope)
4096
- if (error) {
4097
- // ignore 404s since user might not exist anymore
4098
- // ignore 401s since an invalid or expired JWT should sign out the current session
4099
- if (
4100
- !(
4101
- (isAuthApiError(error) &&
4102
- (error.status === 404 || error.status === 401 || error.status === 403)) ||
4103
- isAuthSessionMissingError(error)
4104
- )
4105
- ) {
4106
- if (scope !== 'others') {
4107
- await removeCurrentSession()
4108
- }
4109
- return this._returnResult({ error })
4110
- }
4145
+ // The current behavior is wrong for the current version of auth (v2.197.0). However,
4146
+ // changing it could be considered a breaking change and must wait for v3. Before modifying
4147
+ // this code we also need to consider which range of Auth versions we aim to support. Right
4148
+ // now this is not defined and thus undefined.
4149
+ //
4150
+ // Some constraints to be aware of:
4151
+ // - Since supabase/auth v2.145.0 a missing user answers 403 `user_not_found`.
4152
+ // - Since supabase/auth v2.150.0 a missing session answers 403 `session_not_found`.
4153
+ // - Before v2.145.0 an invalid JWT answered 401.
4154
+ // - Before v2.145.0 a missing user answers 404., so those statuses are treated the same way.
4155
+ //
4156
+ // TODO(v3): if we only support auth v2.150.0 or later, change this to:
4157
+ // - Match on `error.code` instead of `error.status`, and treat only 403 `session_not_found`
4158
+ // and `user_not_found` as already signed out. Drop the 401 and 404 handling.
4159
+ // - Return every other error, e.g. 403 `bad_jwt` or `user_banned`, or a 401 or 404 from a
4160
+ // gateway, instead of treating it as signed out. Today nothing was revoked, yet the
4161
+ // session is cleared and `error` is null. With `global` the session is then kept.
4162
+ // - For `others`, return `session_not_found` as well. It describes the current session, not
4163
+ // the sessions being revoked.
4164
+ if (
4165
+ error &&
4166
+ !(
4167
+ (isAuthApiError(error) &&
4168
+ (error.status === 404 || error.status === 401 || error.status === 403)) ||
4169
+ isAuthSessionMissingError(error)
4170
+ )
4171
+ ) {
4172
+ return finish(this._returnResult({ error }))
4111
4173
  }
4112
4174
  }
4113
- if (scope !== 'others') {
4175
+ if (scope === 'global') {
4114
4176
  await removeCurrentSession()
4115
4177
  }
4116
- return this._returnResult({ error: null })
4178
+ return finish(this._returnResult({ error: null }))
4117
4179
  })
4118
4180
  }
4119
4181
 
@@ -4,4 +4,4 @@
4
4
  // - Debugging and support (identifying which version is running)
5
5
  // - Telemetry and logging (version reporting in errors/analytics)
6
6
  // - Ensuring build artifacts match the published package version
7
- export const version = '2.119.0-canary.3'
7
+ export const version = '2.119.0-canary.4'