dauth-md-node 3.0.2 → 4.0.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/chunk-4A7BR4EM.mjs +82 -0
- package/dist/chunk-4A7BR4EM.mjs.map +1 -0
- package/dist/index.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +108 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -19
- package/dist/index.mjs.map +1 -1
- package/dist/router.d.mts +16 -0
- package/dist/router.d.ts +16 -0
- package/dist/router.js +436 -0
- package/dist/router.js.map +1 -0
- package/dist/router.mjs +333 -0
- package/dist/router.mjs.map +1 -0
- package/package.json +22 -2
- package/src/csrf.ts +18 -0
- package/src/index.ts +100 -15
- package/src/router.ts +444 -0
- package/src/session.ts +81 -0
package/dist/router.mjs
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decryptSessionWithKeys,
|
|
3
|
+
deriveEncryptionKey,
|
|
4
|
+
encryptSession,
|
|
5
|
+
getServerBasePath
|
|
6
|
+
} from "./chunk-4A7BR4EM.mjs";
|
|
7
|
+
|
|
8
|
+
// src/router.ts
|
|
9
|
+
import { Router } from "express";
|
|
10
|
+
import jwt from "jsonwebtoken";
|
|
11
|
+
|
|
12
|
+
// src/csrf.ts
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
function generateCsrfToken() {
|
|
15
|
+
return crypto.randomBytes(32).toString("hex");
|
|
16
|
+
}
|
|
17
|
+
function verifyCsrf(req, csrfCookieName) {
|
|
18
|
+
const headerToken = req.headers["x-csrf-token"];
|
|
19
|
+
const cookieToken = req.cookies?.[csrfCookieName];
|
|
20
|
+
if (!headerToken || !cookieToken) return false;
|
|
21
|
+
return headerToken === cookieToken;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/router.ts
|
|
25
|
+
var refreshLocks = /* @__PURE__ */ new Map();
|
|
26
|
+
function lockKey(refreshToken) {
|
|
27
|
+
return refreshToken.substring(0, 16);
|
|
28
|
+
}
|
|
29
|
+
function clearStaleLocks() {
|
|
30
|
+
if (refreshLocks.size > 100) refreshLocks.clear();
|
|
31
|
+
}
|
|
32
|
+
async function resolveConfig(opts) {
|
|
33
|
+
const secure = opts.secure ?? process.env.NODE_ENV !== "development";
|
|
34
|
+
const cookieName = opts.cookieName ?? (secure ? "__Host-dauth-session" : "dauth-session");
|
|
35
|
+
const csrfCookieName = opts.csrfCookieName ?? (secure ? "__Host-csrf" : "csrf-token");
|
|
36
|
+
const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1e3;
|
|
37
|
+
const keys = [];
|
|
38
|
+
keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));
|
|
39
|
+
if (opts.previousTsk) {
|
|
40
|
+
keys.push(
|
|
41
|
+
await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
let dauthBasePath;
|
|
45
|
+
if (opts.dauthUrl) {
|
|
46
|
+
dauthBasePath = `${opts.dauthUrl.replace(/\/+$/, "")}/api/v1`;
|
|
47
|
+
} else {
|
|
48
|
+
dauthBasePath = getServerBasePath();
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
domainName: opts.domainName,
|
|
52
|
+
dauthBasePath,
|
|
53
|
+
cookieName,
|
|
54
|
+
csrfCookieName,
|
|
55
|
+
maxAgeMs,
|
|
56
|
+
secure,
|
|
57
|
+
encKeys: keys
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function setSessionCookie(res, payload, config) {
|
|
61
|
+
const encrypted = encryptSession(payload, config.encKeys[0]);
|
|
62
|
+
const cookieOpts = {
|
|
63
|
+
httpOnly: true,
|
|
64
|
+
secure: config.secure,
|
|
65
|
+
sameSite: "lax",
|
|
66
|
+
maxAge: config.maxAgeMs,
|
|
67
|
+
path: "/"
|
|
68
|
+
};
|
|
69
|
+
if (!config.secure) {
|
|
70
|
+
}
|
|
71
|
+
res.cookie(config.cookieName, encrypted, cookieOpts);
|
|
72
|
+
}
|
|
73
|
+
function setCsrfCookie(res, config) {
|
|
74
|
+
const csrfToken = generateCsrfToken();
|
|
75
|
+
res.cookie(config.csrfCookieName, csrfToken, {
|
|
76
|
+
httpOnly: false,
|
|
77
|
+
secure: config.secure,
|
|
78
|
+
sameSite: "lax",
|
|
79
|
+
maxAge: config.maxAgeMs,
|
|
80
|
+
path: "/"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function clearCookies(res, config) {
|
|
84
|
+
const baseOpts = { path: "/", secure: config.secure };
|
|
85
|
+
res.clearCookie(config.cookieName, baseOpts);
|
|
86
|
+
res.clearCookie(config.csrfCookieName, baseOpts);
|
|
87
|
+
}
|
|
88
|
+
function readSession(req, config) {
|
|
89
|
+
const cookie = req.cookies?.[config.cookieName];
|
|
90
|
+
if (!cookie) return null;
|
|
91
|
+
return decryptSessionWithKeys(cookie, config.encKeys);
|
|
92
|
+
}
|
|
93
|
+
function isTokenExpiringSoon(token, thresholdMs = 3e5) {
|
|
94
|
+
try {
|
|
95
|
+
const decoded = jwt.decode(token);
|
|
96
|
+
if (!decoded?.exp) return true;
|
|
97
|
+
return decoded.exp * 1e3 - Date.now() < thresholdMs;
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function maybeRefreshTokens(session, config, res) {
|
|
103
|
+
if (!isTokenExpiringSoon(session.accessToken)) return session;
|
|
104
|
+
const key = lockKey(session.refreshToken);
|
|
105
|
+
clearStaleLocks();
|
|
106
|
+
const existingLock = refreshLocks.get(key);
|
|
107
|
+
if (existingLock) {
|
|
108
|
+
const result2 = await existingLock;
|
|
109
|
+
return result2 ?? session;
|
|
110
|
+
}
|
|
111
|
+
const refreshPromise = (async () => {
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(
|
|
114
|
+
`${config.dauthBasePath}/app/${config.domainName}/refresh-token`,
|
|
115
|
+
{
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
refreshToken: session.refreshToken
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
if (!response.ok) return null;
|
|
124
|
+
const data = await response.json();
|
|
125
|
+
if (!data.accessToken || !data.refreshToken) return null;
|
|
126
|
+
const newSession = {
|
|
127
|
+
accessToken: data.accessToken,
|
|
128
|
+
refreshToken: data.refreshToken
|
|
129
|
+
};
|
|
130
|
+
setSessionCookie(res, newSession, config);
|
|
131
|
+
return newSession;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
refreshLocks.set(key, refreshPromise);
|
|
137
|
+
const timeout = setTimeout(() => refreshLocks.delete(key), 1e4);
|
|
138
|
+
refreshPromise.finally(() => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
refreshLocks.delete(key);
|
|
141
|
+
});
|
|
142
|
+
const result = await refreshPromise;
|
|
143
|
+
return result ?? session;
|
|
144
|
+
}
|
|
145
|
+
function dauthRouter(opts) {
|
|
146
|
+
const router = Router();
|
|
147
|
+
let configPromise = null;
|
|
148
|
+
async function getConfig() {
|
|
149
|
+
if (!configPromise) configPromise = resolveConfig(opts);
|
|
150
|
+
return configPromise;
|
|
151
|
+
}
|
|
152
|
+
router.post("/exchange-code", async (req, res) => {
|
|
153
|
+
const config = await getConfig();
|
|
154
|
+
const { code } = req.body;
|
|
155
|
+
if (!code) {
|
|
156
|
+
return res.status(400).send({ status: "code-required", message: "Code required" });
|
|
157
|
+
}
|
|
158
|
+
const response = await fetch(
|
|
159
|
+
`${config.dauthBasePath}/app/${config.domainName}/exchange-code`,
|
|
160
|
+
{
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ code })
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
return res.status(response.status).send({ status: "code-invalid", message: "Code invalid" });
|
|
168
|
+
}
|
|
169
|
+
const data = await response.json();
|
|
170
|
+
setSessionCookie(
|
|
171
|
+
res,
|
|
172
|
+
{
|
|
173
|
+
accessToken: data.accessToken,
|
|
174
|
+
refreshToken: data.refreshToken
|
|
175
|
+
},
|
|
176
|
+
config
|
|
177
|
+
);
|
|
178
|
+
setCsrfCookie(res, config);
|
|
179
|
+
const userResponse = await fetch(
|
|
180
|
+
`${config.dauthBasePath}/app/${config.domainName}/user`,
|
|
181
|
+
{
|
|
182
|
+
method: "GET",
|
|
183
|
+
headers: { Authorization: data.accessToken }
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
const userData = await userResponse.json();
|
|
187
|
+
return res.status(200).send({
|
|
188
|
+
user: userData.user,
|
|
189
|
+
domain: userData.domain,
|
|
190
|
+
isNewUser: data.isNewUser
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
router.get("/session", async (req, res) => {
|
|
194
|
+
const config = await getConfig();
|
|
195
|
+
const session = readSession(req, config);
|
|
196
|
+
if (!session) {
|
|
197
|
+
return res.status(401).send({ status: "no-session", message: "Not authenticated" });
|
|
198
|
+
}
|
|
199
|
+
const refreshed = await maybeRefreshTokens(session, config, res);
|
|
200
|
+
const userResponse = await fetch(
|
|
201
|
+
`${config.dauthBasePath}/app/${config.domainName}/user`,
|
|
202
|
+
{
|
|
203
|
+
method: "GET",
|
|
204
|
+
headers: { Authorization: refreshed.accessToken }
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
if (!userResponse.ok) {
|
|
208
|
+
clearCookies(res, config);
|
|
209
|
+
return res.status(401).send({ status: "session-invalid", message: "Session expired" });
|
|
210
|
+
}
|
|
211
|
+
const userData = await userResponse.json();
|
|
212
|
+
return res.status(200).send({
|
|
213
|
+
user: userData.user,
|
|
214
|
+
domain: userData.domain
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
router.post("/logout", async (req, res) => {
|
|
218
|
+
const config = await getConfig();
|
|
219
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
220
|
+
return res.status(403).send({ status: "csrf-invalid", message: "CSRF token invalid" });
|
|
221
|
+
}
|
|
222
|
+
const session = readSession(req, config);
|
|
223
|
+
if (session) {
|
|
224
|
+
fetch(
|
|
225
|
+
`${config.dauthBasePath}/app/${config.domainName}/logout`,
|
|
226
|
+
{
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
refreshToken: session.refreshToken
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
).catch(() => {
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
clearCookies(res, config);
|
|
237
|
+
return res.status(200).send({ status: "success", message: "Logged out" });
|
|
238
|
+
});
|
|
239
|
+
router.patch("/user", async (req, res) => {
|
|
240
|
+
const config = await getConfig();
|
|
241
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
242
|
+
return res.status(403).send({ status: "csrf-invalid", message: "CSRF token invalid" });
|
|
243
|
+
}
|
|
244
|
+
const session = readSession(req, config);
|
|
245
|
+
if (!session) {
|
|
246
|
+
return res.status(401).send({ status: "no-session", message: "Not authenticated" });
|
|
247
|
+
}
|
|
248
|
+
const refreshed = await maybeRefreshTokens(session, config, res);
|
|
249
|
+
const response = await fetch(
|
|
250
|
+
`${config.dauthBasePath}/app/${config.domainName}/user`,
|
|
251
|
+
{
|
|
252
|
+
method: "PATCH",
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": "application/json",
|
|
255
|
+
Authorization: refreshed.accessToken
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify(req.body)
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
const data = await response.json();
|
|
261
|
+
return res.status(response.status).send(data);
|
|
262
|
+
});
|
|
263
|
+
router.delete("/user", async (req, res) => {
|
|
264
|
+
const config = await getConfig();
|
|
265
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
266
|
+
return res.status(403).send({ status: "csrf-invalid", message: "CSRF token invalid" });
|
|
267
|
+
}
|
|
268
|
+
const session = readSession(req, config);
|
|
269
|
+
if (!session) {
|
|
270
|
+
return res.status(401).send({ status: "no-session", message: "Not authenticated" });
|
|
271
|
+
}
|
|
272
|
+
const response = await fetch(
|
|
273
|
+
`${config.dauthBasePath}/app/${config.domainName}/user`,
|
|
274
|
+
{
|
|
275
|
+
method: "DELETE",
|
|
276
|
+
headers: { Authorization: session.accessToken }
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
clearCookies(res, config);
|
|
281
|
+
return res.status(response.status).send(data);
|
|
282
|
+
});
|
|
283
|
+
router.get(
|
|
284
|
+
"/profile-redirect",
|
|
285
|
+
async (req, res) => {
|
|
286
|
+
const config = await getConfig();
|
|
287
|
+
if (!verifyCsrf(req, config.csrfCookieName)) {
|
|
288
|
+
return res.status(403).send({
|
|
289
|
+
status: "csrf-invalid",
|
|
290
|
+
message: "CSRF token invalid"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const session = readSession(req, config);
|
|
294
|
+
if (!session) {
|
|
295
|
+
return res.status(401).send({
|
|
296
|
+
status: "no-session",
|
|
297
|
+
message: "Not authenticated"
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const refreshed = await maybeRefreshTokens(
|
|
301
|
+
session,
|
|
302
|
+
config,
|
|
303
|
+
res
|
|
304
|
+
);
|
|
305
|
+
const response = await fetch(
|
|
306
|
+
`${config.dauthBasePath}/app/${config.domainName}/profile-code`,
|
|
307
|
+
{
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
Authorization: refreshed.accessToken
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
return res.status(response.status).send({
|
|
317
|
+
status: "profile-code-error",
|
|
318
|
+
message: "Could not generate profile code"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const data = await response.json();
|
|
322
|
+
const dauthFrontendUrl = opts.dauthUrl ? opts.dauthUrl.replace(/\/+$/, "") : process.env.DAUTH_URL ? process.env.DAUTH_URL.replace(/\/+$/, "") : process.env.NODE_ENV === "development" ? "http://localhost:5185" : "https://dauth.ovh";
|
|
323
|
+
return res.status(200).send({
|
|
324
|
+
redirectUrl: `${dauthFrontendUrl}/${config.domainName}/update-user?code=${data.code}`
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
return router;
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
dauthRouter
|
|
332
|
+
};
|
|
333
|
+
//# sourceMappingURL=router.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/router.ts","../src/csrf.ts"],"sourcesContent":["import { Router, Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { getServerBasePath } from './api/utils/config';\nimport {\n deriveEncryptionKey,\n encryptSession,\n decryptSessionWithKeys,\n SessionPayload,\n} from './session';\nimport { generateCsrfToken, verifyCsrf } from './csrf';\n\nexport interface DauthRouterOptions {\n domainName: string;\n tsk: string;\n dauthUrl?: string;\n cookieName?: string;\n csrfCookieName?: string;\n maxAge?: number;\n secure?: boolean;\n previousTsk?: string;\n sessionSalt?: string;\n}\n\ninterface ResolvedConfig {\n domainName: string;\n dauthBasePath: string;\n cookieName: string;\n csrfCookieName: string;\n maxAgeMs: number;\n secure: boolean;\n encKeys: Buffer[];\n}\n\n// Refresh lock to prevent race conditions on concurrent token rotation\nconst refreshLocks = new Map<string, Promise<SessionPayload | null>>();\n\nfunction lockKey(refreshToken: string): string {\n return refreshToken.substring(0, 16);\n}\n\nfunction clearStaleLocks(): void {\n if (refreshLocks.size > 100) refreshLocks.clear();\n}\n\nasync function resolveConfig(\n opts: DauthRouterOptions\n): Promise<ResolvedConfig> {\n const secure =\n opts.secure ?? process.env.NODE_ENV !== 'development';\n const cookieName =\n opts.cookieName ??\n (secure ? '__Host-dauth-session' : 'dauth-session');\n const csrfCookieName =\n opts.csrfCookieName ?? (secure ? '__Host-csrf' : 'csrf-token');\n const maxAgeMs = (opts.maxAge ?? 30 * 24 * 3600) * 1000;\n\n const keys: Buffer[] = [];\n keys.push(await deriveEncryptionKey(opts.tsk, opts.sessionSalt));\n if (opts.previousTsk) {\n keys.push(\n await deriveEncryptionKey(opts.previousTsk, opts.sessionSalt)\n );\n }\n\n let dauthBasePath: string;\n if (opts.dauthUrl) {\n dauthBasePath = `${opts.dauthUrl.replace(/\\/+$/, '')}/api/v1`;\n } else {\n dauthBasePath = getServerBasePath();\n }\n\n return {\n domainName: opts.domainName,\n dauthBasePath,\n cookieName,\n csrfCookieName,\n maxAgeMs,\n secure,\n encKeys: keys,\n };\n}\n\nfunction setSessionCookie(\n res: Response,\n payload: SessionPayload,\n config: ResolvedConfig\n): void {\n const encrypted = encryptSession(payload, config.encKeys[0]);\n const cookieOpts: Record<string, unknown> = {\n httpOnly: true,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n };\n // __Host- prefix requires no domain attribute\n if (!config.secure) {\n // Dev mode: no __Host- prefix, no domain restriction needed\n }\n res.cookie(config.cookieName, encrypted, cookieOpts);\n}\n\nfunction setCsrfCookie(res: Response, config: ResolvedConfig): void {\n const csrfToken = generateCsrfToken();\n res.cookie(config.csrfCookieName, csrfToken, {\n httpOnly: false,\n secure: config.secure,\n sameSite: 'lax',\n maxAge: config.maxAgeMs,\n path: '/',\n });\n}\n\nfunction clearCookies(res: Response, config: ResolvedConfig): void {\n const baseOpts = { path: '/', secure: config.secure };\n res.clearCookie(config.cookieName, baseOpts);\n res.clearCookie(config.csrfCookieName, baseOpts);\n}\n\nfunction readSession(\n req: Request,\n config: ResolvedConfig\n): SessionPayload | null {\n const cookie = req.cookies?.[config.cookieName];\n if (!cookie) return null;\n return decryptSessionWithKeys(cookie, config.encKeys);\n}\n\nfunction isTokenExpiringSoon(token: string, thresholdMs = 300_000): boolean {\n try {\n const decoded = jwt.decode(token) as { exp?: number } | null;\n if (!decoded?.exp) return true;\n return decoded.exp * 1000 - Date.now() < thresholdMs;\n } catch {\n return true;\n }\n}\n\nasync function maybeRefreshTokens(\n session: SessionPayload,\n config: ResolvedConfig,\n res: Response\n): Promise<SessionPayload> {\n if (!isTokenExpiringSoon(session.accessToken)) return session;\n\n const key = lockKey(session.refreshToken);\n clearStaleLocks();\n\n const existingLock = refreshLocks.get(key);\n if (existingLock) {\n const result = await existingLock;\n return result ?? session;\n }\n\n const refreshPromise = (async (): Promise<SessionPayload | null> => {\n try {\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/refresh-token`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n );\n if (!response.ok) return null;\n const data = (await response.json()) as {\n accessToken?: string;\n refreshToken?: string;\n };\n if (!data.accessToken || !data.refreshToken) return null;\n const newSession: SessionPayload = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n setSessionCookie(res, newSession, config);\n return newSession;\n } catch {\n return null;\n }\n })();\n\n refreshLocks.set(key, refreshPromise);\n\n // Timeout safety net: clean lock after 10s\n const timeout = setTimeout(() => refreshLocks.delete(key), 10_000);\n refreshPromise.finally(() => {\n clearTimeout(timeout);\n refreshLocks.delete(key);\n });\n\n const result = await refreshPromise;\n return result ?? session;\n}\n\nexport function dauthRouter(opts: DauthRouterOptions): Router {\n const router = Router();\n let configPromise: Promise<ResolvedConfig> | null = null;\n\n async function getConfig(): Promise<ResolvedConfig> {\n if (!configPromise) configPromise = resolveConfig(opts);\n return configPromise;\n }\n\n // POST /exchange-code — no CSRF (no prior session)\n router.post('/exchange-code', async (req: Request, res: Response) => {\n const config = await getConfig();\n const { code } = req.body;\n if (!code) {\n return res\n .status(400)\n .send({ status: 'code-required', message: 'Code required' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/exchange-code`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n }\n );\n if (!response.ok) {\n return res\n .status(response.status)\n .send({ status: 'code-invalid', message: 'Code invalid' });\n }\n const data = (await response.json()) as {\n accessToken: string;\n refreshToken: string;\n isNewUser: boolean;\n };\n\n setSessionCookie(\n res,\n {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n },\n config\n );\n setCsrfCookie(res, config);\n\n // Fetch user data to return\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: data.accessToken },\n }\n );\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n isNewUser: data.isNewUser,\n });\n });\n\n // GET /session — no CSRF (read-only)\n router.get('/session', async (req: Request, res: Response) => {\n const config = await getConfig();\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const userResponse = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'GET',\n headers: { Authorization: refreshed.accessToken },\n }\n );\n if (!userResponse.ok) {\n clearCookies(res, config);\n return res\n .status(401)\n .send({ status: 'session-invalid', message: 'Session expired' });\n }\n const userData = (await userResponse.json()) as {\n user?: unknown;\n domain?: unknown;\n };\n return res.status(200).send({\n user: userData.user,\n domain: userData.domain,\n });\n });\n\n // POST /logout — CSRF required\n router.post('/logout', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (session) {\n // Revoke refresh token server-to-server (fire-and-forget)\n fetch(\n `${config.dauthBasePath}/app/${config.domainName}/logout`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n refreshToken: session.refreshToken,\n }),\n }\n ).catch(() => {});\n }\n clearCookies(res, config);\n return res\n .status(200)\n .send({ status: 'success', message: 'Logged out' });\n });\n\n // PATCH /user — CSRF required\n router.patch('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n const refreshed = await maybeRefreshTokens(session, config, res);\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n body: JSON.stringify(req.body),\n }\n );\n const data = await response.json();\n return res.status(response.status).send(data);\n });\n\n // DELETE /user — CSRF required\n router.delete('/user', async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res\n .status(403)\n .send({ status: 'csrf-invalid', message: 'CSRF token invalid' });\n }\n const session = readSession(req, config);\n if (!session) {\n return res\n .status(401)\n .send({ status: 'no-session', message: 'Not authenticated' });\n }\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/user`,\n {\n method: 'DELETE',\n headers: { Authorization: session.accessToken },\n }\n );\n const data = await response.json();\n clearCookies(res, config);\n return res.status(response.status).send(data);\n });\n\n // GET /profile-redirect — CSRF required (generates profile code)\n router.get(\n '/profile-redirect',\n async (req: Request, res: Response) => {\n const config = await getConfig();\n if (!verifyCsrf(req, config.csrfCookieName)) {\n return res.status(403).send({\n status: 'csrf-invalid',\n message: 'CSRF token invalid',\n });\n }\n const session = readSession(req, config);\n if (!session) {\n return res.status(401).send({\n status: 'no-session',\n message: 'Not authenticated',\n });\n }\n const refreshed = await maybeRefreshTokens(\n session,\n config,\n res\n );\n\n const response = await fetch(\n `${config.dauthBasePath}/app/${config.domainName}/profile-code`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: refreshed.accessToken,\n },\n }\n );\n if (!response.ok) {\n return res.status(response.status).send({\n status: 'profile-code-error',\n message: 'Could not generate profile code',\n });\n }\n const data = (await response.json()) as { code: string };\n\n // Build redirect URL to dauth frontend\n const dauthFrontendUrl = opts.dauthUrl\n ? opts.dauthUrl.replace(/\\/+$/, '')\n : process.env.DAUTH_URL\n ? process.env.DAUTH_URL.replace(/\\/+$/, '')\n : process.env.NODE_ENV === 'development'\n ? 'http://localhost:5185'\n : 'https://dauth.ovh';\n\n return res.status(200).send({\n redirectUrl: `${dauthFrontendUrl}/${config.domainName}/update-user?code=${data.code}`,\n });\n }\n );\n\n return router;\n}\n","import crypto from 'crypto';\nimport type { Request } from 'express';\n\nexport function generateCsrfToken(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n\nexport function verifyCsrf(\n req: Request,\n csrfCookieName: string\n): boolean {\n const headerToken = req.headers['x-csrf-token'] as string | undefined;\n const cookieToken = req.cookies?.[csrfCookieName] as\n | string\n | undefined;\n if (!headerToken || !cookieToken) return false;\n return headerToken === cookieToken;\n}\n"],"mappings":"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","names":["result"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dauth-md-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "David T. Pizarro Frick",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
13
|
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./router": {
|
|
16
|
+
"types": "./dist/router.d.ts",
|
|
17
|
+
"import": "./dist/router.mjs",
|
|
18
|
+
"require": "./dist/router.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": [
|
|
@@ -45,10 +50,24 @@
|
|
|
45
50
|
{
|
|
46
51
|
"path": "dist/index.mjs",
|
|
47
52
|
"limit": "10 KB"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"path": "dist/router.js",
|
|
56
|
+
"limit": "10 KB"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "dist/router.mjs",
|
|
60
|
+
"limit": "10 KB"
|
|
48
61
|
}
|
|
49
62
|
],
|
|
50
63
|
"peerDependencies": {
|
|
51
|
-
"express": "^4.18.0 || ^5.0.0"
|
|
64
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
65
|
+
"cookie-parser": "^1.4.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"cookie-parser": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
52
71
|
},
|
|
53
72
|
"dependencies": {
|
|
54
73
|
"jsonwebtoken": "^9.0.3"
|
|
@@ -63,6 +82,7 @@
|
|
|
63
82
|
"@types/express": "^5.0.0",
|
|
64
83
|
"@types/jsonwebtoken": "^9.0.10",
|
|
65
84
|
"@types/node": "~22.19.0",
|
|
85
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
66
86
|
"express": "^5.0.0",
|
|
67
87
|
"husky": "^9.1.7",
|
|
68
88
|
"prettier": "^3.8.1",
|
package/src/csrf.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
|
|
4
|
+
export function generateCsrfToken(): string {
|
|
5
|
+
return crypto.randomBytes(32).toString('hex');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function verifyCsrf(
|
|
9
|
+
req: Request,
|
|
10
|
+
csrfCookieName: string
|
|
11
|
+
): boolean {
|
|
12
|
+
const headerToken = req.headers['x-csrf-token'] as string | undefined;
|
|
13
|
+
const cookieToken = req.cookies?.[csrfCookieName] as
|
|
14
|
+
| string
|
|
15
|
+
| undefined;
|
|
16
|
+
if (!headerToken || !cookieToken) return false;
|
|
17
|
+
return headerToken === cookieToken;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken';
|
|
|
3
3
|
import { getUser } from './api/dauth.api';
|
|
4
4
|
import { UserCache } from './cache';
|
|
5
5
|
import type { CacheOptions } from './cache';
|
|
6
|
+
import { deriveEncryptionKey, decryptSessionWithKeys } from './session';
|
|
6
7
|
|
|
7
8
|
export type AuthMethodType = 'magic-link' | 'passkey';
|
|
8
9
|
|
|
@@ -41,10 +42,18 @@ export interface IRequestDauth extends Request {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export interface SessionOptions {
|
|
46
|
+
cookieName?: string;
|
|
47
|
+
secure?: boolean;
|
|
48
|
+
previousTsk?: string;
|
|
49
|
+
sessionSalt?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
export interface DauthOptions {
|
|
45
53
|
domainName: string;
|
|
46
54
|
tsk: string;
|
|
47
55
|
cache?: CacheOptions;
|
|
56
|
+
session?: SessionOptions;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
interface TCustomResponse extends ExpressResponse {
|
|
@@ -55,26 +64,92 @@ interface TCustomResponse extends ExpressResponse {
|
|
|
55
64
|
export { UserCache };
|
|
56
65
|
export type { CacheOptions };
|
|
57
66
|
|
|
58
|
-
export const dauth = ({
|
|
67
|
+
export const dauth = ({
|
|
68
|
+
domainName,
|
|
69
|
+
tsk,
|
|
70
|
+
cache,
|
|
71
|
+
session,
|
|
72
|
+
}: DauthOptions) => {
|
|
59
73
|
const userCache = cache ? new UserCache(cache) : null;
|
|
60
74
|
|
|
75
|
+
// Lazy-init encryption keys for session cookie mode
|
|
76
|
+
let keysPromise: Promise<Buffer[]> | null = null;
|
|
77
|
+
async function getEncKeys(): Promise<Buffer[]> {
|
|
78
|
+
if (!keysPromise) {
|
|
79
|
+
keysPromise = (async () => {
|
|
80
|
+
const keys: Buffer[] = [];
|
|
81
|
+
keys.push(
|
|
82
|
+
await deriveEncryptionKey(tsk, session?.sessionSalt)
|
|
83
|
+
);
|
|
84
|
+
if (session?.previousTsk) {
|
|
85
|
+
keys.push(
|
|
86
|
+
await deriveEncryptionKey(
|
|
87
|
+
session.previousTsk,
|
|
88
|
+
session.sessionSalt
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return keys;
|
|
93
|
+
})();
|
|
94
|
+
}
|
|
95
|
+
return keysPromise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getSessionCookieName(): string {
|
|
99
|
+
if (session?.cookieName) return session.cookieName;
|
|
100
|
+
const secure =
|
|
101
|
+
session?.secure ?? process.env.NODE_ENV !== 'development';
|
|
102
|
+
return secure ? '__Host-dauth-session' : 'dauth-session';
|
|
103
|
+
}
|
|
104
|
+
|
|
61
105
|
return async (
|
|
62
106
|
req: IRequestDauth,
|
|
63
107
|
res: TCustomResponse,
|
|
64
108
|
next: NextFunction
|
|
65
109
|
) => {
|
|
66
|
-
|
|
67
|
-
return res
|
|
68
|
-
.status(403)
|
|
69
|
-
.send({ status: 'token-not-found', message: 'Token not found' });
|
|
70
|
-
}
|
|
110
|
+
let token: string;
|
|
71
111
|
|
|
72
|
-
|
|
112
|
+
if (session) {
|
|
113
|
+
// Session cookie mode: read encrypted cookie
|
|
114
|
+
const cookieName = getSessionCookieName();
|
|
115
|
+
const cookie = req.cookies?.[cookieName];
|
|
116
|
+
if (!cookie) {
|
|
117
|
+
return res
|
|
118
|
+
.status(401)
|
|
119
|
+
.send({
|
|
120
|
+
status: 'no-session',
|
|
121
|
+
message: 'Not authenticated',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const keys = await getEncKeys();
|
|
125
|
+
const payload = decryptSessionWithKeys(cookie, keys);
|
|
126
|
+
if (!payload) {
|
|
127
|
+
return res
|
|
128
|
+
.status(401)
|
|
129
|
+
.send({
|
|
130
|
+
status: 'session-invalid',
|
|
131
|
+
message: 'Invalid session',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
token = payload.accessToken;
|
|
135
|
+
} else {
|
|
136
|
+
// Authorization header mode
|
|
137
|
+
if (!req.headers.authorization) {
|
|
138
|
+
return res
|
|
139
|
+
.status(403)
|
|
140
|
+
.send({
|
|
141
|
+
status: 'token-not-found',
|
|
142
|
+
message: 'Token not found',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
token = req.headers.authorization.replace(/['"]+/g, '');
|
|
146
|
+
}
|
|
73
147
|
|
|
74
148
|
try {
|
|
75
149
|
jwt.verify(token, tsk);
|
|
76
150
|
} catch (error) {
|
|
77
|
-
const message =
|
|
151
|
+
const message =
|
|
152
|
+
error instanceof Error ? error.message : 'Token invalid';
|
|
78
153
|
|
|
79
154
|
if (message === 'jwt expired') {
|
|
80
155
|
return res
|
|
@@ -84,10 +159,13 @@ export const dauth = ({ domainName, tsk, cache }: DauthOptions) => {
|
|
|
84
159
|
if (message === 'invalid signature') {
|
|
85
160
|
return res.status(401).send({
|
|
86
161
|
status: 'tsk-not-invalid',
|
|
87
|
-
message:
|
|
162
|
+
message:
|
|
163
|
+
'The TSK variable in the backend middleware is not valid',
|
|
88
164
|
});
|
|
89
165
|
}
|
|
90
|
-
return res
|
|
166
|
+
return res
|
|
167
|
+
.status(401)
|
|
168
|
+
.send({ status: 'token-invalid', message });
|
|
91
169
|
}
|
|
92
170
|
|
|
93
171
|
if (userCache) {
|
|
@@ -104,13 +182,15 @@ export const dauth = ({ domainName, tsk, cache }: DauthOptions) => {
|
|
|
104
182
|
if (getUserFetch.response.status === 404) {
|
|
105
183
|
return res.status(404).send({
|
|
106
184
|
status: 'user-not-found',
|
|
107
|
-
message:
|
|
185
|
+
message:
|
|
186
|
+
getUserFetch.data.message ?? 'User does not exist',
|
|
108
187
|
});
|
|
109
188
|
}
|
|
110
189
|
if (getUserFetch.response.status === 500) {
|
|
111
190
|
return res.status(500).send({
|
|
112
191
|
status: 'error',
|
|
113
|
-
message:
|
|
192
|
+
message:
|
|
193
|
+
getUserFetch.data.message ?? 'Dauth server error',
|
|
114
194
|
});
|
|
115
195
|
}
|
|
116
196
|
if (getUserFetch.response.status === 200) {
|
|
@@ -122,12 +202,17 @@ export const dauth = ({ domainName, tsk, cache }: DauthOptions) => {
|
|
|
122
202
|
}
|
|
123
203
|
return res.status(501).send({
|
|
124
204
|
status: 'request-error',
|
|
125
|
-
message:
|
|
205
|
+
message:
|
|
206
|
+
getUserFetch.data.message ?? 'Dauth server error',
|
|
126
207
|
});
|
|
127
208
|
} catch (error) {
|
|
128
209
|
const message =
|
|
129
|
-
error instanceof Error
|
|
130
|
-
|
|
210
|
+
error instanceof Error
|
|
211
|
+
? error.message
|
|
212
|
+
: 'Dauth server error';
|
|
213
|
+
return res
|
|
214
|
+
.status(500)
|
|
215
|
+
.send({ status: 'server-error', message });
|
|
131
216
|
}
|
|
132
217
|
};
|
|
133
218
|
};
|