vinextauth 0.1.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/cloudflare-kv.d.ts +28 -0
- package/dist/adapters/cloudflare-kv.js +51 -0
- package/dist/adapters/cloudflare-kv.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +628 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +24 -0
- package/dist/middleware/index.js +147 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/providers/github.d.ts +12 -0
- package/dist/providers/github.js +41 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/google.d.ts +12 -0
- package/dist/providers/google.js +43 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +113 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.d.ts +24 -0
- package/dist/server/index.js +245 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-G_m6Z3Iz.d.ts +180 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/cookies/strategy.ts
|
|
2
|
+
var SESSION_TOKEN_COOKIE = "vinextauth.session-token";
|
|
3
|
+
var CALLBACK_URL_COOKIE = "vinextauth.callback-url";
|
|
4
|
+
var CSRF_TOKEN_COOKIE = "vinextauth.csrf-token";
|
|
5
|
+
var STATE_COOKIE = "vinextauth.state";
|
|
6
|
+
var NONCE_COOKIE = "vinextauth.nonce";
|
|
7
|
+
var SECURE_PREFIX = "__Secure-";
|
|
8
|
+
function buildCookieNames(useSecure) {
|
|
9
|
+
const prefix = useSecure ? SECURE_PREFIX : "";
|
|
10
|
+
return {
|
|
11
|
+
sessionToken: {
|
|
12
|
+
name: `${prefix}${SESSION_TOKEN_COOKIE}`,
|
|
13
|
+
options: {
|
|
14
|
+
httpOnly: true,
|
|
15
|
+
sameSite: "lax",
|
|
16
|
+
path: "/",
|
|
17
|
+
secure: useSecure
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
callbackUrl: {
|
|
21
|
+
name: `${prefix}${CALLBACK_URL_COOKIE}`,
|
|
22
|
+
options: {
|
|
23
|
+
httpOnly: true,
|
|
24
|
+
sameSite: "lax",
|
|
25
|
+
path: "/",
|
|
26
|
+
secure: useSecure
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
csrfToken: {
|
|
30
|
+
name: `${CSRF_TOKEN_COOKIE}`,
|
|
31
|
+
options: {
|
|
32
|
+
httpOnly: true,
|
|
33
|
+
sameSite: "lax",
|
|
34
|
+
path: "/",
|
|
35
|
+
secure: useSecure
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
state: {
|
|
39
|
+
name: `${prefix}${STATE_COOKIE}`,
|
|
40
|
+
options: {
|
|
41
|
+
httpOnly: true,
|
|
42
|
+
sameSite: "lax",
|
|
43
|
+
path: "/",
|
|
44
|
+
secure: useSecure,
|
|
45
|
+
maxAge: 60 * 15
|
|
46
|
+
// 15 minutes
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
nonce: {
|
|
50
|
+
name: `${prefix}${NONCE_COOKIE}`,
|
|
51
|
+
options: {
|
|
52
|
+
httpOnly: true,
|
|
53
|
+
sameSite: "lax",
|
|
54
|
+
path: "/",
|
|
55
|
+
secure: useSecure,
|
|
56
|
+
maxAge: 60 * 15
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function serializeCookie(name, value, options) {
|
|
62
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
63
|
+
if (options.httpOnly) cookie += "; HttpOnly";
|
|
64
|
+
if (options.secure) cookie += "; Secure";
|
|
65
|
+
if (options.sameSite) cookie += `; SameSite=${capitalize(options.sameSite)}`;
|
|
66
|
+
if (options.path) cookie += `; Path=${options.path}`;
|
|
67
|
+
if (options.maxAge !== void 0) cookie += `; Max-Age=${options.maxAge}`;
|
|
68
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
69
|
+
return cookie;
|
|
70
|
+
}
|
|
71
|
+
function deleteCookieString(name, options) {
|
|
72
|
+
return serializeCookie(name, "", { ...options, maxAge: 0 });
|
|
73
|
+
}
|
|
74
|
+
function capitalize(str) {
|
|
75
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/core/config.ts
|
|
79
|
+
var DEFAULT_MAX_AGE = 30 * 24 * 60 * 60;
|
|
80
|
+
function resolveConfig(config) {
|
|
81
|
+
const secret = config.secret ?? (typeof process !== "undefined" ? process.env.NEXTAUTH_SECRET ?? process.env.VINEXTAUTH_SECRET : void 0);
|
|
82
|
+
if (!secret) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"[VinextAuth] No secret provided. Set NEXTAUTH_SECRET or VINEXTAUTH_SECRET env var, or pass `secret` to VinextAuth()."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const baseUrl = (typeof process !== "undefined" ? process.env.NEXTAUTH_URL ?? process.env.VINEXTAUTH_URL ?? process.env.VERCEL_URL : void 0) ?? "http://localhost:3000";
|
|
88
|
+
const normalizedBaseUrl = baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}`;
|
|
89
|
+
const useSecureCookies = config.useSecureCookies ?? normalizedBaseUrl.startsWith("https://");
|
|
90
|
+
const sessionMaxAge = config.session?.maxAge ?? DEFAULT_MAX_AGE;
|
|
91
|
+
return {
|
|
92
|
+
providers: config.providers,
|
|
93
|
+
secret,
|
|
94
|
+
baseUrl: normalizedBaseUrl,
|
|
95
|
+
basePath: "/api/auth",
|
|
96
|
+
callbacks: config.callbacks ?? {},
|
|
97
|
+
pages: {
|
|
98
|
+
signIn: "/api/auth/signin",
|
|
99
|
+
signOut: "/api/auth/signout",
|
|
100
|
+
error: "/api/auth/error",
|
|
101
|
+
verifyRequest: "/api/auth/verify-request",
|
|
102
|
+
newUser: "/",
|
|
103
|
+
...config.pages
|
|
104
|
+
},
|
|
105
|
+
session: {
|
|
106
|
+
strategy: config.session?.strategy ?? "jwt",
|
|
107
|
+
maxAge: sessionMaxAge,
|
|
108
|
+
updateAge: config.session?.updateAge ?? 24 * 60 * 60
|
|
109
|
+
},
|
|
110
|
+
jwt: {
|
|
111
|
+
secret,
|
|
112
|
+
maxAge: sessionMaxAge,
|
|
113
|
+
encode: config.jwt?.encode,
|
|
114
|
+
decode: config.jwt?.decode
|
|
115
|
+
},
|
|
116
|
+
debug: config.debug ?? false,
|
|
117
|
+
useSecureCookies,
|
|
118
|
+
cookies: {
|
|
119
|
+
...buildCookieNames(useSecureCookies),
|
|
120
|
+
...config.cookies
|
|
121
|
+
},
|
|
122
|
+
adapter: config.adapter
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/cookies/index.ts
|
|
127
|
+
function getSessionToken(request, config) {
|
|
128
|
+
return getCookieValue(request, config.cookies.sessionToken.name);
|
|
129
|
+
}
|
|
130
|
+
function getCallbackUrl(request, config) {
|
|
131
|
+
return getCookieValue(request, config.cookies.callbackUrl.name);
|
|
132
|
+
}
|
|
133
|
+
function getCsrfCookie(request, config) {
|
|
134
|
+
return getCookieValue(request, config.cookies.csrfToken.name);
|
|
135
|
+
}
|
|
136
|
+
function getStateCookie(request, config) {
|
|
137
|
+
return getCookieValue(request, config.cookies.state.name);
|
|
138
|
+
}
|
|
139
|
+
function getCookieValue(request, name) {
|
|
140
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
141
|
+
for (const part of cookieHeader.split(";")) {
|
|
142
|
+
const [key, ...val] = part.trim().split("=");
|
|
143
|
+
if (key.trim() === name) {
|
|
144
|
+
return decodeURIComponent(val.join("="));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function applySessionCookie(headers, token, config) {
|
|
150
|
+
const { name, options } = config.cookies.sessionToken;
|
|
151
|
+
headers.append(
|
|
152
|
+
"Set-Cookie",
|
|
153
|
+
serializeCookie(name, token, { ...options, maxAge: config.session.maxAge })
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function applyCallbackUrlCookie(headers, url, config) {
|
|
157
|
+
const { name, options } = config.cookies.callbackUrl;
|
|
158
|
+
headers.append("Set-Cookie", serializeCookie(name, url, { ...options, maxAge: 60 * 10 }));
|
|
159
|
+
}
|
|
160
|
+
function applyCsrfCookie(headers, value, config) {
|
|
161
|
+
const { name, options } = config.cookies.csrfToken;
|
|
162
|
+
headers.append("Set-Cookie", serializeCookie(name, value, options));
|
|
163
|
+
}
|
|
164
|
+
function applyStateCookie(headers, state, config) {
|
|
165
|
+
const { name, options } = config.cookies.state;
|
|
166
|
+
headers.append("Set-Cookie", serializeCookie(name, state, options));
|
|
167
|
+
}
|
|
168
|
+
function clearSessionCookie(headers, config) {
|
|
169
|
+
const { name, options } = config.cookies.sessionToken;
|
|
170
|
+
headers.append("Set-Cookie", deleteCookieString(name, options));
|
|
171
|
+
}
|
|
172
|
+
function clearStateCookie(headers, config) {
|
|
173
|
+
const { name, options } = config.cookies.state;
|
|
174
|
+
headers.append("Set-Cookie", deleteCookieString(name, options));
|
|
175
|
+
}
|
|
176
|
+
function clearCallbackUrlCookie(headers, config) {
|
|
177
|
+
const { name, options } = config.cookies.callbackUrl;
|
|
178
|
+
headers.append("Set-Cookie", deleteCookieString(name, options));
|
|
179
|
+
}
|
|
180
|
+
function sessionToExpires(maxAge) {
|
|
181
|
+
return new Date(Date.now() + maxAge * 1e3).toISOString();
|
|
182
|
+
}
|
|
183
|
+
function buildSessionFromJWT(jwt, maxAge) {
|
|
184
|
+
return {
|
|
185
|
+
user: {
|
|
186
|
+
id: jwt.sub ?? "",
|
|
187
|
+
name: jwt.name ?? null,
|
|
188
|
+
email: jwt.email ?? null,
|
|
189
|
+
image: jwt.picture ?? null
|
|
190
|
+
},
|
|
191
|
+
expires: sessionToExpires(maxAge)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/handlers/signin.ts
|
|
196
|
+
function randomBase64url(bytes) {
|
|
197
|
+
const arr = new Uint8Array(bytes);
|
|
198
|
+
crypto.getRandomValues(arr);
|
|
199
|
+
const binary = String.fromCharCode(...arr);
|
|
200
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
201
|
+
}
|
|
202
|
+
async function handleSignIn(request, providerId, config) {
|
|
203
|
+
const provider = config.providers.find((p) => p.id === providerId);
|
|
204
|
+
if (!provider) {
|
|
205
|
+
return new Response(`Unknown provider: ${providerId}`, { status: 404 });
|
|
206
|
+
}
|
|
207
|
+
const url = new URL(request.url);
|
|
208
|
+
const callbackUrl = url.searchParams.get("callbackUrl") ?? config.pages.newUser ?? "/";
|
|
209
|
+
const state = randomBase64url(32);
|
|
210
|
+
const redirectUri = `${config.baseUrl}${config.basePath}/callback/${providerId}`;
|
|
211
|
+
const authUrl = new URL(provider.authorization.url);
|
|
212
|
+
authUrl.searchParams.set("client_id", provider.clientId);
|
|
213
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
214
|
+
authUrl.searchParams.set("state", state);
|
|
215
|
+
const params = provider.authorization.params ?? {};
|
|
216
|
+
for (const [key, value] of Object.entries(params)) {
|
|
217
|
+
authUrl.searchParams.set(key, value);
|
|
218
|
+
}
|
|
219
|
+
const headers = new Headers();
|
|
220
|
+
applyStateCookie(headers, state, config);
|
|
221
|
+
applyCallbackUrlCookie(headers, callbackUrl, config);
|
|
222
|
+
headers.set("Location", authUrl.toString());
|
|
223
|
+
return new Response(null, { status: 302, headers });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/jwt/keys.ts
|
|
227
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
228
|
+
async function deriveKey(secret) {
|
|
229
|
+
const cached = keyCache.get(secret);
|
|
230
|
+
if (cached) return cached;
|
|
231
|
+
const encoder = new TextEncoder();
|
|
232
|
+
const keyData = encoder.encode(secret);
|
|
233
|
+
const key = await crypto.subtle.importKey(
|
|
234
|
+
"raw",
|
|
235
|
+
keyData,
|
|
236
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
237
|
+
false,
|
|
238
|
+
["sign", "verify"]
|
|
239
|
+
);
|
|
240
|
+
keyCache.set(secret, key);
|
|
241
|
+
return key;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/jwt/index.ts
|
|
245
|
+
function base64urlEncode(data) {
|
|
246
|
+
const bytes = new Uint8Array(data);
|
|
247
|
+
let binary = "";
|
|
248
|
+
for (const byte of bytes) {
|
|
249
|
+
binary += String.fromCharCode(byte);
|
|
250
|
+
}
|
|
251
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
252
|
+
}
|
|
253
|
+
function base64urlDecode(str) {
|
|
254
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
255
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
256
|
+
const binary = atob(padded);
|
|
257
|
+
const bytes = new Uint8Array(binary.length);
|
|
258
|
+
for (let i = 0; i < binary.length; i++) {
|
|
259
|
+
bytes[i] = binary.charCodeAt(i);
|
|
260
|
+
}
|
|
261
|
+
return bytes;
|
|
262
|
+
}
|
|
263
|
+
function encodeJson(obj) {
|
|
264
|
+
return base64urlEncode(new TextEncoder().encode(JSON.stringify(obj)).buffer);
|
|
265
|
+
}
|
|
266
|
+
function decodeJson(str) {
|
|
267
|
+
return JSON.parse(new TextDecoder().decode(base64urlDecode(str)));
|
|
268
|
+
}
|
|
269
|
+
var HEADER = encodeJson({ alg: "HS256", typ: "JWT" });
|
|
270
|
+
async function sign(payload, secret) {
|
|
271
|
+
const encodedPayload = encodeJson(payload);
|
|
272
|
+
const message = `${HEADER}.${encodedPayload}`;
|
|
273
|
+
const key = await deriveKey(secret);
|
|
274
|
+
const signature = await crypto.subtle.sign(
|
|
275
|
+
"HMAC",
|
|
276
|
+
key,
|
|
277
|
+
new TextEncoder().encode(message)
|
|
278
|
+
);
|
|
279
|
+
return `${message}.${base64urlEncode(signature)}`;
|
|
280
|
+
}
|
|
281
|
+
async function verify(token, secret) {
|
|
282
|
+
try {
|
|
283
|
+
const parts = token.split(".");
|
|
284
|
+
if (parts.length !== 3) return null;
|
|
285
|
+
const [header, payload, sig] = parts;
|
|
286
|
+
const message = `${header}.${payload}`;
|
|
287
|
+
const key = await deriveKey(secret);
|
|
288
|
+
const signatureBytes = base64urlDecode(sig);
|
|
289
|
+
const valid = await crypto.subtle.verify(
|
|
290
|
+
"HMAC",
|
|
291
|
+
key,
|
|
292
|
+
signatureBytes,
|
|
293
|
+
new TextEncoder().encode(message)
|
|
294
|
+
);
|
|
295
|
+
if (!valid) return null;
|
|
296
|
+
const decoded = decodeJson(payload);
|
|
297
|
+
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1e3)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return decoded;
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/core/session.ts
|
|
307
|
+
async function encodeSession(payload, config) {
|
|
308
|
+
if (config.jwt.encode) {
|
|
309
|
+
return config.jwt.encode({
|
|
310
|
+
token: payload,
|
|
311
|
+
secret: config.secret,
|
|
312
|
+
maxAge: config.session.maxAge
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return sign(payload, config.secret);
|
|
316
|
+
}
|
|
317
|
+
async function decodeSession(token, config) {
|
|
318
|
+
if (config.jwt.decode) {
|
|
319
|
+
return config.jwt.decode({ token, secret: config.secret });
|
|
320
|
+
}
|
|
321
|
+
return verify(token, config.secret);
|
|
322
|
+
}
|
|
323
|
+
async function buildSession(jwt, config) {
|
|
324
|
+
const baseSession = buildSessionFromJWT(jwt, config.session.maxAge);
|
|
325
|
+
if (config.callbacks.session) {
|
|
326
|
+
return config.callbacks.session({ session: baseSession, token: jwt });
|
|
327
|
+
}
|
|
328
|
+
return baseSession;
|
|
329
|
+
}
|
|
330
|
+
async function buildJWT(user, account, profile, config) {
|
|
331
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
332
|
+
let token = {
|
|
333
|
+
sub: user.id,
|
|
334
|
+
name: user.name,
|
|
335
|
+
email: user.email,
|
|
336
|
+
picture: user.image,
|
|
337
|
+
iat: now,
|
|
338
|
+
exp: now + config.session.maxAge,
|
|
339
|
+
jti: generateId()
|
|
340
|
+
};
|
|
341
|
+
if (config.callbacks.jwt) {
|
|
342
|
+
token = await config.callbacks.jwt({
|
|
343
|
+
token,
|
|
344
|
+
user,
|
|
345
|
+
account,
|
|
346
|
+
profile,
|
|
347
|
+
trigger: "signIn"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return token;
|
|
351
|
+
}
|
|
352
|
+
function generateId() {
|
|
353
|
+
const bytes = new Uint8Array(16);
|
|
354
|
+
crypto.getRandomValues(bytes);
|
|
355
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/handlers/callback.ts
|
|
359
|
+
async function handleCallback(request, providerId, config) {
|
|
360
|
+
const provider = config.providers.find((p) => p.id === providerId);
|
|
361
|
+
if (!provider) return new Response("Unknown provider", { status: 404 });
|
|
362
|
+
const url = new URL(request.url);
|
|
363
|
+
const code = url.searchParams.get("code");
|
|
364
|
+
const stateParam = url.searchParams.get("state");
|
|
365
|
+
const error = url.searchParams.get("error");
|
|
366
|
+
if (error) {
|
|
367
|
+
const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
|
|
368
|
+
errorUrl.searchParams.set("error", error);
|
|
369
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
370
|
+
}
|
|
371
|
+
if (!code) {
|
|
372
|
+
return new Response("Missing code", { status: 400 });
|
|
373
|
+
}
|
|
374
|
+
const storedState = getStateCookie(request, config);
|
|
375
|
+
if (provider.checks?.includes("state")) {
|
|
376
|
+
if (!storedState || storedState !== stateParam) {
|
|
377
|
+
const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
|
|
378
|
+
errorUrl.searchParams.set("error", "OAuthStateError");
|
|
379
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const redirectUri = `${config.baseUrl}${config.basePath}/callback/${providerId}`;
|
|
383
|
+
let tokenData;
|
|
384
|
+
try {
|
|
385
|
+
const tokenResponse = await fetch(provider.token.url, {
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: {
|
|
388
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
389
|
+
Accept: "application/json"
|
|
390
|
+
},
|
|
391
|
+
body: new URLSearchParams({
|
|
392
|
+
grant_type: "authorization_code",
|
|
393
|
+
code,
|
|
394
|
+
redirect_uri: redirectUri,
|
|
395
|
+
client_id: provider.clientId,
|
|
396
|
+
client_secret: provider.clientSecret
|
|
397
|
+
}).toString()
|
|
398
|
+
});
|
|
399
|
+
if (!tokenResponse.ok) {
|
|
400
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
401
|
+
}
|
|
402
|
+
tokenData = await tokenResponse.json();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (config.debug) console.error("[VinextAuth] Token exchange error:", err);
|
|
405
|
+
const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
|
|
406
|
+
errorUrl.searchParams.set("error", "OAuthCallbackError");
|
|
407
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
408
|
+
}
|
|
409
|
+
let rawProfile;
|
|
410
|
+
try {
|
|
411
|
+
const userInfoResponse = await fetch(provider.userinfo.url, {
|
|
412
|
+
headers: {
|
|
413
|
+
Authorization: `Bearer ${tokenData.access_token}`,
|
|
414
|
+
Accept: "application/json"
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
if (!userInfoResponse.ok) {
|
|
418
|
+
throw new Error(`UserInfo fetch failed: ${userInfoResponse.status}`);
|
|
419
|
+
}
|
|
420
|
+
rawProfile = await userInfoResponse.json();
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (config.debug) console.error("[VinextAuth] UserInfo error:", err);
|
|
423
|
+
const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
|
|
424
|
+
errorUrl.searchParams.set("error", "OAuthCallbackError");
|
|
425
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
426
|
+
}
|
|
427
|
+
const user = provider.profile(rawProfile);
|
|
428
|
+
const account = {
|
|
429
|
+
provider: providerId,
|
|
430
|
+
type: "oauth",
|
|
431
|
+
providerAccountId: user.id,
|
|
432
|
+
access_token: tokenData.access_token,
|
|
433
|
+
refresh_token: tokenData.refresh_token,
|
|
434
|
+
expires_at: tokenData.expires_in ? Math.floor(Date.now() / 1e3) + tokenData.expires_in : void 0,
|
|
435
|
+
token_type: tokenData.token_type,
|
|
436
|
+
scope: tokenData.scope,
|
|
437
|
+
id_token: tokenData.id_token
|
|
438
|
+
};
|
|
439
|
+
if (config.callbacks.signIn) {
|
|
440
|
+
const result = await config.callbacks.signIn({ user, account, profile: rawProfile });
|
|
441
|
+
if (result === false) {
|
|
442
|
+
const errorUrl = new URL(`${config.baseUrl}${config.pages.error}`);
|
|
443
|
+
errorUrl.searchParams.set("error", "AccessDenied");
|
|
444
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
445
|
+
}
|
|
446
|
+
if (typeof result === "string") {
|
|
447
|
+
return Response.redirect(result, 302);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const jwtPayload = await buildJWT(user, account, rawProfile, config);
|
|
451
|
+
const sessionToken = await encodeSession(jwtPayload, config);
|
|
452
|
+
const session = await buildSession(jwtPayload, config);
|
|
453
|
+
const callbackUrl = getCallbackUrl(request, config) ?? config.pages.newUser ?? "/";
|
|
454
|
+
const redirectUrl = isAbsoluteUrl(callbackUrl) ? callbackUrl : `${config.baseUrl}${callbackUrl}`;
|
|
455
|
+
const headers = new Headers();
|
|
456
|
+
applySessionCookie(headers, sessionToken, config);
|
|
457
|
+
clearStateCookie(headers, config);
|
|
458
|
+
clearCallbackUrlCookie(headers, config);
|
|
459
|
+
headers.set("Location", redirectUrl);
|
|
460
|
+
if (config.debug) {
|
|
461
|
+
console.log("[VinextAuth] Signed in:", session.user.email);
|
|
462
|
+
}
|
|
463
|
+
return new Response(null, { status: 302, headers });
|
|
464
|
+
}
|
|
465
|
+
function isAbsoluteUrl(url) {
|
|
466
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/core/csrf.ts
|
|
470
|
+
function bytesToHex(bytes) {
|
|
471
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
472
|
+
}
|
|
473
|
+
function randomHex(bytes) {
|
|
474
|
+
const arr = new Uint8Array(bytes);
|
|
475
|
+
crypto.getRandomValues(arr);
|
|
476
|
+
return bytesToHex(arr);
|
|
477
|
+
}
|
|
478
|
+
async function hmacHex(secret, value) {
|
|
479
|
+
const key = await deriveKey(secret);
|
|
480
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
|
|
481
|
+
return bytesToHex(new Uint8Array(sig));
|
|
482
|
+
}
|
|
483
|
+
async function generateCsrfToken(secret) {
|
|
484
|
+
const token = randomHex(32);
|
|
485
|
+
const hash = await hmacHex(secret, token);
|
|
486
|
+
return { token, cookieValue: `${token}|${hash}` };
|
|
487
|
+
}
|
|
488
|
+
async function verifyCsrfToken(submittedToken, cookieValue, secret) {
|
|
489
|
+
const [storedToken] = cookieValue.split("|");
|
|
490
|
+
if (storedToken !== submittedToken) return false;
|
|
491
|
+
const expectedHash = await hmacHex(secret, storedToken);
|
|
492
|
+
const expectedCookieValue = `${storedToken}|${expectedHash}`;
|
|
493
|
+
if (cookieValue.length !== expectedCookieValue.length) return false;
|
|
494
|
+
let diff = 0;
|
|
495
|
+
for (let i = 0; i < cookieValue.length; i++) {
|
|
496
|
+
diff |= cookieValue.charCodeAt(i) ^ expectedCookieValue.charCodeAt(i);
|
|
497
|
+
}
|
|
498
|
+
return diff === 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/handlers/signout.ts
|
|
502
|
+
async function handleSignOut(request, config) {
|
|
503
|
+
let callbackUrl = config.baseUrl;
|
|
504
|
+
if (request.method === "POST") {
|
|
505
|
+
let body = {};
|
|
506
|
+
try {
|
|
507
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
508
|
+
if (contentType.includes("application/json")) {
|
|
509
|
+
body = await request.json();
|
|
510
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
511
|
+
const text = await request.text();
|
|
512
|
+
body = Object.fromEntries(new URLSearchParams(text));
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
const csrfCookie = getCsrfCookie(request, config);
|
|
517
|
+
const submittedToken = body.csrfToken ?? "";
|
|
518
|
+
if (csrfCookie) {
|
|
519
|
+
const valid = await verifyCsrfToken(submittedToken, csrfCookie, config.secret);
|
|
520
|
+
if (!valid && config.debug) {
|
|
521
|
+
console.warn("[VinextAuth] CSRF verification failed on signout");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
callbackUrl = body.callbackUrl ?? config.baseUrl;
|
|
525
|
+
}
|
|
526
|
+
const headers = new Headers();
|
|
527
|
+
clearSessionCookie(headers, config);
|
|
528
|
+
clearCallbackUrlCookie(headers, config);
|
|
529
|
+
const redirectUrl = isAbsoluteUrl2(callbackUrl) ? callbackUrl : `${config.baseUrl}${callbackUrl}`;
|
|
530
|
+
headers.set("Location", redirectUrl);
|
|
531
|
+
return new Response(null, { status: 302, headers });
|
|
532
|
+
}
|
|
533
|
+
function isAbsoluteUrl2(url) {
|
|
534
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/handlers/session-route.ts
|
|
538
|
+
async function handleSessionRoute(request, config) {
|
|
539
|
+
const token = getSessionToken(request, config);
|
|
540
|
+
if (!token) {
|
|
541
|
+
return Response.json({});
|
|
542
|
+
}
|
|
543
|
+
const jwt = await decodeSession(token, config);
|
|
544
|
+
if (!jwt) {
|
|
545
|
+
return Response.json({});
|
|
546
|
+
}
|
|
547
|
+
const session = await buildSession(jwt, config);
|
|
548
|
+
return Response.json(session, {
|
|
549
|
+
headers: {
|
|
550
|
+
"Cache-Control": "no-store, max-age=0"
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/handlers/csrf-route.ts
|
|
556
|
+
async function handleCsrfRoute(request, config) {
|
|
557
|
+
const existing = getCsrfCookie(request, config);
|
|
558
|
+
if (existing) {
|
|
559
|
+
const token2 = existing.split("|")[0];
|
|
560
|
+
return Response.json({ csrfToken: token2 });
|
|
561
|
+
}
|
|
562
|
+
const { token, cookieValue } = await generateCsrfToken(config.secret);
|
|
563
|
+
const headers = new Headers();
|
|
564
|
+
applyCsrfCookie(headers, cookieValue, config);
|
|
565
|
+
return Response.json({ csrfToken: token }, { headers });
|
|
566
|
+
}
|
|
567
|
+
function VinextAuth(config) {
|
|
568
|
+
const resolved = resolveConfig(config);
|
|
569
|
+
async function handler(request) {
|
|
570
|
+
const url = new URL(request.url);
|
|
571
|
+
const basePath = resolved.basePath;
|
|
572
|
+
const pathname = url.pathname;
|
|
573
|
+
if (!pathname.startsWith(basePath)) {
|
|
574
|
+
return new Response("Not Found", { status: 404 });
|
|
575
|
+
}
|
|
576
|
+
const action = pathname.slice(basePath.length).replace(/^\//, "");
|
|
577
|
+
const parts = action.split("/");
|
|
578
|
+
const verb = parts[0];
|
|
579
|
+
const param = parts[1];
|
|
580
|
+
if (verb === "signin" && param) {
|
|
581
|
+
return handleSignIn(request, param, resolved);
|
|
582
|
+
}
|
|
583
|
+
if (verb === "signin" && !param) {
|
|
584
|
+
return handleSignInPage(resolved);
|
|
585
|
+
}
|
|
586
|
+
if (verb === "callback" && param) {
|
|
587
|
+
return handleCallback(request, param, resolved);
|
|
588
|
+
}
|
|
589
|
+
if (verb === "signout") {
|
|
590
|
+
if (request.method === "POST") {
|
|
591
|
+
return handleSignOut(request, resolved);
|
|
592
|
+
}
|
|
593
|
+
return handleSignOut(request, resolved);
|
|
594
|
+
}
|
|
595
|
+
if (verb === "session") {
|
|
596
|
+
return handleSessionRoute(request, resolved);
|
|
597
|
+
}
|
|
598
|
+
if (verb === "csrf") {
|
|
599
|
+
return handleCsrfRoute(request, resolved);
|
|
600
|
+
}
|
|
601
|
+
if (verb === "error") {
|
|
602
|
+
const error = url.searchParams.get("error") ?? "Unknown";
|
|
603
|
+
return new Response(
|
|
604
|
+
`<!DOCTYPE html><html><body><h1>Authentication Error</h1><p>${error}</p><a href="${resolved.pages.signIn}">Try again</a></body></html>`,
|
|
605
|
+
{ status: 400, headers: { "Content-Type": "text/html" } }
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return new Response("Not Found", { status: 404 });
|
|
609
|
+
}
|
|
610
|
+
return { GET: handler, POST: handler };
|
|
611
|
+
}
|
|
612
|
+
function handleSignInPage(config) {
|
|
613
|
+
const providers = config.providers.map((p) => `
|
|
614
|
+
<a href="${config.basePath}/signin/${p.id}" style="display:block;margin:8px 0;padding:12px 24px;border:1px solid #ccc;border-radius:6px;text-decoration:none;color:#000;">
|
|
615
|
+
Sign in with ${p.name}
|
|
616
|
+
</a>
|
|
617
|
+
`).join("");
|
|
618
|
+
return new Response(
|
|
619
|
+
`<!DOCTYPE html><html><body style="font-family:sans-serif;max-width:400px;margin:80px auto;padding:24px">
|
|
620
|
+
<h1>Sign In</h1>${providers}
|
|
621
|
+
</body></html>`,
|
|
622
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export { VinextAuth, VinextAuth as default };
|
|
627
|
+
//# sourceMappingURL=index.js.map
|
|
628
|
+
//# sourceMappingURL=index.js.map
|