@draftlab/auth 0.4.1 → 0.5.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} +2 -2
- package/dist/{client.js → client.mjs} +55 -10
- package/dist/{core.d.ts → core.d.mts} +10 -10
- package/dist/{core.js → core.mjs} +72 -55
- 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/{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/{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/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/provider/{totp.js → totp.mjs} +51 -14
- package/dist/{random.js → random.mjs} +1 -2
- 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
|
|
@@ -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,
|
|
@@ -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,13 @@
|
|
|
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 { setTheme } from "./themes/theme.
|
|
10
|
-
import { Select } from "./ui/select.
|
|
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 { setTheme } from "./themes/theme.mjs";
|
|
10
|
+
import { Select } from "./ui/select.mjs";
|
|
11
11
|
import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
|
|
12
12
|
import { Router } from "@draftlab/auth-router";
|
|
13
13
|
import { deleteCookie, getCookie, setCookie } from "@draftlab/auth-router/cookies";
|
|
@@ -15,6 +15,33 @@ import { cors } from "@draftlab/auth-router/middleware/cors";
|
|
|
15
15
|
|
|
16
16
|
//#region src/core.ts
|
|
17
17
|
/**
|
|
18
|
+
* Core issuer implementation.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Performs an operation with guaranteed minimum execution time.
|
|
22
|
+
* Adds random jitter to prevent timing-based attacks even if operation completes quickly.
|
|
23
|
+
*
|
|
24
|
+
* Used for validating sensitive data where timing differences could leak information
|
|
25
|
+
* (e.g., authorization codes, refresh tokens).
|
|
26
|
+
*
|
|
27
|
+
* @param fn - Async function to execute
|
|
28
|
+
* @param minTimeMs - Minimum execution time in milliseconds (default: 100ms)
|
|
29
|
+
* @returns Result of the function, guaranteed to take at least minTimeMs
|
|
30
|
+
*/
|
|
31
|
+
const normalizeTimingAsync = async (fn, minTimeMs = 100) => {
|
|
32
|
+
const startTime = performance.now();
|
|
33
|
+
const result = await fn();
|
|
34
|
+
const elapsed = performance.now() - startTime;
|
|
35
|
+
const remainingTime = Math.max(0, minTimeMs - elapsed);
|
|
36
|
+
if (remainingTime > 0) {
|
|
37
|
+
const jitterBuffer = new Uint32Array(1);
|
|
38
|
+
crypto.getRandomValues(jitterBuffer);
|
|
39
|
+
const totalDelay = remainingTime + (jitterBuffer[0] ?? 0) / 4294967295 * 20;
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
18
45
|
* Determines if the incoming request is using HTTPS protocol.
|
|
19
46
|
* Checks multiple proxy headers to handle load balancers and reverse proxies.
|
|
20
47
|
*
|
|
@@ -60,8 +87,7 @@ const issuer = (input) => {
|
|
|
60
87
|
const issuer$1 = (ctx) => {
|
|
61
88
|
const baseUrl = new URL(getRelativeUrl(ctx, "/"));
|
|
62
89
|
if (input.basePath) {
|
|
63
|
-
|
|
64
|
-
baseUrl.pathname = normalizedBasePath.replace(/\/$/, "");
|
|
90
|
+
baseUrl.pathname = (input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`).replace(/\/$/, "");
|
|
65
91
|
return baseUrl.href;
|
|
66
92
|
}
|
|
67
93
|
return baseUrl.origin;
|
|
@@ -90,12 +116,9 @@ const issuer = (input) => {
|
|
|
90
116
|
*/
|
|
91
117
|
const resolveSubject = async (type, properties) => {
|
|
92
118
|
const jsonString = JSON.stringify(properties);
|
|
93
|
-
const
|
|
94
|
-
const data = encoder.encode(jsonString);
|
|
119
|
+
const data = new TextEncoder().encode(jsonString);
|
|
95
120
|
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)}`;
|
|
121
|
+
return `${type}:${Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
|
|
99
122
|
};
|
|
100
123
|
/**
|
|
101
124
|
* Generates access and refresh tokens for OAuth 2.0.
|
|
@@ -128,23 +151,21 @@ const issuer = (input) => {
|
|
|
128
151
|
if (!signingKeyData) throw new Error("Signing key not available");
|
|
129
152
|
const now = Math.floor(Date.now() / 1e3);
|
|
130
153
|
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
154
|
return {
|
|
147
|
-
access
|
|
155
|
+
access: await new SignJWT({
|
|
156
|
+
type: value.type,
|
|
157
|
+
properties: value.properties,
|
|
158
|
+
sub: value.subject,
|
|
159
|
+
aud: value.clientID,
|
|
160
|
+
iss: issuer$1(ctx),
|
|
161
|
+
exp: now + value.ttl.access,
|
|
162
|
+
iat: now,
|
|
163
|
+
mode: "access"
|
|
164
|
+
}).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
|
|
165
|
+
alg: signingKeyData.alg,
|
|
166
|
+
kid: signingKeyData.id,
|
|
167
|
+
typ: "JWT"
|
|
168
|
+
}).sign(signingKeyData.private),
|
|
148
169
|
refresh: [value.subject, refreshToken].join(":"),
|
|
149
170
|
expiresIn: Math.floor(now + value.ttl.access - Date.now() / 1e3)
|
|
150
171
|
};
|
|
@@ -227,8 +248,7 @@ const issuer = (input) => {
|
|
|
227
248
|
},
|
|
228
249
|
async set(ctx, key, maxAge, value) {
|
|
229
250
|
const isHttps = isHttpsRequest(ctx);
|
|
230
|
-
|
|
231
|
-
setCookie(ctx, key, encryptedValue, {
|
|
251
|
+
setCookie(ctx, key, await encrypt(value), {
|
|
232
252
|
maxAge,
|
|
233
253
|
httpOnly: true,
|
|
234
254
|
secure: isHttps,
|
|
@@ -238,13 +258,12 @@ const issuer = (input) => {
|
|
|
238
258
|
},
|
|
239
259
|
async get(ctx, key) {
|
|
240
260
|
const raw = getCookie(ctx, key);
|
|
241
|
-
if (!raw) return
|
|
261
|
+
if (!raw) return;
|
|
242
262
|
try {
|
|
243
|
-
|
|
244
|
-
return decrypted;
|
|
263
|
+
return await decrypt(raw);
|
|
245
264
|
} catch {
|
|
246
265
|
deleteCookie(ctx, key, { path: input.basePath || "/" });
|
|
247
|
-
return
|
|
266
|
+
return;
|
|
248
267
|
}
|
|
249
268
|
},
|
|
250
269
|
async unset(ctx, key) {
|
|
@@ -281,8 +300,7 @@ const issuer = (input) => {
|
|
|
281
300
|
credentials: false
|
|
282
301
|
})],
|
|
283
302
|
handler: async (c) => {
|
|
284
|
-
const
|
|
285
|
-
const jwksDocument = { keys: signingKeys$1.map((keyInfo) => ({
|
|
303
|
+
const jwksDocument = { keys: (await allSigning()).map((keyInfo) => ({
|
|
286
304
|
...keyInfo.jwk,
|
|
287
305
|
alg: keyInfo.alg,
|
|
288
306
|
exp: keyInfo.expired ? Math.floor(keyInfo.expired.getTime() / 1e3) : void 0
|
|
@@ -326,19 +344,20 @@ const issuer = (input) => {
|
|
|
326
344
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
327
345
|
}
|
|
328
346
|
const key = ["oauth:code", code.toString()];
|
|
329
|
-
const payload = await
|
|
330
|
-
|
|
347
|
+
const { isValid, payload } = await normalizeTimingAsync(async () => {
|
|
348
|
+
const data = await Storage.get(storage, key);
|
|
349
|
+
const redirectUri = form.get("redirect_uri");
|
|
350
|
+
const clientId = form.get("client_id");
|
|
351
|
+
const valid = !!(data && data.redirectURI === redirectUri && data.clientID === clientId);
|
|
352
|
+
return {
|
|
353
|
+
isValid: valid,
|
|
354
|
+
payload: valid ? data : void 0
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
if (!isValid || !payload) {
|
|
331
358
|
const error$1 = new OauthError("invalid_grant", "Authorization code has been used or expired");
|
|
332
359
|
return c.json(error$1.toJSON(), { status: 400 });
|
|
333
360
|
}
|
|
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
361
|
if (payload.pkce) {
|
|
343
362
|
const codeVerifier = form.get("code_verifier")?.toString();
|
|
344
363
|
if (!codeVerifier) {
|
|
@@ -398,7 +417,6 @@ const issuer = (input) => {
|
|
|
398
417
|
payload.properties = refreshResult.properties;
|
|
399
418
|
if (refreshResult.subject) payload.subject = refreshResult.subject;
|
|
400
419
|
if (refreshResult.scopes) payload.scopes = refreshResult.scopes;
|
|
401
|
-
payload.properties = refreshResult.properties;
|
|
402
420
|
} catch {
|
|
403
421
|
return c.json({
|
|
404
422
|
error: "server_error",
|
|
@@ -449,14 +467,13 @@ const issuer = (input) => {
|
|
|
449
467
|
const audience = c.query("audience");
|
|
450
468
|
const code_challenge = c.query("code_challenge");
|
|
451
469
|
const code_challenge_method = c.query("code_challenge_method");
|
|
452
|
-
const scope = c.query("scope");
|
|
453
470
|
const authorization = {
|
|
454
471
|
response_type,
|
|
455
472
|
redirect_uri,
|
|
456
473
|
state,
|
|
457
474
|
client_id,
|
|
458
475
|
audience,
|
|
459
|
-
scope,
|
|
476
|
+
scope: c.query("scope"),
|
|
460
477
|
...code_challenge && code_challenge_method && { pkce: {
|
|
461
478
|
challenge: code_challenge,
|
|
462
479
|
method: code_challenge_method
|
|
@@ -472,7 +489,7 @@ const issuer = (input) => {
|
|
|
472
489
|
redirectURI: redirect_uri,
|
|
473
490
|
audience
|
|
474
491
|
}, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
|
|
475
|
-
await auth.set(c, "authorization",
|
|
492
|
+
await auth.set(c, "authorization", 900, authorization);
|
|
476
493
|
if (provider) return c.redirect(`${provider}/authorize`);
|
|
477
494
|
const availableProviders = Object.keys(input.providers);
|
|
478
495
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateUnbiasedDigits, timingSafeCompare } from "../random.
|
|
1
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/code.ts
|
|
4
4
|
/**
|
|
@@ -130,8 +130,7 @@ const CodeProvider = (config) => {
|
|
|
130
130
|
const action = formData.get("action")?.toString();
|
|
131
131
|
if (action === "request" || action === "resend") {
|
|
132
132
|
const code = generateCode();
|
|
133
|
-
const
|
|
134
|
-
const { action: _,...claims } = formEntries;
|
|
133
|
+
const { action: _, ...claims } = Object.fromEntries(formData);
|
|
135
134
|
const sendError = await config.sendCode(claims, code);
|
|
136
135
|
if (sendError) return transition(c, { type: "start" }, formData, sendError);
|
|
137
136
|
return transition(c, {
|
|
@@ -1,7 +1,65 @@
|
|
|
1
|
-
import { Oauth2Provider } from "./oauth2.
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/provider/discord.ts
|
|
4
4
|
/**
|
|
5
|
+
* Discord OAuth 2.0 authentication provider for Draft Auth.
|
|
6
|
+
* Provides access tokens for calling Discord APIs on behalf of users.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { DiscordProvider } from "@draftlab/auth/provider/discord"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* providers: {
|
|
15
|
+
* discord: DiscordProvider({
|
|
16
|
+
* clientID: process.env.DISCORD_CLIENT_ID,
|
|
17
|
+
* clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
18
|
+
* scopes: ["identify", "email", "guilds"]
|
|
19
|
+
* })
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Common Scopes
|
|
25
|
+
*
|
|
26
|
+
* - `identify` - Access to user's basic account information
|
|
27
|
+
* - `email` - Access to user's email address
|
|
28
|
+
* - `guilds` - Access to user's guilds (servers)
|
|
29
|
+
* - `guilds.join` - Ability to join user to guilds
|
|
30
|
+
* - `gdm.join` - Ability to join user to group DMs
|
|
31
|
+
* - `connections` - Access to user's connections (Steam, YouTube, etc.)
|
|
32
|
+
* - `guilds.members.read` - Read guild member information
|
|
33
|
+
* - `bot` - For bot applications (requires additional setup)
|
|
34
|
+
*
|
|
35
|
+
* ## User Data Access
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* success: async (ctx, value) => {
|
|
39
|
+
* if (value.provider === "discord") {
|
|
40
|
+
* const accessToken = value.tokenset.access
|
|
41
|
+
*
|
|
42
|
+
* // Fetch user information
|
|
43
|
+
* const userResponse = await fetch('https://discord.com/api/users/@me', {
|
|
44
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
45
|
+
* })
|
|
46
|
+
* const user = await userResponse.json()
|
|
47
|
+
*
|
|
48
|
+
* // Fetch user guilds (requires guilds scope)
|
|
49
|
+
* const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', {
|
|
50
|
+
* headers: { Authorization: `Bearer ${accessToken}` }
|
|
51
|
+
* })
|
|
52
|
+
* const guilds = await guildsResponse.json()
|
|
53
|
+
*
|
|
54
|
+
* // User info: user.username + user.discriminator
|
|
55
|
+
* // Avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @packageDocumentation
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
5
63
|
* Creates a Discord OAuth 2.0 authentication provider.
|
|
6
64
|
* Use this when you need access tokens to call Discord APIs on behalf of the user.
|
|
7
65
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Provider } from "./provider.
|
|
2
|
-
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.
|
|
1
|
+
import { Provider } from "./provider.mjs";
|
|
2
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/provider/facebook.d.ts
|
|
5
5
|
|