@supabase/gotrue-js 2.0.1 → 2.2.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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/main/GoTrueAdminApi.d.ts +6 -1
  3. package/dist/main/GoTrueAdminApi.d.ts.map +1 -1
  4. package/dist/main/GoTrueAdminApi.js +40 -2
  5. package/dist/main/GoTrueAdminApi.js.map +1 -1
  6. package/dist/main/GoTrueClient.d.ts +43 -1
  7. package/dist/main/GoTrueClient.d.ts.map +1 -1
  8. package/dist/main/GoTrueClient.js +207 -3
  9. package/dist/main/GoTrueClient.js.map +1 -1
  10. package/dist/main/index.d.ts.map +1 -1
  11. package/dist/main/index.js.map +1 -1
  12. package/dist/main/lib/helpers.d.ts +1 -0
  13. package/dist/main/lib/helpers.d.ts.map +1 -1
  14. package/dist/main/lib/helpers.js +11 -1
  15. package/dist/main/lib/helpers.js.map +1 -1
  16. package/dist/main/lib/types.d.ts +378 -1
  17. package/dist/main/lib/types.d.ts.map +1 -1
  18. package/dist/main/lib/version.d.ts +1 -1
  19. package/dist/main/lib/version.js +1 -1
  20. package/dist/module/GoTrueAdminApi.d.ts +6 -1
  21. package/dist/module/GoTrueAdminApi.d.ts.map +1 -1
  22. package/dist/module/GoTrueAdminApi.js +40 -2
  23. package/dist/module/GoTrueAdminApi.js.map +1 -1
  24. package/dist/module/GoTrueClient.d.ts +43 -1
  25. package/dist/module/GoTrueClient.d.ts.map +1 -1
  26. package/dist/module/GoTrueClient.js +208 -4
  27. package/dist/module/GoTrueClient.js.map +1 -1
  28. package/dist/module/index.d.ts.map +1 -1
  29. package/dist/module/index.js.map +1 -1
  30. package/dist/module/lib/helpers.d.ts +1 -0
  31. package/dist/module/lib/helpers.d.ts.map +1 -1
  32. package/dist/module/lib/helpers.js +9 -0
  33. package/dist/module/lib/helpers.js.map +1 -1
  34. package/dist/module/lib/types.d.ts +378 -1
  35. package/dist/module/lib/types.d.ts.map +1 -1
  36. package/dist/module/lib/version.d.ts +1 -1
  37. package/dist/module/lib/version.js +1 -1
  38. package/package.json +2 -2
  39. package/src/GoTrueAdminApi.ts +63 -3
  40. package/src/GoTrueClient.ts +259 -4
  41. package/src/index.ts +0 -1
  42. package/src/lib/helpers.ts +11 -0
  43. package/src/lib/types.ts +425 -0
  44. package/src/lib/version.ts +1 -1
@@ -17,7 +17,6 @@ import {
17
17
  } from './lib/errors'
18
18
  import { Fetch, _request, _sessionResponse, _userResponse } from './lib/fetch'
