@supabase/auth-js 2.68.0 → 2.69.0-rc.2

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 (60) hide show
  1. package/dist/main/GoTrueClient.d.ts +28 -5
  2. package/dist/main/GoTrueClient.d.ts.map +1 -1
  3. package/dist/main/GoTrueClient.js +96 -8
  4. package/dist/main/GoTrueClient.js.map +1 -1
  5. package/dist/main/lib/base64url.d.ts +74 -0
  6. package/dist/main/lib/base64url.d.ts.map +1 -0
  7. package/dist/main/lib/base64url.js +258 -0
  8. package/dist/main/lib/base64url.js.map +1 -0
  9. package/dist/main/lib/constants.d.ts +1 -0
  10. package/dist/main/lib/constants.d.ts.map +1 -1
  11. package/dist/main/lib/constants.js +2 -1
  12. package/dist/main/lib/constants.js.map +1 -1
  13. package/dist/main/lib/errors.d.ts +3 -0
  14. package/dist/main/lib/errors.d.ts.map +1 -1
  15. package/dist/main/lib/errors.js +7 -1
  16. package/dist/main/lib/errors.js.map +1 -1
  17. package/dist/main/lib/helpers.d.ts +12 -3
  18. package/dist/main/lib/helpers.d.ts.map +1 -1
  19. package/dist/main/lib/helpers.js +51 -41
  20. package/dist/main/lib/helpers.js.map +1 -1
  21. package/dist/main/lib/types.d.ts +25 -0
  22. package/dist/main/lib/types.d.ts.map +1 -1
  23. package/dist/main/lib/version.d.ts +1 -1
  24. package/dist/main/lib/version.d.ts.map +1 -1
  25. package/dist/main/lib/version.js +1 -1
  26. package/dist/main/lib/version.js.map +1 -1
  27. package/dist/module/GoTrueClient.d.ts +28 -5
  28. package/dist/module/GoTrueClient.d.ts.map +1 -1
  29. package/dist/module/GoTrueClient.js +98 -10
  30. package/dist/module/GoTrueClient.js.map +1 -1
  31. package/dist/module/lib/base64url.d.ts +74 -0
  32. package/dist/module/lib/base64url.d.ts.map +1 -0
  33. package/dist/module/lib/base64url.js +246 -0
  34. package/dist/module/lib/base64url.js.map +1 -0
  35. package/dist/module/lib/constants.d.ts +1 -0
  36. package/dist/module/lib/constants.d.ts.map +1 -1
  37. package/dist/module/lib/constants.js +1 -0
  38. package/dist/module/lib/constants.js.map +1 -1
  39. package/dist/module/lib/errors.d.ts +3 -0
  40. package/dist/module/lib/errors.d.ts.map +1 -1
  41. package/dist/module/lib/errors.js +5 -0
  42. package/dist/module/lib/errors.js.map +1 -1
  43. package/dist/module/lib/helpers.d.ts +12 -3
  44. package/dist/module/lib/helpers.d.ts.map +1 -1
  45. package/dist/module/lib/helpers.js +48 -39
  46. package/dist/module/lib/helpers.js.map +1 -1
  47. package/dist/module/lib/types.d.ts +25 -0
  48. package/dist/module/lib/types.d.ts.map +1 -1
  49. package/dist/module/lib/version.d.ts +1 -1
  50. package/dist/module/lib/version.d.ts.map +1 -1
  51. package/dist/module/lib/version.js +1 -1
  52. package/dist/module/lib/version.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/GoTrueClient.ts +139 -17
  55. package/src/lib/base64url.ts +290 -0
  56. package/src/lib/constants.ts +2 -0
  57. package/src/lib/errors.ts +6 -0
  58. package/src/lib/helpers.ts +59 -46
  59. package/src/lib/types.ts +29 -0
  60. package/src/lib/version.ts +1 -1
