@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.
- package/dist/main/GoTrueClient.d.ts +28 -5
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +96 -8
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/base64url.d.ts +74 -0
- package/dist/main/lib/base64url.d.ts.map +1 -0
- package/dist/main/lib/base64url.js +258 -0
- package/dist/main/lib/base64url.js.map +1 -0
- package/dist/main/lib/constants.d.ts +1 -0
- package/dist/main/lib/constants.d.ts.map +1 -1
- package/dist/main/lib/constants.js +2 -1
- package/dist/main/lib/constants.js.map +1 -1
- package/dist/main/lib/errors.d.ts +3 -0
- package/dist/main/lib/errors.d.ts.map +1 -1
- package/dist/main/lib/errors.js +7 -1
- package/dist/main/lib/errors.js.map +1 -1
- package/dist/main/lib/helpers.d.ts +12 -3
- package/dist/main/lib/helpers.d.ts.map +1 -1
- package/dist/main/lib/helpers.js +51 -41
- package/dist/main/lib/helpers.js.map +1 -1
- package/dist/main/lib/types.d.ts +25 -0
- package/dist/main/lib/types.d.ts.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.d.ts.map +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/main/lib/version.js.map +1 -1
- package/dist/module/GoTrueClient.d.ts +28 -5
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +98 -10
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/base64url.d.ts +74 -0
- package/dist/module/lib/base64url.d.ts.map +1 -0
- package/dist/module/lib/base64url.js +246 -0
- package/dist/module/lib/base64url.js.map +1 -0
- package/dist/module/lib/constants.d.ts +1 -0
- package/dist/module/lib/constants.d.ts.map +1 -1
- package/dist/module/lib/constants.js +1 -0
- package/dist/module/lib/constants.js.map +1 -1
- package/dist/module/lib/errors.d.ts +3 -0
- package/dist/module/lib/errors.d.ts.map +1 -1
- package/dist/module/lib/errors.js +5 -0
- package/dist/module/lib/errors.js.map +1 -1
- package/dist/module/lib/helpers.d.ts +12 -3
- package/dist/module/lib/helpers.d.ts.map +1 -1
- package/dist/module/lib/helpers.js +48 -39
- package/dist/module/lib/helpers.js.map +1 -1
- package/dist/module/lib/types.d.ts +25 -0
- package/dist/module/lib/types.d.ts.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.d.ts.map +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/version.js.map +1 -1
- package/package.json +1 -1
- package/src/GoTrueClient.ts +139 -17
- package/src/lib/base64url.ts +290 -0
- package/src/lib/constants.ts +2 -0
- package/src/lib/errors.ts +6 -0
- package/src/lib/helpers.ts +59 -46
- package/src/lib/types.ts +29 -0
- package/src/lib/version.ts +1 -1
package/src/GoTrueClient.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
+
}
|
package/src/lib/constants.ts
CHANGED
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
|
+
}
|
package/src/lib/helpers.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { API_VERSION_HEADER_NAME } from './constants'
|
|
2
|
-
import {
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
183
|
+
throw new AuthInvalidJwtError('Invalid JWT structure')
|
|
206
184
|
}
|
|
207
185
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
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
|
+
}
|