19
19
  import {
20
- decodeBase64URL,
21
20
  Deferred,
22
21
  getItemAsync,
23
22
  getParameterByName,
@@ -26,6 +25,7 @@ import {
26
25
  resolveFetch,
27
26
  setItemAsync,
28
27
  uuid,
28
+ decodeJWTPayload,
29
29
  } from './lib/helpers'
30
30
  import localStorageAdapter from './lib/local-storage'
31
31
  import { polyfillGlobalThis } from './lib/polyfills'
@@ -48,6 +48,21 @@ import type {
48
48
  UserAttributes,
49
49
  UserResponse,
50
50
  VerifyOtpParams,
51
+ GoTrueMFAApi,
52
+ MFAEnrollParams,
53
+ AuthMFAEnrollResponse,
54
+ MFAChallengeParams,
55
+ AuthMFAChallengeResponse,
56
+ MFAUnenrollParams,
57
+ AuthMFAUnenrollResponse,
58
+ MFAVerifyParams,
59
+ AuthMFAVerifyResponse,
60
+ AuthMFAListFactorsResponse,
61
+ AMREntry,
62
+ AuthMFAGetAuthenticatorAssuranceLevelResponse,
63
+ AuthenticatorAssuranceLevels,
64
+ Factor,
65
+ MFAChallengeAndVerifyParams,
51
66
  } from './lib/types'
52
67
 
53
68
  polyfillGlobalThis() // Make "globalThis" available
@@ -67,6 +82,10 @@ export default class GoTrueClient {
67
82
  * These methods should only be used in a trusted server-side environment.
68
83
  */
69
84
  admin: GoTrueAdminApi
85
+ /**
86
+ * Namespace for the MFA methods.
87
+ */
88
+ mfa: GoTrueMFAApi
70
89
  /**
71
90
  * The storage key used to identify the values saved in localStorage
72
91
  */
@@ -121,6 +140,15 @@ export default class GoTrueClient {
121
140
  this.detectSessionInUrl = settings.detectSessionInUrl
122
141
 
123
142
  this.initialize()
143
+ this.mfa = {
144
+ verify: this._verify.bind(this),
145
+ enroll: this._enroll.bind(this),
146
+ unenroll: this._unenroll.bind(this),
147
+ challenge: this._challenge.bind(this),
148
+ listFactors: this._listFactors.bind(this),
149
+ challengeAndVerify: this._challengeAndVerify.bind(this),
150
+ getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
151
+ }
124
152
  }
125
153
 
126
154
  /**
@@ -204,7 +232,7 @@ export default class GoTrueClient {
204
232
  body: {
205
233
  email,
206
234
  password,
207
- data: options?.data,
235
+ data: options?.data ?? {},
208
236
  gotrue_meta_security: { captcha_token: options?.captchaToken },
209
237
  },
210
238
  xform: _sessionResponse,
@@ -216,7 +244,7 @@ export default class GoTrueClient {
216
244
  body: {
217
245
  phone,
218
246
  password,
219
- data: options?.data,
247
+ data: options?.data ?? {},
220
248
  gotrue_meta_security: { captcha_token: options?.captchaToken },
221
249
  },
222
250
  xform: _sessionResponse,
@@ -266,6 +294,7 @@ export default class GoTrueClient {
266
294
  body: {
267
295
  email,
268
296
  password,
297
+ data: options?.data ?? {},
269
298
  gotrue_meta_security: { captcha_token: options?.captchaToken },
270
299
  },
271
300
  xform: _sessionResponse,
@@ -277,6 +306,7 @@ export default class GoTrueClient {
277
306
  body: {
278
307
  phone,
279
308
  password,
309
+ data: options?.data ?? {},
280
310
  gotrue_meta_security: { captcha_token: options?.captchaToken },
281
311
  },
282
312
  xform: _sessionResponse,
@@ -530,6 +560,17 @@ export default class GoTrueClient {
530
560
  }
531
561
  }
532
562
 
563
+ /**
564
+ * Decodes a JWT (without performing any validation).
565
+ */
566
+ private _decodeJWT(jwt: string): {
567
+ exp?: number
568
+ aal?: AuthenticatorAssuranceLevels | null
569
+ amr?: AMREntry[] | null
570
+ } {
571
+ return decodeJWTPayload(jwt)
572
+ }
573
+
533
574
  /**
534
575
  * 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.
535
576
  * If the refresh token in the current session is invalid and the current session has expired, an error will be thrown.
@@ -545,7 +586,8 @@ export default class GoTrueClient {
545
586
  let hasExpired = true
546
587
  let session: Session | null = null
547
588
  if (currentSession.access_token && currentSession.access_token.split('.')[1]) {
548
- const payload = JSON.parse(decodeBase64URL(currentSession.access_token.split('.')[1]))
589
+ const payload = this._decodeJWT(currentSession.access_token)
590
+
549
591
  if (payload.exp) {
550
592
  expiresAt = payload.exp
551
593
  hasExpired = expiresAt <= timeNow
@@ -593,6 +635,46 @@ export default class GoTrueClient {
593
635
  }
594
636
  }
595
637
 
638
+ /**
639
+ * Returns a new session, regardless of expiry status.
640
+ * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
641
+ * If the current session's refresh token is invalid, an error will be thrown.
642
+ * @param currentSession The current session. If passed in, it must contain a refresh token.
643
+ */
644
+ async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
645
+ try {
646
+ if (!currentSession) {
647
+ const { data, error } = await this.getSession()
648
+ if (error) {
649
+ throw error
650
+ }
651
+
652
+ currentSession = data.session ?? undefined
653
+ }
654
+
655
+ if (!currentSession?.refresh_token) {
656
+ throw new AuthSessionMissingError()
657
+ }
658
+
659
+ const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
660
+ if (error) {
661
+ return { data: { user: null, session: null }, error: error }
662
+ }
663
+
664
+ if (!session) {
665
+ return { data: { user: null, session: null }, error: null }
666
+ }
667
+
668
+ return { data: { user: session.user, session }, error: null }
669
+ } catch (error) {
670
+ if (isAuthError(error)) {
671
+ return { data: { user: null, session: null }, error }
672
+ }
673
+
674
+ throw error
675
+ }
676
+ }
677
+
596
678
  /**
597
679
  * Gets the session data from a URL string
598
680
  */
@@ -1005,4 +1087,177 @@ export default class GoTrueClient {
1005
1087
  }
1006
1088
  return `${this.url}/authorize?${urlParams.join('&')}`
1007
1089
  }
