Skip to content

Commit

Permalink
feat: consider session expired with margin on getSession() without au…
Browse files Browse the repository at this point in the history
…to refresh (#1027)

When `autoRefreshToken` is off (or when a tab is in the background) but
`getSession()` is called -- such as in an active Realtime channel,
`getSession()` might return a JWT which will expire while the message is
travelling over the internet. There is one confirmed case of this
happening.

This PR adjusts this using the established `EXPIRY_MARGIN_MS` constant
(which only applies on initial initialization of the client). The
constant's value is brought in line with the `autoRefreshToken` ticks
which run every 30 seconds and refreshing is attempted 3 ticks prior to
the session expiring.

This means that JWTs with an expiry value **less than 90s** will always
refresh the session; which is acceptable.
  • Loading branch information
hf authored Jan 21, 2025
1 parent 9748dd9 commit 80f88e4
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 18 deletions.
39 changes: 22 additions & 17 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import GoTrueAdminApi from './GoTrueAdminApi'
import { DEFAULT_HEADERS, EXPIRY_MARGIN, GOTRUE_URL, STORAGE_KEY } from './lib/constants'
import {
DEFAULT_HEADERS,
EXPIRY_MARGIN_MS,
AUTO_REFRESH_TICK_DURATION_MS,
AUTO_REFRESH_TICK_THRESHOLD,
GOTRUE_URL,
STORAGE_KEY,
} from './lib/constants'
import {
AuthError,
AuthImplicitGrantRedirectError,
Expand Down Expand Up @@ -109,13 +116,6 @@ const DEFAULT_OPTIONS: Omit<Required<GoTrueClientOptions>, 'fetch' | 'storage' |
hasCustomAuthorizationHeader: false,
}

/** Current session will be checked for refresh at this interval. */
const AUTO_REFRESH_TICK_DURATION = 30 * 1000

/**
* A token refresh will be attempted this many ticks before the current session expires. */
const AUTO_REFRESH_TICK_THRESHOLD = 3

async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
return await fn()
}
Expand Down Expand Up @@ -1107,8 +1107,13 @@ export default class GoTrueClient {
return { data: { session: null }, error: null }
}

// A session is considered expired before the access token _actually_
// expires. When the autoRefreshToken option is off (or when the tab is
// in the background), very eager users of getSession() -- like
// realtime-js -- might send a valid JWT which will expire by the time it
// reaches the server.
const hasExpired = currentSession.expires_at
? currentSession.expires_at <= Date.now() / 1000
? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
: false

this._debug(
Expand Down Expand Up @@ -1503,7 +1508,7 @@ export default class GoTrueClient {
}

const actuallyExpiresIn = expiresAt - timeNow
if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION) {
if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
console.warn(
`@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
)
Expand Down Expand Up @@ -1850,7 +1855,7 @@ export default class GoTrueClient {
error &&
isAuthRetryableFetchError(error) &&
// retryable only if the request can be sent before the backoff overflows the tick duration
Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION
Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
)
}
)
Expand Down Expand Up @@ -1923,12 +1928,12 @@ export default class GoTrueClient {
return
}

const timeNow = Math.round(Date.now() / 1000)
const expiresWithMargin = (currentSession.expires_at ?? Infinity) < timeNow + EXPIRY_MARGIN
const expiresWithMargin =
(currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS

this._debug(
debugName,
`session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN}s`
`session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
)

if (expiresWithMargin) {
Expand Down Expand Up @@ -2101,7 +2106,7 @@ export default class GoTrueClient {

this._debug('#_startAutoRefresh()')

const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION)
const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
this.autoRefreshTicker = ticker

if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
Expand Down Expand Up @@ -2208,12 +2213,12 @@ export default class GoTrueClient {

// session will expire in this many ticks (or has already expired if <= 0)
const expiresInTicks = Math.floor(
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
)

this._debug(
'#_autoRefreshTokenTick()',
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
)

if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
Expand Down
14 changes: 13 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { version } from './version'

/** Current session will be checked for refresh at this interval. */
export const AUTO_REFRESH_TICK_DURATION_MS = 30 * 1000

/**
* A token refresh will be attempted this many ticks before the current session expires. */
export const AUTO_REFRESH_TICK_THRESHOLD = 3

/*
* Earliest time before an access token expires that the session should be refreshed.
*/
export const EXPIRY_MARGIN_MS = AUTO_REFRESH_TICK_THRESHOLD * AUTO_REFRESH_TICK_DURATION_MS

export const GOTRUE_URL = 'http://localhost:9999'
export const STORAGE_KEY = 'supabase.auth.token'
export const AUDIENCE = ''
export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` }
export const EXPIRY_MARGIN = 10 // in seconds
export const NETWORK_FAILURE = {
MAX_RETRIES: 10,
RETRY_INTERVAL: 2, // in deciseconds
Expand Down

0 comments on commit 80f88e4

Please sign in to comment.