@draftlab/auth 0.15.0 → 0.16.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.
- package/dist/esm/allow.js +26 -0
- package/dist/esm/client.js +254 -0
- package/dist/esm/core.js +597 -0
- package/dist/esm/css.d.js +0 -0
- package/dist/esm/error.js +88 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/keys.js +126 -0
- package/dist/esm/mutex.js +53 -0
- package/dist/esm/pkce.js +87 -0
- package/dist/esm/provider/apple.js +15 -0
- package/dist/esm/provider/code.js +62 -0
- package/dist/esm/provider/discord.js +15 -0
- package/dist/esm/provider/facebook.js +15 -0
- package/dist/esm/provider/github.js +15 -0
- package/dist/esm/provider/gitlab.js +15 -0
- package/dist/esm/provider/google.js +16 -0
- package/dist/esm/provider/linkedin.js +15 -0
- package/dist/esm/provider/magiclink.js +83 -0
- package/dist/esm/provider/microsoft.js +15 -0
- package/dist/esm/provider/oauth2.js +130 -0
- package/dist/esm/provider/password.js +331 -0
- package/dist/esm/provider/provider.js +18 -0
- package/dist/esm/provider/reddit.js +15 -0
- package/dist/esm/provider/slack.js +15 -0
- package/dist/esm/provider/spotify.js +15 -0
- package/dist/esm/provider/twitch.js +15 -0
- package/dist/esm/provider/vercel.js +17 -0
- package/dist/esm/random.js +40 -0
- package/dist/esm/revocation.js +27 -0
- package/dist/esm/storage/memory.js +110 -0
- package/dist/esm/storage/storage.js +56 -0
- package/dist/esm/storage/turso.js +93 -0
- package/dist/esm/storage/unstorage.js +78 -0
- package/dist/esm/subject.js +7 -0
- package/dist/esm/themes/theme.js +115 -0
- package/dist/esm/toolkit/client.js +119 -0
- package/dist/esm/toolkit/index.js +25 -0
- package/dist/esm/toolkit/providers/facebook.js +11 -0
- package/dist/esm/toolkit/providers/github.js +11 -0
- package/dist/esm/toolkit/providers/google.js +11 -0
- package/dist/esm/toolkit/providers/strategy.js +0 -0
- package/dist/esm/toolkit/storage.js +81 -0
- package/dist/esm/toolkit/utils.js +18 -0
- package/dist/esm/types.js +0 -0
- package/dist/esm/ui/base.js +478 -0
- package/dist/esm/ui/code.js +186 -0
- package/dist/esm/ui/form.js +46 -0
- package/dist/esm/ui/icon.js +242 -0
- package/dist/esm/ui/magiclink.js +158 -0
- package/dist/esm/ui/password.js +435 -0
- package/dist/esm/ui/select.js +102 -0
- package/dist/esm/util.js +59 -0
- package/dist/{allow.d.mts → types/allow.d.ts} +9 -11
- package/dist/types/allow.d.ts.map +1 -0
- package/dist/types/client.d.ts +462 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/core.d.ts +113 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/{error.d.mts → types/error.d.ts} +95 -97
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/{keys.d.mts → types/keys.d.ts} +20 -24
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/mutex.d.ts +42 -0
- package/dist/types/mutex.d.ts.map +1 -0
- package/dist/{pkce.d.mts → types/pkce.d.ts} +10 -11
- package/dist/types/pkce.d.ts.map +1 -0
- package/dist/types/provider/apple.d.ts +197 -0
- package/dist/types/provider/apple.d.ts.map +1 -0
- package/dist/types/provider/code.d.ts +288 -0
- package/dist/types/provider/code.d.ts.map +1 -0
- package/dist/types/provider/discord.d.ts +206 -0
- package/dist/types/provider/discord.d.ts.map +1 -0
- package/dist/types/provider/facebook.d.ts +200 -0
- package/dist/types/provider/facebook.d.ts.map +1 -0
- package/dist/types/provider/github.d.ts +220 -0
- package/dist/types/provider/github.d.ts.map +1 -0
- package/dist/types/provider/gitlab.d.ts +180 -0
- package/dist/types/provider/gitlab.d.ts.map +1 -0
- package/dist/types/provider/google.d.ts +158 -0
- package/dist/types/provider/google.d.ts.map +1 -0
- package/dist/types/provider/linkedin.d.ts +190 -0
- package/dist/types/provider/linkedin.d.ts.map +1 -0
- package/dist/types/provider/magiclink.d.ts +141 -0
- package/dist/types/provider/magiclink.d.ts.map +1 -0
- package/dist/types/provider/microsoft.d.ts +247 -0
- package/dist/types/provider/microsoft.d.ts.map +1 -0
- package/dist/types/provider/oauth2.d.ts +229 -0
- package/dist/types/provider/oauth2.d.ts.map +1 -0
- package/dist/types/provider/password.d.ts +408 -0
- package/dist/types/provider/password.d.ts.map +1 -0
- package/dist/types/provider/provider.d.ts +226 -0
- package/dist/types/provider/provider.d.ts.map +1 -0
- package/dist/types/provider/reddit.d.ts +159 -0
- package/dist/types/provider/reddit.d.ts.map +1 -0
- package/dist/types/provider/slack.d.ts +171 -0
- package/dist/types/provider/slack.d.ts.map +1 -0
- package/dist/types/provider/spotify.d.ts +168 -0
- package/dist/types/provider/spotify.d.ts.map +1 -0
- package/dist/types/provider/twitch.d.ts +163 -0
- package/dist/types/provider/twitch.d.ts.map +1 -0
- package/dist/types/provider/vercel.d.ts +294 -0
- package/dist/types/provider/vercel.d.ts.map +1 -0
- package/dist/{random.d.mts → types/random.d.ts} +4 -6
- package/dist/types/random.d.ts.map +1 -0
- package/dist/types/revocation.d.ts +76 -0
- package/dist/types/revocation.d.ts.map +1 -0
- package/dist/{storage/memory.d.mts → types/storage/memory.d.ts} +17 -21
- package/dist/types/storage/memory.d.ts.map +1 -0
- package/dist/types/storage/storage.d.ts +177 -0
- package/dist/types/storage/storage.d.ts.map +1 -0
- package/dist/{storage/turso.d.mts → types/storage/turso.d.ts} +4 -8
- package/dist/types/storage/turso.d.ts.map +1 -0
- package/dist/{storage/unstorage.d.mts → types/storage/unstorage.d.ts} +12 -11
- package/dist/types/storage/unstorage.d.ts.map +1 -0
- package/dist/types/subject.d.ts +115 -0
- package/dist/types/subject.d.ts.map +1 -0
- package/dist/types/themes/theme.d.ts +207 -0
- package/dist/types/themes/theme.d.ts.map +1 -0
- package/dist/types/toolkit/client.d.ts +235 -0
- package/dist/types/toolkit/client.d.ts.map +1 -0
- package/dist/types/toolkit/index.d.ts +45 -0
- package/dist/types/toolkit/index.d.ts.map +1 -0
- package/dist/types/toolkit/providers/facebook.d.ts +8 -0
- package/dist/types/toolkit/providers/facebook.d.ts.map +1 -0
- package/dist/types/toolkit/providers/github.d.ts +8 -0
- package/dist/types/toolkit/providers/github.d.ts.map +1 -0
- package/dist/types/toolkit/providers/google.d.ts +8 -0
- package/dist/types/toolkit/providers/google.d.ts.map +1 -0
- package/dist/types/toolkit/providers/strategy.d.ts +38 -0
- package/dist/types/toolkit/providers/strategy.d.ts.map +1 -0
- package/dist/{toolkit/storage.d.mts → types/toolkit/storage.d.ts} +37 -39
- package/dist/types/toolkit/storage.d.ts.map +1 -0
- package/dist/{toolkit/utils.d.mts → types/toolkit/utils.d.ts} +2 -4
- package/dist/types/toolkit/utils.d.ts.map +1 -0
- package/dist/types/types.d.ts +92 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/ui/base.d.ts +18 -0
- package/dist/types/ui/base.d.ts.map +1 -0
- package/dist/types/ui/code.d.ts +43 -0
- package/dist/types/ui/code.d.ts.map +1 -0
- package/dist/types/ui/form.d.ts +24 -0
- package/dist/types/ui/form.d.ts.map +1 -0
- package/dist/types/ui/icon.d.ts +60 -0
- package/dist/types/ui/icon.d.ts.map +1 -0
- package/dist/types/ui/magiclink.d.ts +41 -0
- package/dist/types/ui/magiclink.d.ts.map +1 -0
- package/dist/types/ui/password.d.ts +43 -0
- package/dist/types/ui/password.d.ts.map +1 -0
- package/dist/types/ui/select.d.ts +33 -0
- package/dist/types/ui/select.d.ts.map +1 -0
- package/dist/{util.d.mts → types/util.d.ts} +11 -13
- package/dist/types/util.d.ts.map +1 -0
- package/package.json +10 -16
- package/dist/adapters/node.d.mts +0 -18
- package/dist/adapters/node.mjs +0 -69
- package/dist/allow.mjs +0 -63
- package/dist/client.d.mts +0 -456
- package/dist/client.mjs +0 -283
- package/dist/core.d.mts +0 -110
- package/dist/core.mjs +0 -595
- package/dist/error.mjs +0 -237
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -3
- package/dist/keys.mjs +0 -146
- package/dist/mutex.d.mts +0 -44
- package/dist/mutex.mjs +0 -110
- package/dist/pkce.mjs +0 -157
- package/dist/provider/apple.d.mts +0 -111
- package/dist/provider/apple.mjs +0 -164
- package/dist/provider/code.d.mts +0 -228
- package/dist/provider/code.mjs +0 -246
- package/dist/provider/discord.d.mts +0 -146
- package/dist/provider/discord.mjs +0 -156
- package/dist/provider/facebook.d.mts +0 -142
- package/dist/provider/facebook.mjs +0 -150
- package/dist/provider/github.d.mts +0 -140
- package/dist/provider/github.mjs +0 -169
- package/dist/provider/gitlab.d.mts +0 -106
- package/dist/provider/gitlab.mjs +0 -147
- package/dist/provider/google.d.mts +0 -112
- package/dist/provider/google.mjs +0 -109
- package/dist/provider/linkedin.d.mts +0 -132
- package/dist/provider/linkedin.mjs +0 -142
- package/dist/provider/magiclink.d.mts +0 -89
- package/dist/provider/magiclink.mjs +0 -143
- package/dist/provider/microsoft.d.mts +0 -178
- package/dist/provider/microsoft.mjs +0 -177
- package/dist/provider/oauth2.d.mts +0 -176
- package/dist/provider/oauth2.mjs +0 -222
- package/dist/provider/passkey.d.mts +0 -104
- package/dist/provider/passkey.mjs +0 -320
- package/dist/provider/password.d.mts +0 -412
- package/dist/provider/password.mjs +0 -363
- package/dist/provider/provider.d.mts +0 -227
- package/dist/provider/provider.mjs +0 -44
- package/dist/provider/reddit.d.mts +0 -107
- package/dist/provider/reddit.mjs +0 -127
- package/dist/provider/slack.d.mts +0 -114
- package/dist/provider/slack.mjs +0 -138
- package/dist/provider/spotify.d.mts +0 -113
- package/dist/provider/spotify.mjs +0 -135
- package/dist/provider/totp.d.mts +0 -112
- package/dist/provider/totp.mjs +0 -191
- package/dist/provider/twitch.d.mts +0 -108
- package/dist/provider/twitch.mjs +0 -131
- package/dist/provider/vercel.d.mts +0 -177
- package/dist/provider/vercel.mjs +0 -230
- package/dist/random.mjs +0 -86
- package/dist/revocation.d.mts +0 -55
- package/dist/revocation.mjs +0 -63
- package/dist/router/context.d.mts +0 -21
- package/dist/router/context.mjs +0 -193
- package/dist/router/cookies.d.mts +0 -8
- package/dist/router/cookies.mjs +0 -13
- package/dist/router/index.d.mts +0 -21
- package/dist/router/index.mjs +0 -107
- package/dist/router/matcher.d.mts +0 -15
- package/dist/router/matcher.mjs +0 -76
- package/dist/router/middleware/cors.d.mts +0 -15
- package/dist/router/middleware/cors.mjs +0 -114
- package/dist/router/safe-request.d.mts +0 -52
- package/dist/router/safe-request.mjs +0 -160
- package/dist/router/types.d.mts +0 -67
- package/dist/router/types.mjs +0 -1
- package/dist/router/variables.d.mts +0 -12
- package/dist/router/variables.mjs +0 -20
- package/dist/storage/memory.mjs +0 -125
- package/dist/storage/storage.d.mts +0 -179
- package/dist/storage/storage.mjs +0 -104
- package/dist/storage/turso.mjs +0 -117
- package/dist/storage/unstorage.mjs +0 -103
- package/dist/subject.d.mts +0 -62
- package/dist/subject.mjs +0 -36
- package/dist/themes/theme.d.mts +0 -209
- package/dist/themes/theme.mjs +0 -120
- package/dist/toolkit/client.d.mts +0 -169
- package/dist/toolkit/client.mjs +0 -209
- package/dist/toolkit/index.d.mts +0 -9
- package/dist/toolkit/index.mjs +0 -9
- package/dist/toolkit/providers/facebook.d.mts +0 -12
- package/dist/toolkit/providers/facebook.mjs +0 -16
- package/dist/toolkit/providers/github.d.mts +0 -12
- package/dist/toolkit/providers/github.mjs +0 -16
- package/dist/toolkit/providers/google.d.mts +0 -12
- package/dist/toolkit/providers/google.mjs +0 -20
- package/dist/toolkit/providers/strategy.d.mts +0 -40
- package/dist/toolkit/providers/strategy.mjs +0 -1
- package/dist/toolkit/storage.mjs +0 -157
- package/dist/toolkit/utils.mjs +0 -30
- package/dist/types.d.mts +0 -94
- package/dist/types.mjs +0 -1
- package/dist/ui/base.d.mts +0 -30
- package/dist/ui/base.mjs +0 -407
- package/dist/ui/code.d.mts +0 -43
- package/dist/ui/code.mjs +0 -173
- package/dist/ui/form.d.mts +0 -32
- package/dist/ui/form.mjs +0 -49
- package/dist/ui/icon.d.mts +0 -58
- package/dist/ui/icon.mjs +0 -247
- package/dist/ui/magiclink.d.mts +0 -41
- package/dist/ui/magiclink.mjs +0 -152
- package/dist/ui/passkey.d.mts +0 -27
- package/dist/ui/passkey.mjs +0 -323
- package/dist/ui/password.d.mts +0 -42
- package/dist/ui/password.mjs +0 -402
- package/dist/ui/select.d.mts +0 -34
- package/dist/ui/select.mjs +0 -98
- package/dist/ui/totp.d.mts +0 -34
- package/dist/ui/totp.mjs +0 -270
- package/dist/util.mjs +0 -128
package/dist/error.mjs
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
//#region src/error.ts
|
|
2
|
-
/**
|
|
3
|
-
* Base OAuth error class for handling standard OAuth error responses.
|
|
4
|
-
* Contains both the error code and human-readable description.
|
|
5
|
-
*/
|
|
6
|
-
var OauthError = class extends Error {
|
|
7
|
-
/** The OAuth error code as defined in the specification */
|
|
8
|
-
error;
|
|
9
|
-
/** Human-readable description of the error */
|
|
10
|
-
description;
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new OAuth error with the specified error code and description.
|
|
13
|
-
*
|
|
14
|
-
* @param error - The OAuth error type
|
|
15
|
-
* @param description - Human-readable error description
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* throw new OauthError("invalid_grant", "Authorization code has expired")
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
constructor(error, description) {
|
|
23
|
-
super(`${error} - ${description}`);
|
|
24
|
-
this.name = "OauthError";
|
|
25
|
-
this.error = error;
|
|
26
|
-
this.description = description;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Converts the error to a standard OAuth JSON response format.
|
|
30
|
-
*
|
|
31
|
-
* @returns Object with error and error_description fields
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* const oauthError = new OauthError("invalid_request", "Missing parameter")
|
|
36
|
-
* return c.json(oauthError.toJSON(), 400)
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
toJSON() {
|
|
40
|
-
return {
|
|
41
|
-
error: this.error,
|
|
42
|
-
error_description: this.description
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Error thrown when a provider parameter is missing from the authorization request.
|
|
48
|
-
* Occurs when multiple providers are configured but no specific provider is selected.
|
|
49
|
-
*/
|
|
50
|
-
var MissingProviderError = class extends OauthError {
|
|
51
|
-
/**
|
|
52
|
-
* Creates a missing provider error.
|
|
53
|
-
* Thrown when the provider query parameter is required but not provided.
|
|
54
|
-
*/
|
|
55
|
-
constructor() {
|
|
56
|
-
super("invalid_request", "Must specify `provider` query parameter if `select` callback on issuer is not specified");
|
|
57
|
-
this.name = "MissingProviderError";
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Error thrown when a required parameter is missing from a request.
|
|
62
|
-
* Used for validating OAuth request parameters.
|
|
63
|
-
*/
|
|
64
|
-
var MissingParameterError = class extends OauthError {
|
|
65
|
-
/** The name of the missing parameter */
|
|
66
|
-
parameter;
|
|
67
|
-
/**
|
|
68
|
-
* Creates a missing parameter error.
|
|
69
|
-
*
|
|
70
|
-
* @param parameter - The name of the missing parameter
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* throw new MissingParameterError("client_id")
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
constructor(parameter) {
|
|
78
|
-
super("invalid_request", `Missing parameter: ${parameter}`);
|
|
79
|
-
this.name = "MissingParameterError";
|
|
80
|
-
this.parameter = parameter;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Error thrown when a client is not authorized to use a specific redirect URI.
|
|
85
|
-
* Prevents unauthorized clients from hijacking authorization codes.
|
|
86
|
-
*/
|
|
87
|
-
var UnauthorizedClientError = class extends OauthError {
|
|
88
|
-
/** The client ID that attempted unauthorized access */
|
|
89
|
-
clientID;
|
|
90
|
-
/**
|
|
91
|
-
* Creates an unauthorized client error.
|
|
92
|
-
*
|
|
93
|
-
* @param clientID - The client ID attempting unauthorized access
|
|
94
|
-
* @param redirectURI - The unauthorized redirect URI
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* ```ts
|
|
98
|
-
* throw new UnauthorizedClientError("malicious-client", "https://evil.com/callback")
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
constructor(clientID, redirectURI) {
|
|
102
|
-
super("unauthorized_client", `Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}`);
|
|
103
|
-
this.name = "UnauthorizedClientError";
|
|
104
|
-
this.clientID = clientID;
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* Error thrown when the authentication flow is in an unknown or invalid state.
|
|
109
|
-
*
|
|
110
|
-
* ## Common Causes
|
|
111
|
-
* - Session cookies have expired during the authentication flow
|
|
112
|
-
* - User switched browsers or devices mid-flow
|
|
113
|
-
* - Authentication state was manually tampered with
|
|
114
|
-
* - Server session storage was cleared
|
|
115
|
-
*
|
|
116
|
-
* @example
|
|
117
|
-
* ```ts
|
|
118
|
-
* // In provider callback handling
|
|
119
|
-
* const state = await getAuthState(request)
|
|
120
|
-
* if (!state) {
|
|
121
|
-
* throw new UnknownStateError()
|
|
122
|
-
* }
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
var UnknownStateError = class extends Error {
|
|
126
|
-
/**
|
|
127
|
-
* Creates an unknown state error.
|
|
128
|
-
* Indicates that the authentication flow cannot continue due to missing state.
|
|
129
|
-
*/
|
|
130
|
-
constructor() {
|
|
131
|
-
super("The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.");
|
|
132
|
-
this.name = "UnknownStateError";
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
/**
|
|
136
|
-
* Error thrown when a subject (user identifier) is invalid or malformed.
|
|
137
|
-
* Used during token verification and subject validation.
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```ts
|
|
141
|
-
* // During token verification
|
|
142
|
-
* const subject = extractSubject(token)
|
|
143
|
-
* if (!isValidSubject(subject)) {
|
|
144
|
-
* throw new InvalidSubjectError()
|
|
145
|
-
* }
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
var InvalidSubjectError = class extends Error {
|
|
149
|
-
/**
|
|
150
|
-
* Creates an invalid subject error.
|
|
151
|
-
*/
|
|
152
|
-
constructor() {
|
|
153
|
-
super("Invalid subject");
|
|
154
|
-
this.name = "InvalidSubjectError";
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
/**
|
|
158
|
-
* Error thrown when a refresh token is invalid, expired, or revoked.
|
|
159
|
-
* Occurs during token refresh operations.
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```ts
|
|
163
|
-
* // During token refresh
|
|
164
|
-
* try {
|
|
165
|
-
* const newTokens = await client.refresh(refreshToken)
|
|
166
|
-
* } catch (error) {
|
|
167
|
-
* if (error instanceof InvalidRefreshTokenError) {
|
|
168
|
-
* // Redirect user to login again
|
|
169
|
-
* redirectToLogin()
|
|
170
|
-
* }
|
|
171
|
-
* }
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
var InvalidRefreshTokenError = class extends Error {
|
|
175
|
-
/**
|
|
176
|
-
* Creates an invalid refresh token error.
|
|
177
|
-
*/
|
|
178
|
-
constructor() {
|
|
179
|
-
super("Invalid refresh token");
|
|
180
|
-
this.name = "InvalidRefreshTokenError";
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
/**
|
|
184
|
-
* Error thrown when an access token is invalid, expired, or malformed.
|
|
185
|
-
* Occurs during token verification and API requests.
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* ```ts
|
|
189
|
-
* // During API request
|
|
190
|
-
* try {
|
|
191
|
-
* const user = await api.getUser(accessToken)
|
|
192
|
-
* } catch (error) {
|
|
193
|
-
* if (error instanceof InvalidAccessTokenError) {
|
|
194
|
-
* // Try to refresh the token
|
|
195
|
-
* await refreshAccessToken()
|
|
196
|
-
* }
|
|
197
|
-
* }
|
|
198
|
-
* ```
|
|
199
|
-
*/
|
|
200
|
-
var InvalidAccessTokenError = class extends Error {
|
|
201
|
-
/**
|
|
202
|
-
* Creates an invalid access token error.
|
|
203
|
-
*/
|
|
204
|
-
constructor() {
|
|
205
|
-
super("Invalid access token");
|
|
206
|
-
this.name = "InvalidAccessTokenError";
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
/**
|
|
210
|
-
* Error thrown when an authorization code is invalid, expired, or already used.
|
|
211
|
-
* Occurs during the token exchange step of the OAuth flow.
|
|
212
|
-
*
|
|
213
|
-
* @example
|
|
214
|
-
* ```ts
|
|
215
|
-
* // During authorization code exchange
|
|
216
|
-
* try {
|
|
217
|
-
* const tokens = await client.exchange(code, redirectUri)
|
|
218
|
-
* } catch (error) {
|
|
219
|
-
* if (error instanceof InvalidAuthorizationCodeError) {
|
|
220
|
-
* // Code may have expired or been used already
|
|
221
|
-
* redirectToAuthorize()
|
|
222
|
-
* }
|
|
223
|
-
* }
|
|
224
|
-
* ```
|
|
225
|
-
*/
|
|
226
|
-
var InvalidAuthorizationCodeError = class extends Error {
|
|
227
|
-
/**
|
|
228
|
-
* Creates an invalid authorization code error.
|
|
229
|
-
*/
|
|
230
|
-
constructor() {
|
|
231
|
-
super("Invalid authorization code");
|
|
232
|
-
this.name = "InvalidAuthorizationCodeError";
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
//#endregion
|
|
237
|
-
export { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError, MissingParameterError, MissingProviderError, OauthError, UnauthorizedClientError, UnknownStateError };
|
package/dist/index.d.mts
DELETED
package/dist/index.mjs
DELETED
package/dist/keys.mjs
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { Mutex } from "./mutex.mjs";
|
|
2
|
-
import { generateSecureToken } from "./random.mjs";
|
|
3
|
-
import { Storage } from "./storage/storage.mjs";
|
|
4
|
-
import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, importSPKI } from "jose";
|
|
5
|
-
|
|
6
|
-
//#region src/keys.ts
|
|
7
|
-
/**
|
|
8
|
-
* Cryptographic key management for JWT signing and encryption operations.
|
|
9
|
-
* Handles automatic key generation, rotation, and storage for OAuth operations.
|
|
10
|
-
*/
|
|
11
|
-
/** Elliptic Curve algorithm used for JWT signing operations */
|
|
12
|
-
const signingAlg = "ES256";
|
|
13
|
-
/** RSA algorithm used for token encryption operations */
|
|
14
|
-
const encryptionAlg = "RSA-OAEP-512";
|
|
15
|
-
/** Mutex to prevent concurrent key generation (race condition with eventually consistent storage) */
|
|
16
|
-
const signingKeyMutex = new Mutex();
|
|
17
|
-
const encryptionKeyMutex = new Mutex();
|
|
18
|
-
/**
|
|
19
|
-
* Loads or generates signing keys for JWT operations.
|
|
20
|
-
* Returns existing valid keys, or generates new ones if none are available.
|
|
21
|
-
* Keys are automatically sorted by creation date (newest first).
|
|
22
|
-
*
|
|
23
|
-
* @param storage - Storage adapter for persistent key storage
|
|
24
|
-
* @returns Promise resolving to array of available signing key pairs
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* const keys = await signingKeys(storage)
|
|
29
|
-
* const currentKey = keys[0] // Most recent key
|
|
30
|
-
*
|
|
31
|
-
* // Use for JWT signing
|
|
32
|
-
* const jwt = await new SignJWT(payload)
|
|
33
|
-
* .setProtectedHeader({ alg: currentKey.alg, kid: currentKey.id })
|
|
34
|
-
* .sign(currentKey.private)
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
const signingKeys = async (storage) => {
|
|
38
|
-
return signingKeyMutex.runExclusive(async () => {
|
|
39
|
-
const results = [];
|
|
40
|
-
const scanner = Storage.scan(storage, ["signing:key"]);
|
|
41
|
-
for await (const [, value] of scanner) try {
|
|
42
|
-
const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
|
|
43
|
-
const privateKey = await importPKCS8(value.privateKey, value.alg);
|
|
44
|
-
const jwk$1 = await exportJWK(publicKey);
|
|
45
|
-
jwk$1.kid = value.id;
|
|
46
|
-
jwk$1.use = "sig";
|
|
47
|
-
results.push({
|
|
48
|
-
id: value.id,
|
|
49
|
-
alg: signingAlg,
|
|
50
|
-
created: new Date(value.created),
|
|
51
|
-
expired: value.expired ? new Date(value.expired) : void 0,
|
|
52
|
-
public: publicKey,
|
|
53
|
-
private: privateKey,
|
|
54
|
-
jwk: jwk$1
|
|
55
|
-
});
|
|
56
|
-
} catch {}
|
|
57
|
-
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
58
|
-
if (results.filter((item) => !item.expired).length) return results;
|
|
59
|
-
const key = await generateKeyPair(signingAlg, { extractable: true });
|
|
60
|
-
const serialized = {
|
|
61
|
-
id: generateSecureToken(16),
|
|
62
|
-
publicKey: await exportSPKI(key.publicKey),
|
|
63
|
-
privateKey: await exportPKCS8(key.privateKey),
|
|
64
|
-
created: Date.now(),
|
|
65
|
-
alg: signingAlg
|
|
66
|
-
};
|
|
67
|
-
await Storage.set(storage, ["signing:key", serialized.id], serialized);
|
|
68
|
-
const jwk = await exportJWK(key.publicKey);
|
|
69
|
-
jwk.kid = serialized.id;
|
|
70
|
-
jwk.use = "sig";
|
|
71
|
-
return [{
|
|
72
|
-
id: serialized.id,
|
|
73
|
-
alg: signingAlg,
|
|
74
|
-
created: new Date(serialized.created),
|
|
75
|
-
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
76
|
-
public: key.publicKey,
|
|
77
|
-
private: key.privateKey,
|
|
78
|
-
jwk
|
|
79
|
-
}, ...results];
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Loads or generates encryption keys for token encryption operations.
|
|
84
|
-
* Returns existing valid keys, or generates new ones if none are available.
|
|
85
|
-
* Keys are automatically sorted by creation date (newest first).
|
|
86
|
-
*
|
|
87
|
-
* @param storage - Storage adapter for persistent key storage
|
|
88
|
-
* @returns Promise resolving to array of available encryption key pairs
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* const keys = await encryptionKeys(storage)
|
|
93
|
-
* const currentKey = keys[0] // Most recent key
|
|
94
|
-
*
|
|
95
|
-
* // Use for token encryption
|
|
96
|
-
* const encrypted = await new EncryptJWT(payload)
|
|
97
|
-
* .setProtectedHeader({ alg: 'RSA-OAEP-512', enc: 'A256GCM' })
|
|
98
|
-
* .encrypt(currentKey.public)
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
const encryptionKeys = async (storage) => {
|
|
102
|
-
return encryptionKeyMutex.runExclusive(async () => {
|
|
103
|
-
const results = [];
|
|
104
|
-
const scanner = Storage.scan(storage, ["encryption:key"]);
|
|
105
|
-
for await (const [, value] of scanner) try {
|
|
106
|
-
const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
|
|
107
|
-
const privateKey = await importPKCS8(value.privateKey, value.alg);
|
|
108
|
-
const jwk$1 = await exportJWK(publicKey);
|
|
109
|
-
jwk$1.kid = value.id;
|
|
110
|
-
results.push({
|
|
111
|
-
id: value.id,
|
|
112
|
-
alg: encryptionAlg,
|
|
113
|
-
created: new Date(value.created),
|
|
114
|
-
expired: value.expired ? new Date(value.expired) : void 0,
|
|
115
|
-
public: publicKey,
|
|
116
|
-
private: privateKey,
|
|
117
|
-
jwk: jwk$1
|
|
118
|
-
});
|
|
119
|
-
} catch {}
|
|
120
|
-
results.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
121
|
-
if (results.filter((item) => !item.expired).length) return results;
|
|
122
|
-
const key = await generateKeyPair(encryptionAlg, { extractable: true });
|
|
123
|
-
const serialized = {
|
|
124
|
-
id: generateSecureToken(16),
|
|
125
|
-
publicKey: await exportSPKI(key.publicKey),
|
|
126
|
-
privateKey: await exportPKCS8(key.privateKey),
|
|
127
|
-
created: Date.now(),
|
|
128
|
-
alg: encryptionAlg
|
|
129
|
-
};
|
|
130
|
-
await Storage.set(storage, ["encryption:key", serialized.id], serialized);
|
|
131
|
-
const jwk = await exportJWK(key.publicKey);
|
|
132
|
-
jwk.kid = serialized.id;
|
|
133
|
-
return [{
|
|
134
|
-
id: serialized.id,
|
|
135
|
-
alg: encryptionAlg,
|
|
136
|
-
created: new Date(serialized.created),
|
|
137
|
-
expired: serialized.expired ? new Date(serialized.expired) : void 0,
|
|
138
|
-
public: key.publicKey,
|
|
139
|
-
private: key.privateKey,
|
|
140
|
-
jwk
|
|
141
|
-
}, ...results];
|
|
142
|
-
});
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
//#endregion
|
|
146
|
-
export { encryptionKeys, signingKeys };
|
package/dist/mutex.d.mts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
//#region src/mutex.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* A Mutex (mutual exclusion lock) for async functions.
|
|
4
|
-
* It allows only one async task to access a critical section at a time.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* const mutex = new Mutex();
|
|
8
|
-
*
|
|
9
|
-
* async function criticalSection() {
|
|
10
|
-
* await mutex.acquire();
|
|
11
|
-
* try {
|
|
12
|
-
* // This code section cannot be executed simultaneously
|
|
13
|
-
* } finally {
|
|
14
|
-
* mutex.release();
|
|
15
|
-
* }
|
|
16
|
-
* }
|
|
17
|
-
*/
|
|
18
|
-
declare class Mutex {
|
|
19
|
-
private semaphore;
|
|
20
|
-
/**
|
|
21
|
-
* Checks if the mutex is currently locked.
|
|
22
|
-
* @returns True if the mutex is locked, false otherwise.
|
|
23
|
-
*/
|
|
24
|
-
get isLocked(): boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Acquires the mutex, blocking if necessary until it is available.
|
|
27
|
-
* @returns A promise that resolves when the mutex is acquired.
|
|
28
|
-
*/
|
|
29
|
-
acquire(): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Releases the mutex, allowing another waiting task to proceed.
|
|
32
|
-
*/
|
|
33
|
-
release(): void;
|
|
34
|
-
/**
|
|
35
|
-
* Runs a function while holding the mutex lock.
|
|
36
|
-
* Automatically acquires before and releases after the function execution.
|
|
37
|
-
*
|
|
38
|
-
* @param fn - The function to execute while holding the lock
|
|
39
|
-
* @returns The result of the function
|
|
40
|
-
*/
|
|
41
|
-
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
42
|
-
}
|
|
43
|
-
//#endregion
|
|
44
|
-
export { Mutex };
|
package/dist/mutex.mjs
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
//#region src/mutex.ts
|
|
2
|
-
/**
|
|
3
|
-
* A counting semaphore for async functions that manages available permits.
|
|
4
|
-
* Semaphores are mainly used to limit the number of concurrent async tasks.
|
|
5
|
-
*
|
|
6
|
-
* Each `acquire` operation takes a permit or waits until one is available.
|
|
7
|
-
* Each `release` operation adds a permit, potentially allowing a waiting task to proceed.
|
|
8
|
-
*
|
|
9
|
-
* The semaphore ensures fairness by maintaining a FIFO (First In, First Out) order for acquirers.
|
|
10
|
-
*/
|
|
11
|
-
var Semaphore = class {
|
|
12
|
-
/**
|
|
13
|
-
* The maximum number of concurrent operations allowed.
|
|
14
|
-
*/
|
|
15
|
-
capacity;
|
|
16
|
-
/**
|
|
17
|
-
* The number of available permits.
|
|
18
|
-
*/
|
|
19
|
-
available;
|
|
20
|
-
deferredTasks = [];
|
|
21
|
-
/**
|
|
22
|
-
* Creates an instance of Semaphore.
|
|
23
|
-
* @param capacity - The maximum number of concurrent operations allowed.
|
|
24
|
-
*/
|
|
25
|
-
constructor(capacity) {
|
|
26
|
-
this.capacity = capacity;
|
|
27
|
-
this.available = capacity;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Acquires a semaphore, blocking if necessary until one is available.
|
|
31
|
-
* @returns A promise that resolves when the semaphore is acquired.
|
|
32
|
-
*/
|
|
33
|
-
async acquire() {
|
|
34
|
-
if (this.available > 0) {
|
|
35
|
-
this.available--;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
return new Promise((resolve) => {
|
|
39
|
-
this.deferredTasks.push(resolve);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Releases a semaphore, allowing one more operation to proceed.
|
|
44
|
-
*/
|
|
45
|
-
release() {
|
|
46
|
-
const deferredTask = this.deferredTasks.shift();
|
|
47
|
-
if (deferredTask != null) {
|
|
48
|
-
deferredTask();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (this.available < this.capacity) this.available++;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* A Mutex (mutual exclusion lock) for async functions.
|
|
56
|
-
* It allows only one async task to access a critical section at a time.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* const mutex = new Mutex();
|
|
60
|
-
*
|
|
61
|
-
* async function criticalSection() {
|
|
62
|
-
* await mutex.acquire();
|
|
63
|
-
* try {
|
|
64
|
-
* // This code section cannot be executed simultaneously
|
|
65
|
-
* } finally {
|
|
66
|
-
* mutex.release();
|
|
67
|
-
* }
|
|
68
|
-
* }
|
|
69
|
-
*/
|
|
70
|
-
var Mutex = class {
|
|
71
|
-
semaphore = new Semaphore(1);
|
|
72
|
-
/**
|
|
73
|
-
* Checks if the mutex is currently locked.
|
|
74
|
-
* @returns True if the mutex is locked, false otherwise.
|
|
75
|
-
*/
|
|
76
|
-
get isLocked() {
|
|
77
|
-
return this.semaphore.available === 0;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Acquires the mutex, blocking if necessary until it is available.
|
|
81
|
-
* @returns A promise that resolves when the mutex is acquired.
|
|
82
|
-
*/
|
|
83
|
-
async acquire() {
|
|
84
|
-
return this.semaphore.acquire();
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Releases the mutex, allowing another waiting task to proceed.
|
|
88
|
-
*/
|
|
89
|
-
release() {
|
|
90
|
-
this.semaphore.release();
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Runs a function while holding the mutex lock.
|
|
94
|
-
* Automatically acquires before and releases after the function execution.
|
|
95
|
-
*
|
|
96
|
-
* @param fn - The function to execute while holding the lock
|
|
97
|
-
* @returns The result of the function
|
|
98
|
-
*/
|
|
99
|
-
async runExclusive(fn) {
|
|
100
|
-
await this.acquire();
|
|
101
|
-
try {
|
|
102
|
-
return await fn();
|
|
103
|
-
} finally {
|
|
104
|
-
this.release();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
//#endregion
|
|
110
|
-
export { Mutex };
|
package/dist/pkce.mjs
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { base64url } from "jose";
|
|
2
|
-
|
|
3
|
-
//#region src/pkce.ts
|
|
4
|
-
/**
|
|
5
|
-
* Performs a timing-safe comparison of two strings to prevent timing attacks.
|
|
6
|
-
* This implementation is platform-agnostic, uses a constant-time algorithm,
|
|
7
|
-
* and correctly handles all Unicode characters by operating on their UTF-8 byte representation.
|
|
8
|
-
* It always takes a time proportional to the length of the expected string,
|
|
9
|
-
* regardless of where the strings differ, making it safe for comparing sensitive values.
|
|
10
|
-
*
|
|
11
|
-
* @param a - The first string to compare (often the expected, secret value).
|
|
12
|
-
* @param b - The second string to compare (often the user-provided value).
|
|
13
|
-
* @returns True if the strings are identical, false otherwise.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // Safe for comparing sensitive values like PKCE verifiers or tokens
|
|
18
|
-
* const isValid = await timingSafeCompare(receivedVerifier, expectedChallenge);
|
|
19
|
-
*
|
|
20
|
-
* // Safe for password hash verification
|
|
21
|
-
* const isValidPassword = timingSafeCompare(hashedInput, storedHash);
|
|
22
|
-
*
|
|
23
|
-
* // Returns false for different types or lengths without leaking timing info
|
|
24
|
-
* timingSafeCompare("abc", 123 as any); // false
|
|
25
|
-
* timingSafeCompare("abc", "abcd"); // false
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
const timingSafeCompare = (a, b) => {
|
|
29
|
-
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
30
|
-
const encoder = new TextEncoder();
|
|
31
|
-
const aBytes = encoder.encode(a);
|
|
32
|
-
const bBytes = encoder.encode(b);
|
|
33
|
-
let diff = aBytes.length ^ bBytes.length;
|
|
34
|
-
for (const [i, aByte] of aBytes.entries()) diff |= aByte ^ (bBytes[i] ?? 0);
|
|
35
|
-
return diff === 0;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Generates a cryptographically secure code verifier for PKCE.
|
|
39
|
-
* The verifier is a URL-safe base64-encoded string of random bytes.
|
|
40
|
-
*
|
|
41
|
-
* @param length - Length of the random buffer in bytes
|
|
42
|
-
* @returns Base64url-encoded verifier string
|
|
43
|
-
*/
|
|
44
|
-
const generateVerifier = (length) => {
|
|
45
|
-
const buffer = new Uint8Array(length);
|
|
46
|
-
crypto.getRandomValues(buffer);
|
|
47
|
-
return base64url.encode(buffer);
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Generates a code challenge from a verifier using the specified method.
|
|
51
|
-
* For 'S256', applies SHA-256 hash then base64url encoding.
|
|
52
|
-
* For 'plain', returns the verifier unchanged (not recommended for production).
|
|
53
|
-
*
|
|
54
|
-
* @param verifier - The code verifier string
|
|
55
|
-
* @param method - Challenge generation method
|
|
56
|
-
* @returns Promise resolving to the code challenge string
|
|
57
|
-
*/
|
|
58
|
-
const generateChallenge = async (verifier, method) => {
|
|
59
|
-
if (method === "plain") return verifier;
|
|
60
|
-
const data = new TextEncoder().encode(verifier);
|
|
61
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
62
|
-
return base64url.encode(new Uint8Array(hash));
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Generates a complete PKCE challenge for OAuth authorization requests.
|
|
66
|
-
* Creates a cryptographically secure verifier and corresponding S256 challenge.
|
|
67
|
-
* Validates that the generated verifier meets standard requirements (43-128 characters).
|
|
68
|
-
*
|
|
69
|
-
* @param length - Length of the random buffer in bytes (32-96 range to generate 43-128 character verifier)
|
|
70
|
-
* @returns Promise resolving to PKCE challenge data
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* const pkce = await generatePKCE()
|
|
75
|
-
*
|
|
76
|
-
* // Use challenge in authorization URL
|
|
77
|
-
* authUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
78
|
-
* authUrl.searchParams.set('code_challenge_method', pkce.method)
|
|
79
|
-
*
|
|
80
|
-
* // Store verifier for token exchange
|
|
81
|
-
* sessionStorage.setItem('code_verifier', pkce.verifier)
|
|
82
|
-
* ```
|
|
83
|
-
*
|
|
84
|
-
* @throws {RangeError} If length is outside valid range or generated verifier doesn't meet requirements
|
|
85
|
-
*/
|
|
86
|
-
const generatePKCE = async (length = 48) => {
|
|
87
|
-
if (!Number.isInteger(length) || length < 32 || length > 96) throw new RangeError("Random buffer length must be between 32 and 96 bytes (generates 43-128 character verifier)");
|
|
88
|
-
const verifier = generateVerifier(length);
|
|
89
|
-
if (verifier.length < 43 || verifier.length > 128) throw new Error("Generated verifier does not meet requirements");
|
|
90
|
-
if (!/^[A-Za-z0-9_-]+$/.test(verifier)) throw new Error("Generated verifier is not valid base64url format");
|
|
91
|
-
return {
|
|
92
|
-
verifier,
|
|
93
|
-
challenge: await generateChallenge(verifier, "S256"),
|
|
94
|
-
method: "S256"
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* Validates a PKCE code verifier against a previously generated challenge.
|
|
99
|
-
* Uses timing-safe comparison and timing normalization to prevent timing attacks.
|
|
100
|
-
* All validation paths take the same computational time regardless of input validity,
|
|
101
|
-
* making it resistant to timing-based side-channel attacks.
|
|
102
|
-
*
|
|
103
|
-
* @param verifier - The code verifier received from the client
|
|
104
|
-
* @param challenge - The code challenge stored during authorization
|
|
105
|
-
* @param method - The challenge method used during generation
|
|
106
|
-
* @returns Promise resolving to true if verifier matches challenge
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```ts
|
|
110
|
-
* // During token exchange
|
|
111
|
-
* const isValid = await validatePKCE(
|
|
112
|
-
* receivedVerifier,
|
|
113
|
-
* storedChallenge,
|
|
114
|
-
* 'S256'
|
|
115
|
-
* )
|
|
116
|
-
*
|
|
117
|
-
* if (!isValid) {
|
|
118
|
-
* throw new Error('Invalid PKCE verifier')
|
|
119
|
-
* }
|
|
120
|
-
* ```
|
|
121
|
-
*/
|
|
122
|
-
const validatePKCE = async (verifier, challenge, method = "S256") => {
|
|
123
|
-
const MIN_PROCESSING_TIME = 50;
|
|
124
|
-
const RANDOM_JITTER_MAX = 20;
|
|
125
|
-
const startTime = performance.now();
|
|
126
|
-
let isValid = false;
|
|
127
|
-
let hasEarlyFailure = false;
|
|
128
|
-
const normalizedVerifier = String(verifier || "");
|
|
129
|
-
const normalizedChallenge = String(challenge || "");
|
|
130
|
-
hasEarlyFailure = ![
|
|
131
|
-
typeof verifier === "string" && typeof challenge === "string" && verifier && challenge,
|
|
132
|
-
normalizedVerifier.length >= 43 && normalizedVerifier.length <= 128,
|
|
133
|
-
normalizedChallenge.length >= 43 && normalizedChallenge.length <= 128,
|
|
134
|
-
/^[A-Za-z0-9_-]+$/.test(normalizedVerifier),
|
|
135
|
-
/^[A-Za-z0-9_-]+$/.test(normalizedChallenge)
|
|
136
|
-
].every(Boolean);
|
|
137
|
-
const verifierToUse = hasEarlyFailure ? "dummyverifier_".repeat(6) : normalizedVerifier;
|
|
138
|
-
try {
|
|
139
|
-
const comparisonResult = timingSafeCompare(await generateChallenge(verifierToUse, method), hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge);
|
|
140
|
-
isValid = !hasEarlyFailure && comparisonResult;
|
|
141
|
-
} catch {
|
|
142
|
-
isValid = false;
|
|
143
|
-
}
|
|
144
|
-
const elapsed = performance.now() - startTime;
|
|
145
|
-
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
146
|
-
if (remainingTime > 0 || elapsed < MIN_PROCESSING_TIME) {
|
|
147
|
-
const jitterArray = new Uint32Array(1);
|
|
148
|
-
crypto.getRandomValues(jitterArray);
|
|
149
|
-
const jitter = (jitterArray[0] ?? 0) / 4294967295 * RANDOM_JITTER_MAX;
|
|
150
|
-
const totalDelay = Math.max(remainingTime, MIN_PROCESSING_TIME - elapsed) + jitter;
|
|
151
|
-
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
152
|
-
}
|
|
153
|
-
return isValid;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
//#endregion
|
|
157
|
-
export { generatePKCE, validatePKCE };
|