1090
+
1091
+ private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
1092
+ const { data: sessionData, error: sessionError } = await this.getSession()
1093
+ if (sessionError) {
1094
+ return { data: null, error: sessionError }
1095
+ }
1096
+
1097
+ return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
1098
+ headers: this.headers,
1099
+ jwt: sessionData?.session?.access_token,
1100
+ })
1101
+ }
1102
+
1103
+ /**
1104
+ * Deletes a registered factor from GoTrue
1105
+ * @param friendlyName Human readable name assigned to a device
1106
+ * @param factorType device which we're validating against. Can only be TOTP for now.
1107
+ * @param issuer domain which the user is enrolling with
1108
+ */
1109
+ private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
1110
+ const { data: sessionData, error: sessionError } = await this.getSession()
1111
+ if (sessionError) {
1112
+ return { data: null, error: sessionError }
1113
+ }
1114
+
1115
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
1116
+ body: {
1117
+ friendly_name: params.friendlyName,
1118
+ factor_type: params.factorType,
1119
+ issuer: params.issuer,
1120
+ },
1121
+ headers: this.headers,
1122
+ jwt: sessionData?.session?.access_token,
1123
+ })
1124
+
1125
+ if (error) {
1126
+ return { data: null, error }
1127
+ }
1128
+
1129
+ if (data?.totp?.qr_code) {
1130
+ data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
1131
+ }
1132
+
1133
+ return { data, error: null }
1134
+ }
1135
+
1136
+ /**
1137
+ * Validates a device as part of the enrollment step.
1138
+ * @param factorID System assigned identifier for authenticator device as returned by enroll
1139
+ * @param code Code Generated by an authenticator device
1140
+ */
1141
+ private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
1142
+ const { data: sessionData, error: sessionError } = await this.getSession()
1143
+ if (sessionError) {
1144
+ return { data: null, error: sessionError }
1145
+ }
1146
+
1147
+ const { data, error } = await _request(
1148
+ this.fetch,
1149
+ 'POST',
1150
+ `${this.url}/factors/${params.factorId}/verify`,
1151
+ {
1152
+ body: { code: params.code, challenge_id: params.challengeId },
1153
+ headers: this.headers,
1154
+ jwt: sessionData?.session?.access_token,
1155
+ }
1156
+ )
1157
+ if (error) {
1158
+ return { data: null, error }
1159
+ }
1160
+
1161
+ await this._saveSession({
1162
+ expires_at: Math.round(Date.now() / 1000) + data.expires_in,
1163
+ ...data,
1164
+ })
1165
+ this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
1166
+
1167
+ return { data, error }
1168
+ }
1169
+
1170
+ /**
1171
+ * Creates a challenge which a user can verify against
1172
+ * @param factorID System assigned identifier for authenticator device as returned by enroll
1173
+ */
1174
+ private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
1175
+ const { data: sessionData, error: sessionError } = await this.getSession()
1176
+ if (sessionError) {
1177
+ return { data: null, error: sessionError }
1178
+ }
1179
+
1180
+ return await _request(this.fetch, 'POST', `${this.url}/factors/${params.factorId}/challenge`, {
1181
+ headers: this.headers,
1182
+ jwt: sessionData?.session?.access_token,
1183
+ })
1184
+ }
1185
+ private async _challengeAndVerify(
1186
+ params: MFAChallengeAndVerifyParams
1187
+ ): Promise<AuthMFAVerifyResponse> {
1188
+ const { data: challengeData, error: challengeError } = await this._challenge({
1189
+ factorId: params.factorId,
1190
+ })
1191
+ if (challengeError) {
1192
+ return { data: null, error: challengeError }
1193
+ }
1194
+ return await this._verify({
1195
+ factorId: params.factorId,
1196
+ challengeId: challengeData.id,
1197
+ code: params.code,
1198
+ })
1199
+ }
1200
+
1201
+ /**
1202
+ * Displays all devices for a given user
1203
+ */
1204
+ private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
1205
+ const {
1206
+ data: { user },
1207
+ error: userError,
1208
+ } = await this.getUser()
1209
+ if (userError) {
1210
+ return { data: null, error: userError }
1211
+ }
1212
+
1213
+ const factors = user?.factors || []
1214
+ const totp = factors.filter(
1215
+ (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
1216
+ )
1217
+
1218
+ return {
1219
+ data: {
1220
+ all: factors,
1221
+ totp,
1222
+ },
1223
+ error: null,
1224
+ }
1225
+ }
1226
+
1227
+ private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
1228
+ const {
1229
+ data: { session },
1230
+ error: sessionError,
1231
+ } = await this.getSession()
1232
+ if (sessionError) {
1233
+ return { data: null, error: sessionError }
1234
+ }
1235
+ if (!session) {
1236
+ return {
1237
+ data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
1238
+ error: null,
1239
+ }
1240
+ }
1241
+
1242
+ const payload = this._decodeJWT(session.access_token)
1243
+
1244
+ let currentLevel: AuthenticatorAssuranceLevels | null = null
1245
+
1246
+ if (payload.aal) {
1247
+ currentLevel = payload.aal
1248
+ }
1249
+
1250
+ let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
1251
+
1252
+ const verifiedFactors =
1253
+ session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
1254
+
1255
+ if (verifiedFactors.length > 0) {
1256
+ nextLevel = 'aal2'
1257
+ }
1258
+
1259
+ const currentAuthenticationMethods = payload.amr || []
1260
+
1261
+ return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
1262
+ }
1008
1263
  }
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import GoTrueAdminApi from './GoTrueAdminApi'
2
2
  import GoTrueClient from './GoTrueClient'
3
-
4
3
  export { GoTrueAdminApi, GoTrueClient }
5
4
  export * from './lib/types'
6
5
  export * from './lib/errors'
@@ -120,3 +120,14 @@ export class Deferred<T = any> {
120
120
  })
121
121
  }
122
122
  }
123
+ // Taken from: https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library
124
+ export function decodeJWTPayload(token: string) {
125
+ const parts = token.split('.')
126
+
127
+ if (parts.length !== 3) {
128
+ throw new Error('JWT is not valid: not a JWT structure')
129
+ }
130
+
131
+ const base64Url = parts[1]
132
+ return JSON.parse(decodeBase64URL(base64Url))
133
+ }