@supabase/gotrue-js 2.15.0 → 2.17.0

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.
@@ -23,9 +23,12 @@ import {
23
23
  uuid,
24
24
  retryable,
25
25
  sleep,
26
+ generatePKCEVerifier,
27
+ generatePKCEChallenge,
26
28
  } from './lib/helpers'
27
29
  import localStorageAdapter from './lib/local-storage'
28
30
  import { polyfillGlobalThis } from './lib/polyfills'
31
+
29
32
  import type {
30
33
  AuthChangeEvent,
31
34
  AuthResponse,
@@ -63,6 +66,7 @@ import type {
63
66
  AuthenticatorAssuranceLevels,
64
67
  Factor,
65
68
  MFAChallengeAndVerifyParams,
69
+ OAuthFlowType,
66
70
  } from './lib/types'
67
71
 
68
72
  polyfillGlobalThis() // Make "globalThis" available
@@ -378,14 +382,43 @@ export default class GoTrueClient {
378
382
  */
379
383
  async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
380
384
  await this._removeSession()
381
- return this._handleProviderSignIn(credentials.provider, {
385
+
386
+ return await this._handleProviderSignIn(credentials.provider, {
382
387
  redirectTo: credentials.options?.redirectTo,
383
388
  scopes: credentials.options?.scopes,
384
389
  queryParams: credentials.options?.queryParams,
385
390
  skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
391
+ flowType: credentials.options?.flowType ?? 'implicit',
386
392
  })
387
393
  }
388
394
 
395
+ /**
396
+ * Log in an existing user via a third-party provider.
397
+ */
398
+ async exchangeCodeForSession(authCode: string): Promise<AuthResponse> {
399
+ const codeVerifier = await getItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`)
400
+ const { data, error } = await _request(
401
+ this.fetch,
402
+ 'POST',
403
+ `${this.url}/token?grant_type=oauth_pkce`,
404
+ {
405
+ headers: this.headers,
406
+ body: {
407
+ auth_code: authCode,
408
+ code_verifier: codeVerifier,
409
+ },
410
+ xform: _sessionResponse,
411
+ }
412
+ )
413
+ await removeItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`)
414
+ if (error || !data) return { data: { user: null, session: null }, error }
415
+ if (data.session) {
416
+ await this._saveSession(data.session)
417
+ this._notifyAllSubscribers('SIGNED_IN', data.session)
418
+ }
419
+ return { data, error }
420
+ }
421
+
389
422
  /**
390
423
  * Allows signing in with an ID token issued by certain supported providers.
391
424
  * The ID token is verified for validity and a new session is established.
@@ -941,9 +974,26 @@ export default class GoTrueClient {
941
974
 
942
975
  this.stateChangeEmitters.set(id, subscription)
943
976
 
977
+ this.emitInitialSession(id)
978
+
944
979
  return { data: { subscription } }
945
980
  }
946
981
 
982
+ private async emitInitialSession(id: string): Promise<void> {
983
+ try {
984
+ const {
985
+ data: { session },
986
+ error,
987
+ } = await this.getSession()
988
+ if (error) throw error
989
+
990
+ this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
991
+ } catch (err) {
992
+ this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
993
+ console.error(err)
994
+ }
995
+ }
996
+
947
997
  /**
948
998
  * Sends a password reset request to an email address.
949
999
  * @param email The email address of the user.
@@ -1023,24 +1073,27 @@ export default class GoTrueClient {
1023
1073
  return isValidSession
1024
1074
  }
1025
1075
 
1026
- private _handleProviderSignIn(
1076
+ private async _handleProviderSignIn(
1027
1077
  provider: Provider,
1028
1078
  options: {
1029
1079
  redirectTo?: string
1030
1080
  scopes?: string
1031
1081
  queryParams?: { [key: string]: string }
1032
1082
  skipBrowserRedirect?: boolean
1033
- } = {}
1083
+ flowType: OAuthFlowType
1084
+ }
1034
1085
  ) {
1035
- const url: string = this._getUrlForProvider(provider, {
1086
+ const url: string = await this._getUrlForProvider(provider, {
1036
1087
  redirectTo: options.redirectTo,
1037
1088
  scopes: options.scopes,
1038
1089
  queryParams: options.queryParams,
1090
+ flowType: options.flowType,
1039
1091
  })
1040
1092
  // try to open on the browser
1041
1093
  if (isBrowser() && !options.skipBrowserRedirect) {
1042
1094
  window.location.assign(url)
1043
1095
  }
1096
+
1044
1097
  return { data: { provider, url }, error: null }
1045
1098
  }
1046
1099
 
@@ -1338,13 +1391,15 @@ export default class GoTrueClient {
1338
1391
  * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
1339
1392
  * @param options.scopes A space-separated list of scopes granted to the OAuth application.
1340
1393
  * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
1394
+ * @param options.flowType OAuth flow to use - defaults to implicit flow. PKCE is recommended for mobile and server-side applications.
1341
1395
  */
1342
- private _getUrlForProvider(
1396
+ private async _getUrlForProvider(
1343
1397
  provider: Provider,
1344
1398
  options: {
1345
1399
  redirectTo?: string
1346
1400
  scopes?: string
1347
1401
  queryParams?: { [key: string]: string }
1402
+ flowType: OAuthFlowType
1348
1403
  }
1349
1404
  ) {
1350
1405
  const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
@@ -1354,10 +1409,22 @@ export default class GoTrueClient {
1354
1409
  if (options?.scopes) {
1355
1410
  urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
1356
1411
  }
1412
+ if (options?.flowType === 'pkce') {
1413
+ const codeVerifier = generatePKCEVerifier()
1414
+ await setItemAsync(this.storage, `${this.storageKey}-oauth-code-verifier`, codeVerifier)
1415
+ const codeChallenge = await generatePKCEChallenge(codeVerifier)
1416
+ const flowParams = new URLSearchParams({
1417
+ flow_type: `${encodeURIComponent(options.flowType)}`,
1418
+ code_challenge: `${encodeURIComponent(codeChallenge)}`,
1419
+ code_challenge_method: `${encodeURIComponent('s256')}`,
1420
+ })
1421
+ urlParams.push(flowParams.toString())
1422
+ }
1357
1423
  if (options?.queryParams) {
1358
1424
  const query = new URLSearchParams(options.queryParams)
1359
1425
  urlParams.push(query.toString())
1360
1426
  }
1427
+
1361
1428
  return `${this.url}/authorize?${urlParams.join('&')}`
1362
1429
  }
1363
1430
 
@@ -1,5 +1,5 @@
1
1
  import { SupportedStorage } from './types'
2
-
2
+ import sha256CryptoJS from 'crypto-js/sha256'
3
3
  export function expiresAt(expiresIn: number) {
4
4
  const timeNow = Math.round(Date.now() / 1000)
5
5
  return timeNow + expiresIn
@@ -236,3 +236,44 @@ export function retryable<T>(
236
236
 
237
237
  return promise
238
238
  }
239
+
240
+ function dec2hex(dec: number) {
241
+ return ('0' + dec.toString(16)).substr(-2)
242
+ }
243
+
244
+ // Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs
245
+ export function generatePKCEVerifier() {
246
+ const verifierLength = 56
247
+ const array = new Uint32Array(verifierLength)
248
+ if (!isBrowser()) {
249
+ for (let i = 0; i < verifierLength; i++) {
250
+ array[i] = Math.floor(Math.random() * 256)
251
+ }
252
+ } else {
253
+ window.crypto.getRandomValues(array)
254
+ }
255
+ return Array.from(array, dec2hex).join('')
256
+ }
257
+
258
+ async function sha256(randomString: string) {
259
+ const encoder = new TextEncoder()
260
+ const encodedData = encoder.encode(randomString)
261
+ if (!isBrowser()) {
262
+ return sha256CryptoJS(randomString).toString()
263
+ }
264
+ const hash = await window.crypto.subtle.digest('SHA-256', encodedData)
265
+ const bytes = new Uint8Array(hash)
266
+
267
+ return Array.from(bytes)
268
+ .map((c) => String.fromCharCode(c))
269
+ .join('')
270
+ }
271
+
272
+ function base64urlencode(str: string) {
273
+ return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
274
+ }
275
+
276
+ export async function generatePKCEChallenge(verifier: string) {
277
+ const hashed = await sha256(verifier)
278
+ return base64urlencode(hashed)
279
+ }
package/src/lib/types.ts CHANGED
@@ -23,6 +23,7 @@ export type Provider =
23
23
  export type AuthChangeEventMFA = 'MFA_CHALLENGE_VERIFIED'
24
24
 
25
25
  export type AuthChangeEvent =
26
+ | 'INITIAL_SESSION'
26
27
  | 'PASSWORD_RECOVERY'
27
28
  | 'SIGNED_IN'
28
29
  | 'SIGNED_OUT'
@@ -428,6 +429,7 @@ export type SignInWithPasswordlessCredentials =
428
429
  }
429
430
  }
430
431
 
432
+ export type OAuthFlowType = 'implicit' | 'pkce'
431
433
  export type SignInWithOAuthCredentials = {
432
434
  /** One of the providers supported by GoTrue. */
433
435
  provider: Provider
@@ -440,6 +442,8 @@ export type SignInWithOAuthCredentials = {
440
442
  queryParams?: { [key: string]: string }
441
443
  /** If set to true does not immediately redirect the current browser context to visit the OAuth authorization page for the provider. */
442
444
  skipBrowserRedirect?: boolean
445
+ /** If set to 'pkce' PKCE flow. Defaults to the 'implicit' flow otherwise */
446
+ flowType?: OAuthFlowType
443
447
  }
444
448
  }
445
449
 
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '2.15.0'
2
+ export const version = '2.17.0'