@draftlab/auth 0.4.1 → 0.6.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/adapters/{node.js → node.mjs} +2 -4
- package/dist/{allow.js → allow.mjs} +1 -1
- package/dist/{client.d.ts → client.d.mts} +47 -4
- package/dist/{client.js → client.mjs} +81 -10
- package/dist/{core.d.ts → core.d.mts} +10 -10
- package/dist/{core.js → core.mjs} +104 -56
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +3 -0
- package/dist/{keys.d.ts → keys.d.mts} +1 -1
- package/dist/{keys.js → keys.mjs} +6 -8
- package/dist/{pkce.js → pkce.mjs} +5 -10
- package/dist/plugin/{builder.d.ts → builder.d.mts} +1 -1
- package/dist/plugin/{manager.d.ts → manager.d.mts} +2 -2
- package/dist/plugin/{manager.js → manager.mjs} +1 -1
- package/dist/plugin/{plugin.d.ts → plugin.d.mts} +1 -1
- package/dist/plugin/{types.d.ts → types.d.mts} +1 -1
- package/dist/provider/apple.d.mts +105 -0
- package/dist/provider/apple.mjs +151 -0
- package/dist/provider/{code.d.ts → code.d.mts} +1 -1
- package/dist/provider/{code.js → code.mjs} +2 -3
- package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
- package/dist/provider/{discord.js → discord.mjs} +59 -1
- package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
- package/dist/provider/{facebook.js → facebook.mjs} +57 -1
- package/dist/provider/{github.d.ts → github.d.mts} +2 -2
- package/dist/provider/{github.js → github.mjs} +79 -1
- package/dist/provider/gitlab.d.mts +100 -0
- package/dist/provider/gitlab.mjs +128 -0
- package/dist/provider/{google.d.ts → google.d.mts} +2 -2
- package/dist/provider/{google.js → google.mjs} +45 -1
- package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
- package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
- package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
- package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
- package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
- package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
- package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
- package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/provider/{passkey.js → passkey.mjs} +8 -13
- package/dist/provider/{password.d.ts → password.d.mts} +1 -1
- package/dist/provider/{password.js → password.mjs} +31 -44
- package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
- package/dist/provider/reddit.d.mts +101 -0
- package/dist/provider/reddit.mjs +114 -0
- package/dist/provider/slack.d.mts +108 -0
- package/dist/provider/slack.mjs +125 -0
- package/dist/provider/spotify.d.mts +107 -0
- package/dist/provider/spotify.mjs +122 -0
- package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/provider/{totp.js → totp.mjs} +51 -14
- package/dist/provider/twitch.d.mts +102 -0
- package/dist/provider/twitch.mjs +118 -0
- package/dist/{random.js → random.mjs} +1 -2
- package/dist/revocation.d.mts +55 -0
- package/dist/revocation.mjs +63 -0
- package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
- package/dist/storage/{memory.js → memory.mjs} +3 -5
- package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
- package/dist/storage/storage.mjs +104 -0
- package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
- package/dist/storage/{turso.js → turso.mjs} +1 -1
- package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
- package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
- package/dist/{subject.d.ts → subject.d.mts} +1 -1
- package/dist/ui/{base.d.ts → base.d.mts} +1 -1
- package/dist/ui/{base.js → base.mjs} +1 -1
- package/dist/ui/{code.d.ts → code.d.mts} +1 -1
- package/dist/ui/{code.js → code.mjs} +3 -4
- package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
- package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/ui/{passkey.js → passkey.mjs} +2 -2
- package/dist/ui/{password.d.ts → password.d.mts} +1 -1
- package/dist/ui/{password.js → password.mjs} +3 -4
- package/dist/ui/{select.d.ts → select.d.mts} +1 -1
- package/dist/ui/{select.js → select.mjs} +2 -2
- package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/ui/{totp.js → totp.mjs} +2 -2
- package/dist/{util.js → util.mjs} +2 -5
- package/package.json +17 -16
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/storage/storage.js +0 -62
- /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
- /package/dist/{allow.d.ts → allow.d.mts} +0 -0
- /package/dist/{error.d.ts → error.d.mts} +0 -0
- /package/dist/{error.js → error.mjs} +0 -0
- /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
- /package/dist/plugin/{builder.js → builder.mjs} +0 -0
- /package/dist/plugin/{plugin.js → plugin.mjs} +0 -0
- /package/dist/plugin/{types.js → types.mjs} +0 -0
- /package/dist/provider/{provider.js → provider.mjs} +0 -0
- /package/dist/{random.d.ts → random.d.mts} +0 -0
- /package/dist/{subject.js → subject.mjs} +0 -0
- /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
- /package/dist/themes/{theme.js → theme.mjs} +0 -0
- /package/dist/{types.d.ts → types.d.mts} +0 -0
- /package/dist/{types.js → types.mjs} +0 -0
- /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
- /package/dist/ui/{form.js → form.mjs} +0 -0
- /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
- /package/dist/ui/{icon.js → icon.mjs} +0 -0
- /package/dist/{util.d.ts → util.d.mts} +0 -0
|
@@ -5,8 +5,7 @@ import { Readable } from "node:stream";
|
|
|
5
5
|
* Converts Node.js IncomingMessage to Web Standards Request
|
|
6
6
|
*/
|
|
7
7
|
const nodeRequestAdapter = (req) => {
|
|
8
|
-
const
|
|
9
|
-
const sanitizedHost = host.split(",")[0]?.trim();
|
|
8
|
+
const sanitizedHost = (req.headers.host || "localhost").split(",")[0]?.trim();
|
|
10
9
|
const url = new URL(req.url || "/", `http://${sanitizedHost}`);
|
|
11
10
|
const headers = new Headers();
|
|
12
11
|
for (const [key, value] of Object.entries(req.headers)) if (value !== void 0) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
@@ -49,8 +48,7 @@ const nodeResponseAdapter = async (response, res) => {
|
|
|
49
48
|
const createNodeHandler = (fetchHandler) => {
|
|
50
49
|
return (req, res) => {
|
|
51
50
|
try {
|
|
52
|
-
|
|
53
|
-
fetchHandler(request).then((response) => nodeResponseAdapter(response, res)).catch((error) => {
|
|
51
|
+
fetchHandler(nodeRequestAdapter(req)).then((response) => nodeResponseAdapter(response, res)).catch((error) => {
|
|
54
52
|
console.error("Handler error:", error instanceof Error ? error.message : "Unknown error");
|
|
55
53
|
if (!res.headersSent) {
|
|
56
54
|
res.statusCode = 500;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.
|
|
2
|
-
import { SubjectSchema } from "./subject.
|
|
1
|
+
import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.mjs";
|
|
2
|
+
import { SubjectSchema } from "./subject.mjs";
|
|
3
3
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
4
|
|
|
5
5
|
//#region src/client.d.ts
|
|
@@ -252,8 +252,24 @@ interface VerifyResult<T extends SubjectSchema> {
|
|
|
252
252
|
} }[keyof T];
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
-
* Options for
|
|
255
|
+
* Options for token revocation.
|
|
256
256
|
*/
|
|
257
|
+
interface RevokeOptions {
|
|
258
|
+
/**
|
|
259
|
+
* Optional hint about the token type.
|
|
260
|
+
* Can be "access_token" or "refresh_token".
|
|
261
|
+
*
|
|
262
|
+
* Helps the server optimize token lookup.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* {
|
|
267
|
+
* tokenTypeHint: "refresh_token"
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
tokenTypeHint?: "access_token" | "refresh_token";
|
|
272
|
+
}
|
|
257
273
|
/**
|
|
258
274
|
* Draft Auth client with OAuth 2.0 operations.
|
|
259
275
|
*/
|
|
@@ -392,6 +408,33 @@ interface Client {
|
|
|
392
408
|
* ```
|
|
393
409
|
*/
|
|
394
410
|
verify<T extends SubjectSchema>(subjects: T, token: string, options?: VerifyOptions): Promise<Result<VerifyResult<T>, InvalidRefreshTokenError | InvalidAccessTokenError | InvalidSubjectError>>;
|
|
411
|
+
/**
|
|
412
|
+
* Revoke a token (access or refresh token).
|
|
413
|
+
*
|
|
414
|
+
* Once revoked, the token cannot be used to access resources or refresh.
|
|
415
|
+
* Useful for implementing logout functionality.
|
|
416
|
+
*
|
|
417
|
+
* @param token - The token to revoke
|
|
418
|
+
* @param opts - Additional revocation options
|
|
419
|
+
* @returns Empty result on success
|
|
420
|
+
*
|
|
421
|
+
* @example Logout with refresh token revocation
|
|
422
|
+
* ```ts
|
|
423
|
+
* const result = await client.revoke(refreshToken, {
|
|
424
|
+
* tokenTypeHint: "refresh_token"
|
|
425
|
+
* })
|
|
426
|
+
*
|
|
427
|
+
* if (result.success) {
|
|
428
|
+
* // Token revoked successfully, user is logged out
|
|
429
|
+
* clearStoredTokens()
|
|
430
|
+
* redirectToHome()
|
|
431
|
+
* } else {
|
|
432
|
+
* // Revocation failed, but still clear tokens on client
|
|
433
|
+
* clearStoredTokens()
|
|
434
|
+
* }
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
revoke(token: string, opts?: RevokeOptions): Promise<Result<void>>;
|
|
395
438
|
}
|
|
396
439
|
/**
|
|
397
440
|
* Create a Draft Auth client.
|
|
@@ -409,4 +452,4 @@ interface Client {
|
|
|
409
452
|
*/
|
|
410
453
|
declare const createClient: (input: ClientInput) => Client;
|
|
411
454
|
//#endregion
|
|
412
|
-
export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
|
|
455
|
+
export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, RevokeOptions, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
|
|
@@ -1,9 +1,58 @@
|
|
|
1
|
-
import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.
|
|
2
|
-
import { generatePKCE } from "./pkce.
|
|
1
|
+
import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.mjs";
|
|
2
|
+
import { generatePKCE } from "./pkce.mjs";
|
|
3
3
|
import { createLocalJWKSet, errors, jwtVerify } from "jose";
|
|
4
4
|
|
|
5
5
|
//#region src/client.ts
|
|
6
6
|
/**
|
|
7
|
+
* Draft Auth client for OAuth 2.0 authentication.
|
|
8
|
+
*
|
|
9
|
+
* ## Quick Start
|
|
10
|
+
*
|
|
11
|
+
* First, create a client.
|
|
12
|
+
*
|
|
13
|
+
* ```ts title="client.ts"
|
|
14
|
+
* import { createClient } from "@draftlab/auth/client"
|
|
15
|
+
*
|
|
16
|
+
* const client = createClient({
|
|
17
|
+
* clientID: "my-client",
|
|
18
|
+
* issuer: "https://auth.myserver.com"
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Start the OAuth flow by calling `authorize`.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const result = await client.authorize(
|
|
26
|
+
* "https://myapp.com/callback",
|
|
27
|
+
* "code"
|
|
28
|
+
* )
|
|
29
|
+
* if (result.success) {
|
|
30
|
+
* window.location.href = result.data.url
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* When the user completes the flow, exchange the code for tokens.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const result = await client.exchange(code, redirectUri)
|
|
38
|
+
* if (result.success) {
|
|
39
|
+
* const { access, refresh } = result.data
|
|
40
|
+
* // Store tokens securely
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Verify tokens to get user information.
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* const result = await client.verify(subjects, accessToken)
|
|
48
|
+
* if (result.success) {
|
|
49
|
+
* // Access user properties: result.data.subject.properties
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @packageDocumentation
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
7
56
|
* Create a Draft Auth client.
|
|
8
57
|
*
|
|
9
58
|
* @param input - Client configuration
|
|
@@ -34,8 +83,7 @@ const createClient = (input) => {
|
|
|
34
83
|
const wk = await getIssuer();
|
|
35
84
|
const cached = jwksCache.get(issuer);
|
|
36
85
|
if (cached) return cached;
|
|
37
|
-
const
|
|
38
|
-
const result = createLocalJWKSet(keyset);
|
|
86
|
+
const result = createLocalJWKSet(await f(wk.jwks_uri).then((r) => r.json()));
|
|
39
87
|
jwksCache.set(issuer, result);
|
|
40
88
|
return result;
|
|
41
89
|
};
|
|
@@ -72,8 +120,7 @@ const createClient = (input) => {
|
|
|
72
120
|
},
|
|
73
121
|
async exchange(code, redirectURI, verifier) {
|
|
74
122
|
try {
|
|
75
|
-
const
|
|
76
|
-
const response = await f(wk.token_endpoint, {
|
|
123
|
+
const response = await f((await getIssuer()).token_endpoint, {
|
|
77
124
|
method: "POST",
|
|
78
125
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
79
126
|
body: new URLSearchParams({
|
|
@@ -124,8 +171,7 @@ const createClient = (input) => {
|
|
|
124
171
|
data: {}
|
|
125
172
|
};
|
|
126
173
|
} catch {}
|
|
127
|
-
const
|
|
128
|
-
const response = await f(wk.token_endpoint, {
|
|
174
|
+
const response = await f((await getIssuer()).token_endpoint, {
|
|
129
175
|
method: "POST",
|
|
130
176
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
131
177
|
body: new URLSearchParams({
|
|
@@ -155,8 +201,7 @@ const createClient = (input) => {
|
|
|
155
201
|
},
|
|
156
202
|
async verify(subjects, token, options) {
|
|
157
203
|
try {
|
|
158
|
-
const
|
|
159
|
-
const jwtResult = await jwtVerify(token, jwks, { issuer });
|
|
204
|
+
const jwtResult = await jwtVerify(token, await getJWKS(), { issuer });
|
|
160
205
|
const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
|
|
161
206
|
if (!validated?.issues && jwtResult.payload.mode === "access") return {
|
|
162
207
|
success: true,
|
|
@@ -200,6 +245,32 @@ const createClient = (input) => {
|
|
|
200
245
|
error: new InvalidAccessTokenError()
|
|
201
246
|
};
|
|
202
247
|
}
|
|
248
|
+
},
|
|
249
|
+
async revoke(token, opts) {
|
|
250
|
+
try {
|
|
251
|
+
const wk = await getIssuer();
|
|
252
|
+
const body = new URLSearchParams({
|
|
253
|
+
token,
|
|
254
|
+
...opts?.tokenTypeHint ? { token_type_hint: opts.tokenTypeHint } : {}
|
|
255
|
+
});
|
|
256
|
+
if ((await f(wk.token_endpoint.replace("/token", "/revoke"), {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
259
|
+
body: body.toString()
|
|
260
|
+
})).ok) return {
|
|
261
|
+
success: true,
|
|
262
|
+
data: void 0
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error: /* @__PURE__ */ new Error("Failed to revoke token")
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error
|
|
272
|
+
};
|
|
273
|
+
}
|
|
203
274
|
}
|
|
204
275
|
};
|
|
205
276
|
return client;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { AllowCheckInput } from "./allow.
|
|
2
|
-
import { UnknownStateError } from "./error.
|
|
3
|
-
import { Prettify } from "./util.
|
|
4
|
-
import { SubjectPayload, SubjectSchema } from "./subject.
|
|
5
|
-
import { StorageAdapter } from "./storage/storage.
|
|
6
|
-
import { Plugin } from "./plugin/types.
|
|
7
|
-
import { Provider } from "./provider/provider.
|
|
8
|
-
import { Theme } from "./themes/theme.
|
|
1
|
+
import { AllowCheckInput } from "./allow.mjs";
|
|
2
|
+
import { UnknownStateError } from "./error.mjs";
|
|
3
|
+
import { Prettify } from "./util.mjs";
|
|
4
|
+
import { SubjectPayload, SubjectSchema } from "./subject.mjs";
|
|
5
|
+
import { StorageAdapter } from "./storage/storage.mjs";
|
|
6
|
+
import { Plugin } from "./plugin/types.mjs";
|
|
7
|
+
import { Provider } from "./provider/provider.mjs";
|
|
8
|
+
import { Theme } from "./themes/theme.mjs";
|
|
9
9
|
import { Router } from "@draftlab/auth-router";
|
|
10
10
|
|
|
11
11
|
//#region src/core.d.ts
|
|
@@ -13,11 +13,11 @@ import { Router } from "@draftlab/auth-router";
|
|
|
13
13
|
/**
|
|
14
14
|
* Sets the subject payload in the JWT token and returns the response.
|
|
15
15
|
*/
|
|
16
|
-
interface OnSuccessResponder<T extends {
|
|
16
|
+
interface OnSuccessResponder<T$1 extends {
|
|
17
17
|
type: string;
|
|
18
18
|
properties: unknown;
|
|
19
19
|
}> {
|
|
20
|
-
subject<Type extends T["type"]>(type: Type, properties: Extract<T, {
|
|
20
|
+
subject<Type extends T$1["type"]>(type: Type, properties: Extract<T$1, {
|
|
21
21
|
type: Type;
|
|
22
22
|
}>["properties"], opts?: {
|
|
23
23
|
ttl?: {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { getRelativeUrl, lazy } from "./util.
|
|
2
|
-
import { defaultAllowCheck } from "./allow.
|
|
3
|
-
import { MissingParameterError, OauthError, UnauthorizedClientError, UnknownStateError } from "./error.
|
|
4
|
-
import { validatePKCE } from "./pkce.
|
|
5
|
-
import { generateSecureToken } from "./random.
|
|
6
|
-
import { Storage } from "./storage/storage.
|
|
7
|
-
import { encryptionKeys, signingKeys } from "./keys.
|
|
8
|
-
import { PluginManager } from "./plugin/manager.
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import { getRelativeUrl, lazy } from "./util.mjs";
|
|
2
|
+
import { defaultAllowCheck } from "./allow.mjs";
|
|
3
|
+
import { MissingParameterError, OauthError, UnauthorizedClientError, UnknownStateError } from "./error.mjs";
|
|
4
|
+
import { validatePKCE } from "./pkce.mjs";
|
|
5
|
+
import { generateSecureToken } from "./random.mjs";
|
|
6
|
+
import { Storage } from "./storage/storage.mjs";
|
|
7
|
+
import { encryptionKeys, signingKeys } from "./keys.mjs";
|
|
8
|
+
import { PluginManager } from "./plugin/manager.mjs";
|
|
9
|
+
import { Revocation } from "./revocation.mjs";
|
|
10
|
+
import { setTheme } from "./themes/theme.mjs";
|
|
11
|
+
import { Select } from "./ui/select.mjs";
|
|
11
12
|
import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
|
|
12
13
|
import { Router } from "@draftlab/auth-router";
|
|
13
14
|
import { deleteCookie, getCookie, setCookie } from "@draftlab/auth-router/cookies";
|
|
@@ -15,6 +16,33 @@ import { cors } from "@draftlab/auth-router/middleware/cors";
|
|
|
15
16
|
|
|
16
17
|
//#region src/core.ts
|
|
17
18
|
/**
|
|
19
|
+
* Core issuer implementation.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Performs an operation with guaranteed minimum execution time.
|
|
23
|
+
* Adds random jitter to prevent timing-based attacks even if operation completes quickly.
|
|
24
|
+
*
|
|
25
|
+
* Used for validating sensitive data where timing differences could leak information
|
|
26
|
+
* (e.g., authorization codes, refresh tokens).
|
|
27
|
+
*
|
|
28
|
+
* @param fn - Async function to execute
|
|
29
|
+
* @param minTimeMs - Minimum execution time in milliseconds (default: 100ms)
|
|
30
|
+
* @returns Result of the function, guaranteed to take at least minTimeMs
|
|
31
|
+
*/
|
|
32
|
+
const normalizeTimingAsync = async (fn, minTimeMs = 100) => {
|
|
33
|
+
const startTime = performance.now();
|
|
34
|
+
const result = await fn();
|
|
35
|
+
const elapsed = performance.now() - startTime;
|
|
36
|
+
const remainingTime = Math.max(0, minTimeMs - elapsed);
|
|
37
|
+
if (remainingTime > 0) {
|
|
38
|
+
const jitterBuffer = new Uint32Array(1);
|
|
39
|
+
crypto.getRandomValues(jitterBuffer);
|
|
40
|
+
const totalDelay = remainingTime + (jitterBuffer[0] ?? 0) / 4294967295 * 20;
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
18
46
|
* Determines if the incoming request is using HTTPS protocol.
|
|
19
47
|
* Checks multiple proxy headers to handle load balancers and reverse proxies.
|
|
20
48
|
*
|
|
@@ -60,8 +88,7 @@ const issuer = (input) => {
|
|
|
60
88
|
const issuer$1 = (ctx) => {
|
|
61
89
|
const baseUrl = new URL(getRelativeUrl(ctx, "/"));
|
|
62
90
|
if (input.basePath) {
|
|
63
|
-
|
|
64
|
-
baseUrl.pathname = normalizedBasePath.replace(/\/$/, "");
|
|
91
|
+
baseUrl.pathname = (input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`).replace(/\/$/, "");
|
|
65
92
|
return baseUrl.href;
|
|
66
93
|
}
|
|
67
94
|
return baseUrl.origin;
|
|
@@ -90,12 +117,9 @@ const issuer = (input) => {
|
|
|
90
117
|
*/
|
|
91
118
|
const resolveSubject = async (type, properties) => {
|
|
92
119
|
const jsonString = JSON.stringify(properties);
|
|
93
|
-
const
|
|
94
|
-
const data = encoder.encode(jsonString);
|
|
120
|
+
const data = new TextEncoder().encode(jsonString);
|
|
95
121
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
96
|
-
|
|
97
|
-
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
98
|
-
return `${type}:${hashHex.slice(0, 16)}`;
|
|
122
|
+
return `${type}:${Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
|
|
99
123
|
};
|
|
100
124
|
/**
|
|
101
125
|
* Generates access and refresh tokens for OAuth 2.0.
|
|
@@ -128,23 +152,21 @@ const issuer = (input) => {
|
|
|
128
152
|
if (!signingKeyData) throw new Error("Signing key not available");
|
|
129
153
|
const now = Math.floor(Date.now() / 1e3);
|
|
130
154
|
if (!value.clientID.trim()) throw new Error("Invalid audience: client ID cannot be empty");
|
|
131
|
-
const accessPayload = {
|
|
132
|
-
type: value.type,
|
|
133
|
-
properties: value.properties,
|
|
134
|
-
sub: value.subject,
|
|
135
|
-
aud: value.clientID,
|
|
136
|
-
iss: issuer$1(ctx),
|
|
137
|
-
exp: now + value.ttl.access,
|
|
138
|
-
iat: now,
|
|
139
|
-
mode: "access"
|
|
140
|
-
};
|
|
141
|
-
const access = await new SignJWT(accessPayload).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
|
|
142
|
-
alg: signingKeyData.alg,
|
|
143
|
-
kid: signingKeyData.id,
|
|
144
|
-
typ: "JWT"
|
|
145
|
-
}).sign(signingKeyData.private);
|
|
146
155
|
return {
|
|
147
|
-
access
|
|
156
|
+
access: await new SignJWT({
|
|
157
|
+
type: value.type,
|
|
158
|
+
properties: value.properties,
|
|
159
|
+
sub: value.subject,
|
|
160
|
+
aud: value.clientID,
|
|
161
|
+
iss: issuer$1(ctx),
|
|
162
|
+
exp: now + value.ttl.access,
|
|
163
|
+
iat: now,
|
|
164
|
+
mode: "access"
|
|
165
|
+
}).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
|
|
166
|
+
alg: signingKeyData.alg,
|
|
167
|
+
kid: signingKeyData.id,
|
|
168
|
+
typ: "JWT"
|
|
169
|
+
}).sign(signingKeyData.private),
|
|
148
170
|
refresh: [value.subject, refreshToken].join(":"),
|
|
149
171
|
expiresIn: Math.floor(now + value.ttl.access - Date.now() / 1e3)
|
|
150
172
|
};
|
|
@@ -227,8 +249,7 @@ const issuer = (input) => {
|
|
|
227
249
|
},
|
|
228
250
|
async set(ctx, key, maxAge, value) {
|
|
229
251
|
const isHttps = isHttpsRequest(ctx);
|
|
230
|
-
|
|
231
|
-
setCookie(ctx, key, encryptedValue, {
|
|
252
|
+
setCookie(ctx, key, await encrypt(value), {
|
|
232
253
|
maxAge,
|
|
233
254
|
httpOnly: true,
|
|
234
255
|
secure: isHttps,
|
|
@@ -238,13 +259,12 @@ const issuer = (input) => {
|
|
|
238
259
|
},
|
|
239
260
|
async get(ctx, key) {
|
|
240
261
|
const raw = getCookie(ctx, key);
|
|
241
|
-
if (!raw) return
|
|
262
|
+
if (!raw) return;
|
|
242
263
|
try {
|
|
243
|
-
|
|
244
|
-
return decrypted;
|
|
264
|
+
return await decrypt(raw);
|
|
245
265
|
} catch {
|
|
246
266
|
deleteCookie(ctx, key, { path: input.basePath || "/" });
|
|
247
|
-
return
|
|
267
|
+
return;
|
|
248
268
|
}
|
|
249
269
|
},
|
|
250
270
|
async unset(ctx, key) {
|
|
@@ -281,8 +301,7 @@ const issuer = (input) => {
|
|
|
281
301
|
credentials: false
|
|
282
302
|
})],
|
|
283
303
|
handler: async (c) => {
|
|
284
|
-
const
|
|
285
|
-
const jwksDocument = { keys: signingKeys$1.map((keyInfo) => ({
|
|
304
|
+
const jwksDocument = { keys: (await allSigning()).map((keyInfo) => ({
|
|
286
305
|
...keyInfo.jwk,
|
|
287
306
|
alg: keyInfo.alg,
|
|
288
307
|
exp: keyInfo.expired ? Math.floor(keyInfo.expired.getTime() / 1e3) : void 0
|
|
@@ -326,19 +345,20 @@ const issuer = (input) => {
|
|
|
326
345
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
327
346
|
}
|
|
328
347
|
const key = ["oauth:code", code.toString()];
|
|
329
|
-
const payload = await
|
|
330
|
-
|
|
348
|
+
const { isValid, payload } = await normalizeTimingAsync(async () => {
|
|
349
|
+
const data = await Storage.get(storage, key);
|
|
350
|
+
const redirectUri = form.get("redirect_uri");
|
|
351
|
+
const clientId = form.get("client_id");
|
|
352
|
+
const valid = !!(data && data.redirectURI === redirectUri && data.clientID === clientId);
|
|
353
|
+
return {
|
|
354
|
+
isValid: valid,
|
|
355
|
+
payload: valid ? data : void 0
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
if (!isValid || !payload) {
|
|
331
359
|
const error$1 = new OauthError("invalid_grant", "Authorization code has been used or expired");
|
|
332
360
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
333
361
|
}
|
|
334
|
-
if (payload.redirectURI !== form.get("redirect_uri")) {
|
|
335
|
-
const error$1 = new OauthError("invalid_redirect_uri", "Redirect URI mismatch");
|
|
336
|
-
return c.json(error$1.toJSON(), { status: 400 });
|
|
337
|
-
}
|
|
338
|
-
if (payload.clientID !== form.get("client_id")) {
|
|
339
|
-
const error$1 = new OauthError("unauthorized_client", "Client is not authorized to use this authorization code");
|
|
340
|
-
return c.json(error$1.toJSON(), { status: 400 });
|
|
341
|
-
}
|
|
342
362
|
if (payload.pkce) {
|
|
343
363
|
const codeVerifier = form.get("code_verifier")?.toString();
|
|
344
364
|
if (!codeVerifier) {
|
|
@@ -365,7 +385,12 @@ const issuer = (input) => {
|
|
|
365
385
|
const error$1 = new OauthError("invalid_request", "Missing refresh_token");
|
|
366
386
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
367
387
|
}
|
|
368
|
-
const
|
|
388
|
+
const refreshTokenStr = refreshToken.toString();
|
|
389
|
+
if (await Revocation.isRevoked(storage, refreshTokenStr)) {
|
|
390
|
+
const error$1 = new OauthError("invalid_grant", "Refresh token has been revoked");
|
|
391
|
+
return c.json(error$1.toJSON(), { status: 400 });
|
|
392
|
+
}
|
|
393
|
+
const splits = refreshTokenStr.split(":");
|
|
369
394
|
const token = splits.pop();
|
|
370
395
|
if (!token) throw new Error("Invalid refresh token format");
|
|
371
396
|
const subject = splits.join(":");
|
|
@@ -398,7 +423,6 @@ const issuer = (input) => {
|
|
|
398
423
|
payload.properties = refreshResult.properties;
|
|
399
424
|
if (refreshResult.subject) payload.subject = refreshResult.subject;
|
|
400
425
|
if (refreshResult.scopes) payload.scopes = refreshResult.scopes;
|
|
401
|
-
payload.properties = refreshResult.properties;
|
|
402
426
|
} catch {
|
|
403
427
|
return c.json({
|
|
404
428
|
error: "server_error",
|
|
@@ -440,6 +464,31 @@ const issuer = (input) => {
|
|
|
440
464
|
}, { status: 400 });
|
|
441
465
|
}
|
|
442
466
|
});
|
|
467
|
+
app.post("/revoke", {
|
|
468
|
+
middleware: [cors({
|
|
469
|
+
origin: "*",
|
|
470
|
+
allowHeaders: ["Content-Type"],
|
|
471
|
+
allowMethods: ["POST"],
|
|
472
|
+
credentials: false
|
|
473
|
+
})],
|
|
474
|
+
handler: async (c) => {
|
|
475
|
+
const token = (await c.formData()).get("token")?.toString();
|
|
476
|
+
if (!token) {
|
|
477
|
+
const error$1 = new OauthError("invalid_request", "Missing token parameter");
|
|
478
|
+
return c.json(error$1.toJSON(), { status: 400 });
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const expiresAt = Date.now() + ttlRefresh * 1e3;
|
|
482
|
+
await Revocation.revoke(storage, token, expiresAt);
|
|
483
|
+
return c.json({});
|
|
484
|
+
} catch (_err) {
|
|
485
|
+
return c.json({
|
|
486
|
+
error: "server_error",
|
|
487
|
+
error_description: "Token revocation failed"
|
|
488
|
+
}, { status: 500 });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
443
492
|
app.get("/authorize", async (c) => {
|
|
444
493
|
const provider = c.query("provider");
|
|
445
494
|
const response_type = c.query("response_type");
|
|
@@ -449,14 +498,13 @@ const issuer = (input) => {
|
|
|
449
498
|
const audience = c.query("audience");
|
|
450
499
|
const code_challenge = c.query("code_challenge");
|
|
451
500
|
const code_challenge_method = c.query("code_challenge_method");
|
|
452
|
-
const scope = c.query("scope");
|
|
453
501
|
const authorization = {
|
|
454
502
|
response_type,
|
|
455
503
|
redirect_uri,
|
|
456
504
|
state,
|
|
457
505
|
client_id,
|
|
458
506
|
audience,
|
|
459
|
-
scope,
|
|
507
|
+
scope: c.query("scope"),
|
|
460
508
|
...code_challenge && code_challenge_method && { pkce: {
|
|
461
509
|
challenge: code_challenge,
|
|
462
510
|
method: code_challenge_method
|
|
@@ -472,7 +520,7 @@ const issuer = (input) => {
|
|
|
472
520
|
redirectURI: redirect_uri,
|
|
473
521
|
audience
|
|
474
522
|
}, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
|
|
475
|
-
await auth.set(c, "authorization",
|
|
523
|
+
await auth.set(c, "authorization", 900, authorization);
|
|
476
524
|
if (provider) return c.redirect(`${provider}/authorize`);
|
|
477
525
|
const availableProviders = Object.keys(input.providers);
|
|
478
526
|
if (availableProviders.length === 1) return c.redirect(`${availableProviders[0]}/authorize`);
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { generateSecureToken } from "./random.
|
|
2
|
-
import { Storage } from "./storage/storage.
|
|
1
|
+
import { generateSecureToken } from "./random.mjs";
|
|
2
|
+
import { Storage } from "./storage/storage.mjs";
|
|
3
3
|
import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, importSPKI } from "jose";
|
|
4
4
|
|
|
5
5
|
//#region src/keys.ts
|
|
@@ -63,7 +63,7 @@ const signingKeys = async (storage) => {
|
|
|
63
63
|
const jwk = await exportJWK(key.publicKey);
|
|
64
64
|
jwk.kid = serialized.id;
|
|
65
65
|
jwk.use = "sig";
|
|
66
|
-
|
|
66
|
+
return [{
|
|
67
67
|
id: serialized.id,
|
|
68
68
|
alg: signingAlg,
|
|
69
69
|
created: new Date(serialized.created),
|
|
@@ -71,8 +71,7 @@ const signingKeys = async (storage) => {
|
|
|
71
71
|
public: key.publicKey,
|
|
72
72
|
private: key.privateKey,
|
|
73
73
|
jwk
|
|
74
|
-
};
|
|
75
|
-
return [newKeyPair, ...results];
|
|
74
|
+
}, ...results];
|
|
76
75
|
};
|
|
77
76
|
/**
|
|
78
77
|
* Loads or generates encryption keys for token encryption operations.
|
|
@@ -124,7 +123,7 @@ const encryptionKeys = async (storage) => {
|
|
|
124
123
|
await Storage.set(storage, ["encryption:key", serialized.id], serialized);
|
|
125
124
|
const jwk = await exportJWK(key.publicKey);
|
|
126
125
|
jwk.kid = serialized.id;
|
|
127
|
-
|
|
126
|
+
return [{
|
|
128
127
|
id: serialized.id,
|
|
129
128
|
alg: encryptionAlg,
|
|
130
129
|
created: new Date(serialized.created),
|
|
@@ -132,8 +131,7 @@ const encryptionKeys = async (storage) => {
|
|
|
132
131
|
public: key.publicKey,
|
|
133
132
|
private: key.privateKey,
|
|
134
133
|
jwk
|
|
135
|
-
};
|
|
136
|
-
return [newKeyPair, ...results];
|
|
134
|
+
}, ...results];
|
|
137
135
|
};
|
|
138
136
|
|
|
139
137
|
//#endregion
|
|
@@ -57,8 +57,7 @@ const generateVerifier = (length) => {
|
|
|
57
57
|
*/
|
|
58
58
|
const generateChallenge = async (verifier, method) => {
|
|
59
59
|
if (method === "plain") return verifier;
|
|
60
|
-
const
|
|
61
|
-
const data = encoder.encode(verifier);
|
|
60
|
+
const data = new TextEncoder().encode(verifier);
|
|
62
61
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
63
62
|
return base64url.encode(new Uint8Array(hash));
|
|
64
63
|
};
|
|
@@ -89,10 +88,9 @@ const generatePKCE = async (length = 48) => {
|
|
|
89
88
|
const verifier = generateVerifier(length);
|
|
90
89
|
if (verifier.length < 43 || verifier.length > 128) throw new Error("Generated verifier does not meet requirements");
|
|
91
90
|
if (!/^[A-Za-z0-9_-]+$/.test(verifier)) throw new Error("Generated verifier is not valid base64url format");
|
|
92
|
-
const challenge = await generateChallenge(verifier, "S256");
|
|
93
91
|
return {
|
|
94
92
|
verifier,
|
|
95
|
-
challenge,
|
|
93
|
+
challenge: await generateChallenge(verifier, "S256"),
|
|
96
94
|
method: "S256"
|
|
97
95
|
};
|
|
98
96
|
};
|
|
@@ -129,19 +127,16 @@ const validatePKCE = async (verifier, challenge, method = "S256") => {
|
|
|
129
127
|
let hasEarlyFailure = false;
|
|
130
128
|
const normalizedVerifier = String(verifier || "");
|
|
131
129
|
const normalizedChallenge = String(challenge || "");
|
|
132
|
-
|
|
130
|
+
hasEarlyFailure = ![
|
|
133
131
|
typeof verifier === "string" && typeof challenge === "string" && verifier && challenge,
|
|
134
132
|
normalizedVerifier.length >= 43 && normalizedVerifier.length <= 128,
|
|
135
133
|
normalizedChallenge.length >= 43 && normalizedChallenge.length <= 128,
|
|
136
134
|
/^[A-Za-z0-9_-]+$/.test(normalizedVerifier),
|
|
137
135
|
/^[A-Za-z0-9_-]+$/.test(normalizedChallenge)
|
|
138
|
-
];
|
|
139
|
-
hasEarlyFailure = !validations.every(Boolean);
|
|
136
|
+
].every(Boolean);
|
|
140
137
|
const verifierToUse = hasEarlyFailure ? "dummyverifier_".repeat(6) : normalizedVerifier;
|
|
141
138
|
try {
|
|
142
|
-
const
|
|
143
|
-
const challengeToCompare = hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge;
|
|
144
|
-
const comparisonResult = timingSafeCompare(generatedChallenge, challengeToCompare);
|
|
139
|
+
const comparisonResult = timingSafeCompare(await generateChallenge(verifierToUse, method), hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge);
|
|
145
140
|
isValid = !hasEarlyFailure && comparisonResult;
|
|
146
141
|
} catch {
|
|
147
142
|
isValid = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { StorageAdapter } from "../storage/storage.
|
|
2
|
-
import { Plugin } from "./types.
|
|
1
|
+
import { StorageAdapter } from "../storage/storage.mjs";
|
|
2
|
+
import { Plugin } from "./types.mjs";
|
|
3
3
|
import { Router } from "@draftlab/auth-router";
|
|
4
4
|
|
|
5
5
|
//#region src/plugin/manager.d.ts
|