@@ -20,6 +20,7 @@ import {
20
20
  isAuthRetryableFetchError,
21
21
  isAuthSessionMissingError,
22
22
  isAuthImplicitGrantRedirectError,
23
+ AuthInvalidJwtError,
23
24
  } from './lib/errors'
24
25
  import {
25
26
  Fetch,
@@ -30,7 +31,6 @@ import {
30
31
  _ssoResponse,
31
32
  } from './lib/fetch'
32
33
  import {
33
- decodeJWTPayload,
34
34
  Deferred,
35
35
  getItemAsync,
36
36
  isBrowser,
@@ -43,6 +43,9 @@ import {
43
43
  supportsLocalStorage,
44
44
  parseParametersFromURL,
45
45
  getCodeChallengeAndMethod,
46
+ getAlgorithm,
47
+ validateExp,
48
+ decodeJWT,
46
49
  } from './lib/helpers'
47
50
  import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage'
48
51
  import { polyfillGlobalThis } from './lib/polyfills'
@@ -86,7 +89,6 @@ import type {
86
89
  MFAVerifyParams,
87
90
  AuthMFAVerifyResponse,
88
91
  AuthMFAListFactorsResponse,
89
- AMREntry,
90
92
  AuthMFAGetAuthenticatorAssuranceLevelResponse,
91
93
  AuthenticatorAssuranceLevels,
92
94
  Factor,
@@ -100,7 +102,11 @@ import type {
100
102
  MFAEnrollPhoneParams,
101
103
  AuthMFAEnrollTOTPResponse,
102
104
  AuthMFAEnrollPhoneResponse,
105
+ JWK,
106
+ JwtPayload,
107
+ JwtHeader,
103
108
  } from './lib/types'
109
+ import { stringToUint8Array } from './lib/base64url'
104
110
 
105
111
  polyfillGlobalThis() // Make "globalThis" available
106
112
 
@@ -140,7 +146,10 @@ export default class GoTrueClient {
140
146
  protected storageKey: string
141
147
 
142
148
  protected flowType: AuthFlowType
143
-
149
+ /**
150
+ * The JWKS used for verifying asymmetric JWTs
151
+ */
152
+ protected jwks: { keys: JWK[] }
144
153
  protected autoRefreshToken: boolean
145
154
  protected persistSession: boolean
146
155
  protected storage: SupportedStorage
@@ -220,7 +229,7 @@ export default class GoTrueClient {
220
229
  } else {
221
230
  this.lock = lockNoOp
222
231
  }
223
-
232
+ this.jwks = { keys: [] }
224
233
  this.mfa = {
225
234
  verify: this._verify.bind(this),
226
235
  enroll: this._enroll.bind(this),
@@ -1288,17 +1297,6 @@ export default class GoTrueClient {
1288
1297
  }
1289
1298
  }
1290
1299
 
1291
- /**
1292
- * Decodes a JWT (without performing any validation).
1293
- */
1294
- private _decodeJWT(jwt: string): {
1295
- exp?: number
1296
- aal?: AuthenticatorAssuranceLevels | null
1297
- amr?: AMREntry[] | null
1298
- } {
1299
- return decodeJWTPayload(jwt)
1300
- }
1301
-
1302
1300
  /**
1303
1301
  * 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.
1304
1302
  * If the refresh token or access token in the current session is invalid, an error will be thrown.
@@ -1328,7 +1326,7 @@ export default class GoTrueClient {
1328
1326
  let expiresAt = timeNow
1329
1327
  let hasExpired = true
1330
1328
  let session: Session | null = null
1331
- const payload = decodeJWTPayload(currentSession.access_token)
1329
+ const { payload } = decodeJWT(currentSession.access_token)
1332
1330
  if (payload.exp) {
1333
1331
  expiresAt = payload.exp
1334
1332
  hasExpired = expiresAt <= timeNow
@@ -2576,7 +2574,7 @@ export default class GoTrueClient {
2576
2574
  }
2577
2575
  }
2578
2576
 
2579
- const payload = this._decodeJWT(session.access_token)
2577
+ const { payload } = decodeJWT(session.access_token)
2580
2578
 
2581
2579
  let currentLevel: AuthenticatorAssuranceLevels | null = null
2582
2580
 
@@ -2599,4 +2597,128 @@ export default class GoTrueClient {
2599
2597
  })
2600
2598
  })
2601
2599
  }
2600
+
2601
+ private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK> {
2602
+ // try fetching from the supplied jwks
2603
+ let jwk = jwks.keys.find((key) => key.kid === kid)
2604
+ if (jwk) {
2605
+ return jwk
2606
+ }
2607
+
2608
+ // try fetching from cache
2609
+ jwk = this.jwks.keys.find((key) => key.kid === kid)
2610
+ if (jwk) {
2611
+ return jwk
2612
+ }
2613
+ // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
2614
+ const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
2615
+ headers: this.headers,
2616
+ })
2617
+ if (error) {
2618
+ throw error
2619
+ }
2620
+ if (!data.keys || data.keys.length === 0) {
2621
+ throw new AuthInvalidJwtError('JWKS is empty')
2622
+ }
2623
+ this.jwks = data
2624
+ // Find the signing key
2625
+ jwk = data.keys.find((key: any) => key.kid === kid)
2626
+ if (!jwk) {
2627
+ throw new AuthInvalidJwtError('No matching signing key found in JWKS')
2628
+ }
2629
+ return jwk
2630
+ }
2631
+
2632
+ /**
2633
+ * @experimental This method may change in future versions.
2634
+ * @description Gets the claims from a JWT. If the JWT is symmetric JWTs, it will call getUser() to verify against the server. If the JWT is asymmetric, it will be verified against the JWKS using the WebCrypto API.
2635
+ */
2636
+ async getClaims(
2637
+ jwt?: string,
2638
+ jwks: { keys: JWK[] } = { keys: [] }
2639
+ ): Promise<
2640
+ | {
2641
+ data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
2642
+ error: null
2643
+ }
2644
+ | { data: null; error: AuthError }
2645
+ | { data: null; error: null }
2646
+ > {
2647
+ try {
2648
+ let token = jwt
2649
+ if (!token) {
2650
+ const { data, error } = await this.getSession()
2651
+ if (error || !data.session) {
2652
+ return { data: null, error }
2653
+ }
2654
+ token = data.session.access_token
2655
+ }
2656
+
2657
+ const {
2658
+ header,
2659
+ payload,
2660
+ signature,
2661
+ raw: { header: rawHeader, payload: rawPayload },
2662
+ } = decodeJWT(token)
2663
+
2664
+ // Reject expired JWTs
2665
+ validateExp(payload.exp)
2666
+
2667
+ // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
2668
+ if (
2669
+ !header.kid ||
2670
+ header.alg === 'HS256' ||
2671
+ !('crypto' in globalThis && 'subtle' in globalThis.crypto)
2672
+ ) {
2673
+ const { error } = await this.getUser(token)
2674
+ if (error) {
2675
+ throw error
2676
+ }
2677
+ // getUser succeeds so the claims in the JWT can be trusted
2678
+ return {
2679
+ data: {
2680
+ claims: payload,
2681
+ header,
2682
+ signature,
2683
+ },
2684
+ error: null,
2685
+ }
2686
+ }
2687
+
2688
+ const algorithm = getAlgorithm(header.alg)
2689
+ const signingKey = await this.fetchJwk(header.kid, jwks)
2690
+
2691
+ // Convert JWK to CryptoKey
2692
+ const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
2693
+ 'verify',
2694
+ ])
2695
+
2696
+ // Verify the signature
2697
+ const isValid = await crypto.subtle.verify(
2698
+ algorithm,
2699
+ publicKey,
2700
+ signature,
2701
+ stringToUint8Array(`${rawHeader}.${rawPayload}`)
2702
+ )
2703
+
2704
+ if (!isValid) {
2705
+ throw new AuthInvalidJwtError('Invalid JWT signature')
2706
+ }
2707
+
2708
+ // If verification succeeds, decode and return claims
2709
+ return {
2710
+ data: {
2711
+ claims: payload,
2712
+ header,
2713
+ signature,
2714
+ },
2715
+ error: null,
2716
+ }
2717
+ } catch (error) {
2718
+ if (isAuthError(error)) {
2719
+ return { data: null, error }
2720
+ }
2721
+ throw error
2722
+ }
2723
+ }
2602
2724
  }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Avoid modifying this file. It's part of
3
+ * https://github.com/supabase-community/base64url-js. Submit all fixes on
4
+ * that repo!
5
+ */
6
+
7
+ /**
8
+ * An array of characters that encode 6 bits into a Base64-URL alphabet
9
+ * character.
10
+ */
11
+ const TO_BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('')
12
+
13
+ /**
14
+ * An array of characters that can appear in a Base64-URL encoded string but
15
+ * should be ignored.
16
+ */
17
+ const IGNORE_BASE64URL = ' \t\n\r='.split('')
18
+
19
+ /**
20
+ * An array of 128 numbers that map a Base64-URL character to 6 bits, or if -2
21
+ * used to skip the character, or if -1 used to error out.
22
+ */
23
+ const FROM_BASE64URL = (() => {
24
+ const charMap: number[] = new Array(128)
25
+
26
+ for (let i = 0; i < charMap.length; i += 1) {
27
+ charMap[i] = -1
28
+ }
29
+
30
+ for (let i = 0; i < IGNORE_BASE64URL.length; i += 1) {
31
+ charMap[IGNORE_BASE64URL[i].charCodeAt(0)] = -2
32
+ }
33
+
34
+ for (let i = 0; i < TO_BASE64URL.length; i += 1) {
35
+ charMap[TO_BASE64URL[i].charCodeAt(0)] = i
36
+ }
37
+
38
+ return charMap
39
+ })()
40
+
41
+ /**
42
+ * Converts a byte to a Base64-URL string.
43
+ *
44
+ * @param byte The byte to convert, or null to flush at the end of the byte sequence.
45
+ * @param state The Base64 conversion state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
46
+ * @param emit A function called with the next Base64 character when ready.
47
+ */
48
+ export function byteToBase64URL(
49
+ byte: number | null,
50
+ state: { queue: number; queuedBits: number },
51
+ emit: (char: string) => void
52
+ ) {
53
+ if (byte !== null) {
54
+ state.queue = (state.queue << 8) | byte
55
+ state.queuedBits += 8
56
+
57
+ while (state.queuedBits >= 6) {
58
+ const pos = (state.queue >> (state.queuedBits - 6)) & 63
59
+ emit(TO_BASE64URL[pos])
60
+ state.queuedBits -= 6
61
+ }
62
+ } else if (state.queuedBits > 0) {
63
+ state.queue = state.queue << (6 - state.queuedBits)
64
+ state.queuedBits = 6
65
+
66
+ while (state.queuedBits >= 6) {
67
+ const pos = (state.queue >> (state.queuedBits - 6)) & 63
68
+ emit(TO_BASE64URL[pos])
69
+ state.queuedBits -= 6
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Converts a String char code (extracted using `string.charCodeAt(position)`) to a sequence of Base64-URL characters.
76
+ *
77
+ * @param charCode The char code of the JavaScript string.
78
+ * @param state The Base64 state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
79
+ * @param emit A function called with the next byte.
80
+ */
81
+ export function byteFromBase64URL(
82
+ charCode: number,
83
+ state: { queue: number; queuedBits: number },
84
+ emit: (byte: number) => void
85
+ ) {
86
+ const bits = FROM_BASE64URL[charCode]
87
+
88
+ if (bits > -1) {
89
+ // valid Base64-URL character
90
+ state.queue = (state.queue << 6) | bits
91
+ state.queuedBits += 6
92
+
93
+ while (state.queuedBits >= 8) {
94
+ emit((state.queue >> (state.queuedBits - 8)) & 0xff)
95
+ state.queuedBits -= 8
96
+ }
97
+ } else if (bits === -2) {
98
+ // ignore spaces, tabs, newlines, =
99
+ return
100
+ } else {
101
+ throw new Error(`Invalid Base64-URL character "${String.fromCharCode(charCode)}"`)
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Converts a JavaScript string (which may include any valid character) into a
107
+ * Base64-URL encoded string. The string is first encoded in UTF-8 which is
108
+ * then encoded as Base64-URL.
109
+ *
110
+ * @param str The string to convert.
111
+ */
112
+ export function stringToBase64URL(str: string) {
113
+ const base64: string[] = []
114
+
115
+ const emitter = (char: string) => {
116
+ base64.push(char)
117
+ }
118
+
119
+ const state = { queue: 0, queuedBits: 0 }
120
+
121
+ stringToUTF8(str, (byte: number) => {
122
+ byteToBase64URL(byte, state, emitter)
123
+ })
124
+
125
+ byteToBase64URL(null, state, emitter)
126
+
127
+ return base64.join('')
128
+ }
129
+
130
+ /**
131
+ * Converts a Base64-URL encoded string into a JavaScript string. It is assumed
132
+ * that the underlying string has been encoded as UTF-8.
133
+ *
134
+ * @param str The Base64-URL encoded string.
135
+ */
136
+ export function stringFromBase64URL(str: string) {
137
+ const conv: string[] = []
138
+
139
+ const utf8Emit = (codepoint: number) => {
140
+ conv.push(String.fromCodePoint(codepoint))
141
+ }
142
+
143
+ const utf8State = {
144
+ utf8seq: 0,
145
+ codepoint: 0,
146
+ }
147
+
148
+ const b64State = { queue: 0, queuedBits: 0 }
149
+
150
+ const byteEmit = (byte: number) => {
151
+ stringFromUTF8(byte, utf8State, utf8Emit)
152
+ }
153
+
154
+ for (let i = 0; i < str.length; i += 1) {
155
+ byteFromBase64URL(str.charCodeAt(i), b64State, byteEmit)
156
+ }
157
+
158
+ return conv.join('')
159
+ }
160
+
161
+ /**
162
+ * Converts a Unicode codepoint to a multi-byte UTF-8 sequence.
163
+ *
164
+ * @param codepoint The Unicode codepoint.
165
+ * @param emit Function which will be called for each UTF-8 byte that represents the codepoint.
166
+ */
167
+ export function codepointToUTF8(codepoint: number, emit: (byte: number) => void) {
168
+ if (codepoint <= 0x7f) {
169
+ emit(codepoint)
170
+ return
171
+ } else if (codepoint <= 0x7ff) {
172
+ emit(0xc0 | (codepoint >> 6))
173
+ emit(0x80 | (codepoint & 0x3f))
174
+ return
175
+ } else if (codepoint <= 0xffff) {
176
+ emit(0xe0 | (codepoint >> 12))
177
+ emit(0x80 | ((codepoint >> 6) & 0x3f))
178
+ emit(0x80 | (codepoint & 0x3f))
179
+ return
180
+ } else if (codepoint <= 0x10ffff) {
181
+ emit(0xf0 | (codepoint >> 18))
182
+ emit(0x80 | ((codepoint >> 12) & 0x3f))
183
+ emit(0x80 | ((codepoint >> 6) & 0x3f))
184
+ emit(0x80 | (codepoint & 0x3f))
185
+ return
186
+ }
187
+
188
+ throw new Error(`Unrecognized Unicode codepoint: ${codepoint.toString(16)}`)
189
+ }
190
+
191
+ /**
192
+ * Converts a JavaScript string to a sequence of UTF-8 bytes.
193
+ *
194
+ * @param str The string to convert to UTF-8.
195
+ * @param emit Function which will be called for each UTF-8 byte of the string.
196
+ */
197
+ export function stringToUTF8(str: string, emit: (byte: number) => void) {
198
+ for (let i = 0; i < str.length; i += 1) {
199
+ let codepoint = str.charCodeAt(i)
200
+
201
+ if (codepoint > 0xd7ff && codepoint <= 0xdbff) {
202
+ // most UTF-16 codepoints are Unicode codepoints, except values in this
203
+ // range where the next UTF-16 codepoint needs to be combined with the
204
+ // current one to get the Unicode codepoint
205
+ const highSurrogate = ((codepoint - 0xd800) * 0x400) & 0xffff
206
+ const lowSurrogate = (str.charCodeAt(i + 1) - 0xdc00) & 0xffff
207
+ codepoint = (lowSurrogate | highSurrogate) + 0x10000
208
+ i += 1
209
+ }
210
+
211
+ codepointToUTF8(codepoint, emit)
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Converts a UTF-8 byte to a Unicode codepoint.
217
+ *
218
+ * @param byte The UTF-8 byte next in the sequence.
219
+ * @param state The shared state between consecutive UTF-8 bytes in the
220
+ * sequence, an object with the shape `{ utf8seq: 0, codepoint: 0 }`.
221
+ * @param emit Function which will be called for each codepoint.
222
+ */
223
+ export function stringFromUTF8(
224
+ byte: number,
225
+ state: { utf8seq: number; codepoint: number },
226
+ emit: (codepoint: number) => void
227
+ ) {
228
+ if (state.utf8seq === 0) {
229
+ if (byte <= 0x7f) {
230
+ emit(byte)
231
+ return
232
+ }
233
+
234
+ // count the number of 1 leading bits until you reach 0
235
+ for (let leadingBit = 1; leadingBit < 6; leadingBit += 1) {
236
+ if (((byte >> (7 - leadingBit)) & 1) === 0) {
237
+ state.utf8seq = leadingBit
238
+ break
239
+ }
240
+ }
241
+
242
+ if (state.utf8seq === 2) {
243
+ state.codepoint = byte & 31
244
+ } else if (state.utf8seq === 3) {
245
+ state.codepoint = byte & 15
246
+ } else if (state.utf8seq === 4) {
247
+ state.codepoint = byte & 7
248
+ } else {
249
+ throw new Error('Invalid UTF-8 sequence')
250
+ }
251
+
252
+ state.utf8seq -= 1
253
+ } else if (state.utf8seq > 0) {
254
+ if (byte <= 0x7f) {
255
+ throw new Error('Invalid UTF-8 sequence')
256
+ }
257
+
258
+ state.codepoint = (state.codepoint << 6) | (byte & 63)
259
+ state.utf8seq -= 1
260
+
261
+ if (state.utf8seq === 0) {
262
+ emit(state.codepoint)
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Helper functions to convert different types of strings to Uint8Array
269
+ */
270
+
271
+ export function base64UrlToUint8Array(str: string): Uint8Array {
272
+ const result: number[] = []
273
+ const state = { queue: 0, queuedBits: 0 }
274
+
275
+ const onByte = (byte: number) => {
276
+ result.push(byte)
277
+ }
278
+
279
+ for (let i = 0; i < str.length; i += 1) {
280
+ byteFromBase64URL(str.charCodeAt(i), state, onByte)
281
+ }
282
+
283
+ return new Uint8Array(result)
284
+ }
285
+
286
+ export function stringToUint8Array(str: string): Uint8Array {
287
+ const result: number[] = []
288
+ stringToUTF8(str, (byte: number) => result.push(byte))
289
+ return new Uint8Array(result)
290
+ }
@@ -28,3 +28,5 @@ export const API_VERSIONS = {
28
28
  name: '2024-01-01',
29
29
  },
30
30
  }
31
+
32
+ export const BASE64URL_REGEX = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i
package/src/lib/errors.ts CHANGED
@@ -157,3 +157,9 @@ export class AuthWeakPasswordError extends CustomAuthError {
157
157
  export function isAuthWeakPasswordError(error: unknown): error is AuthWeakPasswordError {
158
158
  return isAuthError(error) && error.name === 'AuthWeakPasswordError'
159
159
  }
160
+
161
+ export class AuthInvalidJwtError extends CustomAuthError {
162
+ constructor(message: string) {
163
+ super(message, 'AuthInvalidJwtError', 400, 'invalid_jwt')
164
+ }
165
+ }
@@ -1,5 +1,7 @@
1
- import { API_VERSION_HEADER_NAME } from './constants'
2
- import { SupportedStorage } from './types'
1
+ import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
2
+ import { AuthInvalidJwtError } from './errors'
3
+ import { base64UrlToUint8Array, stringFromBase64URL, stringToBase64URL } from './base64url'
4
+ import { JwtHeader, JwtPayload, SupportedStorage } from './types'
3
5
 
4
6
  export function expiresAt(expiresIn: number) {
5
7
  const timeNow = Math.round(Date.now() / 1000)
@@ -141,34 +143,6 @@ export const removeItemAsync = async (storage: SupportedStorage, key: string): P
141
143
  await storage.removeItem(key)
142
144
  }
143
145
 
144
- export function decodeBase64URL(value: string): string {
145
- const key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
146
- let base64 = ''
147
- let chr1, chr2, chr3
148
- let enc1, enc2, enc3, enc4
149
- let i = 0
150
- value = value.replace('-', '+').replace('_', '/')
151
-
152
- while (i < value.length) {
153
- enc1 = key.indexOf(value.charAt(i++))
154
- enc2 = key.indexOf(value.charAt(i++))
155
- enc3 = key.indexOf(value.charAt(i++))
156
- enc4 = key.indexOf(value.charAt(i++))
157
- chr1 = (enc1 << 2) | (enc2 >> 4)
158
- chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
159
- chr3 = ((enc3 & 3) << 6) | enc4
160
- base64 = base64 + String.fromCharCode(chr1)
161
-
162
- if (enc3 != 64 && chr2 != 0) {
163
- base64 = base64 + String.fromCharCode(chr2)
164
- }
165
- if (enc4 != 64 && chr3 != 0) {
166
- base64 = base64 + String.fromCharCode(chr3)
167
- }
168
- }
169
- return base64
170
- }
171
-
172
146
  /**
173
147
  * A deferred represents some asynchronous work that is not yet finished, which
174
148
  * may or may not culminate in a value.
@@ -194,23 +168,38 @@ export class Deferred<T = any> {
194
168
  }
195
169
  }
196
170
 
197
- // Taken from: https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library
198
- export function decodeJWTPayload(token: string) {
199
- // Regex checks for base64url format
200
- const base64UrlRegex = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}=?$|[a-z0-9_-]{2}(==)?$)$/i
201
-
171
+ export function decodeJWT(token: string): {
172
+ header: JwtHeader
173
+ payload: JwtPayload
174
+ signature: Uint8Array
175
+ raw: {
176
+ header: string
177
+ payload: string
178
+ }
179
+ } {
202
180
  const parts = token.split('.')
203
181
 
204
182
  if (parts.length !== 3) {
205
- throw new Error('JWT is not valid: not a JWT structure')
183
+ throw new AuthInvalidJwtError('Invalid JWT structure')
206
184
  }
207
185
 
208
- if (!base64UrlRegex.test(parts[1])) {
209
- throw new Error('JWT is not valid: payload is not in base64url format')
186
+ // Regex checks for base64url format
187
+ for (let i = 0; i < parts.length; i++) {
188
+ if (!BASE64URL_REGEX.test(parts[i] as string)) {
189
+ throw new AuthInvalidJwtError('JWT not in base64url format')
190
+ }
210
191
  }
211
-
212
- const base64Url = parts[1]
213
- return JSON.parse(decodeBase64URL(base64Url))
192
+ const data = {
193
+ // using base64url lib
194
+ header: JSON.parse(stringFromBase64URL(parts[0])),
195
+ payload: JSON.parse(stringFromBase64URL(parts[1])),
196
+ signature: base64UrlToUint8Array(parts[2]),
197
+ raw: {
198
+ header: parts[0],
199
+ payload: parts[1],
200
+ },
201
+ }
202
+ return data
214
203
  }
215
204
 
216
205
  /**
@@ -287,10 +276,6 @@ async function sha256(randomString: string) {
287
276
  .join('')
288
277
  }
289
278
 
290
- function base64urlencode(str: string) {
291
- return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
292
- }
293
-
294
279
  export async function generatePKCEChallenge(verifier: string) {
295
280
  const hasCryptoSupport =
296
281
  typeof crypto !== 'undefined' &&
@@ -304,7 +289,7 @@ export async function generatePKCEChallenge(verifier: string) {
304
289
  return verifier
305
290
  }
306
291
  const hashed = await sha256(verifier)
307
- return base64urlencode(hashed)
292
+ return stringToBase64URL(hashed)
308
293
  }
309
294
 
310
295
  export async function getCodeChallengeAndMethod(
@@ -344,3 +329,31 @@ export function parseResponseAPIVersion(response: Response) {
344
329
  return null
345
330
  }
346
331
  }
332
+
333
+ export function validateExp(exp: number) {
334
+ if (!exp) {
335
+ throw new Error('Missing exp claim')
336
+ }
337
+ const timeNow = Math.floor(Date.now() / 1000)
338
+ if (exp <= timeNow) {
339
+ throw new Error('JWT has expired')
340
+ }
341
+ }
342
+
343
+ export function getAlgorithm(alg: 'RS256' | 'ES256'): RsaHashedImportParams | EcKeyImportParams {
344
+ switch (alg) {
345
+ case 'RS256':
346
+ return {
347
+ name: 'RSASSA-PKCS1-v1_5',
348
+ hash: { name: 'SHA-256' },
349
+ }
350
+ case 'ES256':
351
+ return {
352
+ name: 'ECDSA',
353
+ namedCurve: 'P-256',
354
+ hash: { name: 'SHA-256' },
355
+ }
356
+ default:
357
+ throw new Error('Invalid alg claim')
358
+ }
359
+ }
package/src/lib/types.ts CHANGED
@@ -1199,3 +1199,32 @@ export type AuthMFAEnrollPhoneResponse =
1199
1199
  data: null
1200
1200
  error: AuthError
1201
1201
  }
1202
+
1203
+ export type JwtHeader = {
1204
+ alg: 'RS256' | 'ES256' | 'HS256'
1205
+ kid: string
1206
+ typ: string
1207
+ }
1208
+
1209
+ export type RequiredClaims = {
1210
+ iss: string
1211
+ sub: string
1212
+ aud: string | string[]
1213
+ exp: number
1214
+ iat: number
1215
+ role: string
1216
+ aal: AuthenticatorAssuranceLevels
1217
+ session_id: string
1218
+ }
1219
+
1220
+ export type JwtPayload = RequiredClaims & {
1221
+ [key: string]: any
1222
+ }
1223
+
1224
+ export interface JWK {
1225
+ kty: 'RSA' | 'EC' | 'oct'
1226
+ key_ops: string[]
1227
+ alg?: string
1228
+ kid?: string
1229
+ [key: string]: any
1230
+ }