Skip to content

Commit 6446c9e

Browse files
authored
Merge pull request #480 from supabase/j0_merge_rc_into_mfa
fix: merge rc into mfa
2 parents b93ff0c + 4f2ada3 commit 6446c9e

File tree

7 files changed

+235
-70
lines changed

7 files changed

+235
-70
lines changed

Diff for: src/GoTrueClient.ts

+67-28
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from './lib/errors'
1818
import { Fetch, _request, _sessionResponse, _userResponse } from './lib/fetch'
1919
import {
20+
decodeBase64URL,
2021
Deferred,
2122
getItemAsync,
2223
getParameterByName,
@@ -348,6 +349,7 @@ export default class GoTrueClient {
348349
headers: this.headers,
349350
body: {
350351
email,
352+
data: options?.data ?? {},
351353
create_user: options?.shouldCreateUser ?? true,
352354
gotrue_meta_security: { captcha_token: options?.captchaToken },
353355
},
@@ -361,6 +363,7 @@ export default class GoTrueClient {
361363
headers: this.headers,
362364
body: {
363365
phone,
366+
data: options?.data ?? {},
364367
create_user: options?.shouldCreateUser ?? true,
365368
gotrue_meta_security: { captcha_token: options?.captchaToken },
366369
},
@@ -496,7 +499,7 @@ export default class GoTrueClient {
496499
}
497500

498501
// Default to Authorization header if there is no existing session
499-
jwt = data.session?.access_token ?? this.headers['Authorization']
502+
jwt = data.session?.access_token ?? undefined
500503
}
501504

502505
return await _request(this.fetch, 'GET', `${this.url}/user`, {
@@ -548,28 +551,59 @@ export default class GoTrueClient {
548551
}
549552

550553
/**
551-
* Sets the session data from refresh token and returns current session or an error if the refresh token is invalid.
552-
* @param refresh_token A refresh token returned by supabase auth.
554+
* Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
555+
* If the refresh token in the current session is invalid and the current session has expired, an error will be thrown.
556+
* If the current session does not contain at expires_at field, setSession will use the exp claim defined in the access token.
557+
* @param currentSession The current session that minimally contains an access token, refresh token and a user.
553558
*/
554-
async setSession(refresh_token: string): Promise<AuthResponse> {
559+
async setSession(
560+
currentSession: Pick<Session, 'access_token' | 'refresh_token'>
561+
): Promise<AuthResponse> {
555562
try {
556-
if (!refresh_token) {
557-
throw new AuthSessionMissingError()
558-
}
559-
const { data, error } = await this._refreshAccessToken(refresh_token)
560-
if (error) {
561-
return { data: { session: null, user: null }, error: error }
563+
const timeNow = Date.now() / 1000
564+
let expiresAt = timeNow
565+
let hasExpired = true
566+
let session: Session | null = null
567+
if (currentSession.access_token && currentSession.access_token.split('.')[1]) {
568+
const payload = JSON.parse(decodeBase64URL(currentSession.access_token.split('.')[1]))
569+
if (payload.exp) {
570+
expiresAt = payload.exp
571+
hasExpired = expiresAt <= timeNow
572+
}
562573
}
563574

564-
if (!data.session) {
565-
return { data: { session: null, user: null }, error: null }
566-
}
575+
if (hasExpired) {
576+
if (!currentSession.refresh_token) {
577+
throw new AuthSessionMissingError()
578+
}
579+
const { data, error } = await this._refreshAccessToken(currentSession.refresh_token)
580+
if (error) {
581+
return { data: { session: null, user: null }, error: error }
582+
}
567583

568-
await this._saveSession(data.session)
584+
if (!data.session) {
585+
return { data: { session: null, user: null }, error: null }
586+
}
587+
session = data.session
588+
} else {
589+
const { data, error } = await this.getUser(currentSession.access_token)
590+
if (error) {
591+
throw error
592+
}
593+
session = {
594+
access_token: currentSession.access_token,
595+
refresh_token: currentSession.refresh_token,
596+
user: data.user,
597+
token_type: 'bearer',
598+
expires_in: expiresAt - timeNow,
599+
expires_at: expiresAt,
600+
}
601+
}
569602

570-
this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
603+
await this._saveSession(session)
604+
this._notifyAllSubscribers('TOKEN_REFRESHED', session)
571605

572-
return { data, error: null }
606+
return { data: { session, user: session.user }, error: null }
573607
} catch (error) {
574608
if (isAuthError(error)) {
575609
return { data: { session: null, user: null }, error }
@@ -606,6 +640,7 @@ export default class GoTrueClient {
606640
}
607641

608642
const provider_token = getParameterByName('provider_token')
643+
const provider_refresh_token = getParameterByName('provider_refresh_token')
609644
const access_token = getParameterByName('access_token')
610645
if (!access_token) throw new AuthImplicitGrantRedirectError('No access_token detected.')
611646
const expires_in = getParameterByName('expires_in')
@@ -623,6 +658,7 @@ export default class GoTrueClient {
623658
const user: User = data.user
624659
const session: Session = {
625660
provider_token,
661+
provider_refresh_token,
626662
access_token,
627663
expires_in: parseInt(expires_in),
628664
expires_at,
@@ -893,7 +929,7 @@ export default class GoTrueClient {
893929
const timeNow = Math.round(Date.now() / 1000)
894930
const expiresIn = expiresAt - timeNow
895931
const refreshDurationBeforeExpires = expiresIn > EXPIRY_MARGIN ? EXPIRY_MARGIN : 0.5
896-
this._startAutoRefreshToken((expiresIn - refreshDurationBeforeExpires) * 1000, session)
932+
this._startAutoRefreshToken((expiresIn - refreshDurationBeforeExpires) * 1000)
897933
}
898934

899935
if (this.persistSession && session.expires_at) {
@@ -922,22 +958,25 @@ export default class GoTrueClient {
922958
* @param value time intervals in milliseconds.
923959
* @param session The current session.
924960
*/
925-
private _startAutoRefreshToken(value: number, session: Session) {
961+
private _startAutoRefreshToken(value: number) {
926962
if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer)
927963
if (value <= 0 || !this.autoRefreshToken) return
928964

929965
this.refreshTokenTimer = setTimeout(async () => {
930966
this.networkRetries++
931-
const { error } = await this._callRefreshToken(session.refresh_token)
932-
if (!error) this.networkRetries = 0
933-
if (
934-
error instanceof AuthRetryableFetchError &&
935-
this.networkRetries < NETWORK_FAILURE.MAX_RETRIES
936-
)
937-
this._startAutoRefreshToken(
938-
NETWORK_FAILURE.RETRY_INTERVAL ** this.networkRetries * 100,
939-
session
940-
) // exponential backoff
967+
const {
968+
data: { session },
969+
error: sessionError,
970+
} = await this.getSession()
971+
if (!sessionError && session) {
972+
const { error } = await this._callRefreshToken(session.refresh_token)
973+
if (!error) this.networkRetries = 0
974+
if (
975+
error instanceof AuthRetryableFetchError &&
976+
this.networkRetries < NETWORK_FAILURE.MAX_RETRIES
977+
)
978+
this._startAutoRefreshToken(NETWORK_FAILURE.RETRY_INTERVAL ** this.networkRetries * 100) // exponential backoff
979+
}
941980
}, value)
942981
if (typeof this.refreshTokenTimer.unref === 'function') this.refreshTokenTimer.unref()
943982
}

Diff for: src/lib/fetch.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ const _getErrorMessage = (err: any): string =>
2727
err.msg || err.message || err.error_description || err.error || JSON.stringify(err)
2828

2929
const handleError = async (error: unknown, reject: (reason?: any) => void) => {
30+
const NETWORK_ERROR_CODES = [502, 503, 504]
3031
if (!looksLikeFetchResponse(error)) {
3132
reject(new AuthRetryableFetchError(_getErrorMessage(error), 0))
32-
} else if (error.status >= 500 && error.status <= 599) {
33+
} else if (NETWORK_ERROR_CODES.includes(error.status)) {
3334
// status in 500...599 range - server had an error, request might be retryed.
3435
reject(new AuthRetryableFetchError(_getErrorMessage(error), error.status))
3536
} else {

Diff for: src/lib/helpers.ts

+18
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,24 @@ export const removeItemAsync = async (storage: SupportedStorage, key: string): P
7878
await storage.removeItem(key)
7979
}
8080

81+
export const decodeBase64URL = (value: string): string => {
82+
try {
83+
// atob is present in all browsers and nodejs >= 16
84+
// but if it is not it will throw a ReferenceError in which case we can try to use Buffer
85+
// replace are here to convert the Base64-URL into Base64 which is what atob supports
86+
// replace with //g regex acts like replaceAll
87+
return atob(value.replace(/[-]/g, '+').replace(/[_]/g, '/'))
88+
} catch (e) {
89+
if (e instanceof ReferenceError) {
90+
// running on nodejs < 16
91+
// Buffer supports Base64-URL transparently
92+
return Buffer.from(value, 'base64').toString('utf-8')
93+
} else {
94+
throw e
95+
}
96+
}
97+
}
98+
8199
/**
82100
* A deferred represents some asynchronous work that is not yet finished, which
83101
* may or may not culminate in a value.

Diff for: src/lib/types.ts

+43-7
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,15 @@ export type UserResponse =
139139
}
140140

141141
export interface Session {
142+
/**
143+
* The oauth provider token. If present, this can be used to make external API requests to the oauth provider used.
144+
*/
142145
provider_token?: string | null
146+
/**
147+
* The oauth provider refresh token. If present, this can be used to refresh the provider_token via the oauth provider's API.
148+
* Not all oauth providers return a provider refresh token. If the provider_refresh_token is missing, please refer to the oauth provider's documentation for information on how to obtain the provider refresh token.
149+
*/
150+
provider_refresh_token?: string | null
143151
/**
144152
* The access token jwt. It is recommended to set the JWT_EXPIRY to a shorter expiry value.
145153
*/
@@ -187,15 +195,19 @@ export interface Factor {
187195
factor_type: string
188196
}
189197

198+
export interface UserAppMetadata {
199+
provider?: string
200+
[key: string]: any
201+
}
202+
203+
export interface UserMetadata {
204+
[key: string]: any
205+
}
206+
190207
export interface User {
191208
id: string
192-
app_metadata: {
193-
provider?: string
194-
[key: string]: any
195-
}
196-
user_metadata: {
197-
[key: string]: any
198-
}
209+
app_metadata: UserAppMetadata
210+
user_metadata: UserMetadata
199211
aud: string
200212
confirmation_sent_at?: string
201213
recovery_sent_at?: string
@@ -278,6 +290,18 @@ export interface AdminUserAttributes extends UserAttributes {
278290
* Only a service role can modify.
279291
*/
280292
phone_confirm?: boolean
293+
294+
/**
295+
* Determines how long a user is banned for.
296+
*
297+
* The format for the ban duration follows a strict sequence of decimal numbers with a unit suffix.
298+
* Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
299+
*
300+
* For example, some possible durations include: '300ms', '2h45m'.
301+
*
302+
* Setting the ban duration to 'none' lifts the ban on the user.
303+
*/
304+
ban_duration?: string | 'none'
281305
}
282306

283307
export interface Subscription {
@@ -365,6 +389,12 @@ export type SignInWithPasswordlessCredentials =
365389
emailRedirectTo?: string
366390
/** If set to false, this method will not create a new user. Defaults to true. */
367391
shouldCreateUser?: boolean
392+
/**
393+
* A custom data object to store the user's metadata. This maps to the `auth.users.user_metadata` column.
394+
*
395+
* The `data` should be a JSON object that includes user-specific info, such as their first and last name.
396+
*/
397+
data?: object
368398
/** Verification token received when the user completes the captcha on the site. */
369399
captchaToken?: string
370400
}
@@ -375,6 +405,12 @@ export type SignInWithPasswordlessCredentials =
375405
options?: {
376406
/** If set to false, this method will not create a new user. Defaults to true. */
377407
shouldCreateUser?: boolean
408+
/**
409+
* A custom data object to store the user's metadata. This maps to the `auth.users.user_metadata` column.
410+
*
411+
* The `data` should be a JSON object that includes user-specific info, such as their first and last name.
412+
*/
413+
data?: object
378414
/** Verification token received when the user completes the captcha on the site. */
379415
captchaToken?: string
380416
}

0 commit comments

Comments
 (0)