@seamless-auth/express 0.0.2-beta.1 → 0.0.2-beta.2
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/index.js +564 -5
- package/package.json +3 -2
- package/dist/createServer.d.ts +0 -48
- package/dist/createServer.d.ts.map +0 -1
- package/dist/createServer.js +0 -164
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/internal/authFetch.d.ts +0 -9
- package/dist/internal/authFetch.d.ts.map +0 -1
- package/dist/internal/authFetch.js +0 -37
- package/dist/internal/cookie.d.ts +0 -11
- package/dist/internal/cookie.d.ts.map +0 -1
- package/dist/internal/cookie.js +0 -28
- package/dist/internal/getSeamlessUser.d.ts +0 -51
- package/dist/internal/getSeamlessUser.d.ts.map +0 -1
- package/dist/internal/getSeamlessUser.js +0 -72
- package/dist/internal/refreshAccessToken.d.ts +0 -10
- package/dist/internal/refreshAccessToken.d.ts.map +0 -1
- package/dist/internal/refreshAccessToken.js +0 -44
- package/dist/internal/verifyCookieJwt.d.ts +0 -2
- package/dist/internal/verifyCookieJwt.d.ts.map +0 -1
- package/dist/internal/verifyCookieJwt.js +0 -13
- package/dist/internal/verifySignedAuthResponse.d.ts +0 -6
- package/dist/internal/verifySignedAuthResponse.d.ts.map +0 -1
- package/dist/internal/verifySignedAuthResponse.js +0 -23
- package/dist/middleware/ensureCookies.d.ts +0 -8
- package/dist/middleware/ensureCookies.d.ts.map +0 -1
- package/dist/middleware/ensureCookies.js +0 -78
- package/dist/middleware/requireAuth.d.ts +0 -53
- package/dist/middleware/requireAuth.d.ts.map +0 -1
- package/dist/middleware/requireAuth.js +0 -118
- package/dist/middleware/requireRole.d.ts +0 -49
- package/dist/middleware/requireRole.d.ts.map +0 -1
- package/dist/middleware/requireRole.js +0 -77
- package/dist/types.d.ts +0 -9
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,564 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/createServer.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
import cookieParser from "cookie-parser";
|
|
4
|
+
|
|
5
|
+
// src/internal/cookie.ts
|
|
6
|
+
import jwt from "jsonwebtoken";
|
|
7
|
+
function setSessionCookie(res, payload, domain, ttlSeconds = 300, name = "sa_session") {
|
|
8
|
+
const COOKIE_SECRET2 = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
9
|
+
if (!COOKIE_SECRET2) {
|
|
10
|
+
console.warn("[SeamlessAuth] Missing SEAMLESS_COOKIE_SIGNING_KEY env var!");
|
|
11
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
12
|
+
}
|
|
13
|
+
const token = jwt.sign(payload, COOKIE_SECRET2, {
|
|
14
|
+
algorithm: "HS256",
|
|
15
|
+
expiresIn: `${ttlSeconds}s`
|
|
16
|
+
});
|
|
17
|
+
res.cookie(name, token, {
|
|
18
|
+
httpOnly: true,
|
|
19
|
+
secure: process.env.NODE_ENV === "production",
|
|
20
|
+
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
|
21
|
+
path: "/",
|
|
22
|
+
domain,
|
|
23
|
+
maxAge: ttlSeconds * 1e3
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function clearSessionCookie(res, domain, name = "sa_session") {
|
|
27
|
+
res.clearCookie(name, { domain, path: "/" });
|
|
28
|
+
}
|
|
29
|
+
function clearAllCookies(res, domain, accesscookieName, registrationCookieName, refreshCookieName) {
|
|
30
|
+
res.clearCookie(accesscookieName, { domain, path: "/" });
|
|
31
|
+
res.clearCookie(registrationCookieName, { domain, path: "/" });
|
|
32
|
+
res.clearCookie(refreshCookieName, { domain, path: "/" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/internal/authFetch.ts
|
|
36
|
+
import jwt2 from "jsonwebtoken";
|
|
37
|
+
async function authFetch(req, url, { method = "POST", body, cookies, headers = {} } = {}) {
|
|
38
|
+
const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
|
|
39
|
+
if (!serviceKey) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const token = jwt2.sign(
|
|
45
|
+
{
|
|
46
|
+
iss: process.env.FRONTEND_URL,
|
|
47
|
+
aud: process.env.AUTH_SERVER_URL,
|
|
48
|
+
sub: req.cookiePayload?.sub,
|
|
49
|
+
roles: req.cookiePayload?.roles ?? [],
|
|
50
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
51
|
+
},
|
|
52
|
+
serviceKey,
|
|
53
|
+
{
|
|
54
|
+
expiresIn: "60s",
|
|
55
|
+
// Short-lived
|
|
56
|
+
algorithm: "HS256"
|
|
57
|
+
// HMAC-based
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const finalHeaders = {
|
|
61
|
+
...method !== "GET" && { "Content-Type": "application/json" },
|
|
62
|
+
...cookies ? { Cookie: cookies.join("; ") } : {},
|
|
63
|
+
Authorization: `Bearer ${token}`,
|
|
64
|
+
...headers
|
|
65
|
+
};
|
|
66
|
+
let finalUrl = url;
|
|
67
|
+
if (method === "GET" && body && typeof body === "object") {
|
|
68
|
+
const qs = new URLSearchParams(body).toString();
|
|
69
|
+
finalUrl += url.includes("?") ? `&${qs}` : `?${qs}`;
|
|
70
|
+
}
|
|
71
|
+
const res = await fetch(finalUrl, {
|
|
72
|
+
method,
|
|
73
|
+
headers: finalHeaders,
|
|
74
|
+
...method !== "GET" && body ? { body: JSON.stringify(body) } : {}
|
|
75
|
+
});
|
|
76
|
+
return res;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/internal/verifyCookieJwt.ts
|
|
80
|
+
import jwt3 from "jsonwebtoken";
|
|
81
|
+
var COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
82
|
+
function verifyCookieJwt(token) {
|
|
83
|
+
try {
|
|
84
|
+
return jwt3.verify(token, COOKIE_SECRET, {
|
|
85
|
+
algorithms: ["HS256"]
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("[SeamlessAuth] Cookie JWT verification failed:", err);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/internal/refreshAccessToken.ts
|
|
94
|
+
import jwt4 from "jsonwebtoken";
|
|
95
|
+
async function refreshAccessToken(req, authServerUrl, refreshToken) {
|
|
96
|
+
try {
|
|
97
|
+
const COOKIE_SECRET2 = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
98
|
+
if (!COOKIE_SECRET2) {
|
|
99
|
+
console.warn(
|
|
100
|
+
"[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing \u2014 requireAuth will always fail."
|
|
101
|
+
);
|
|
102
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
103
|
+
}
|
|
104
|
+
const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
|
|
105
|
+
if (!serviceKey) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const payload = jwt4.verify(refreshToken, COOKIE_SECRET2, {
|
|
111
|
+
algorithms: ["HS256"]
|
|
112
|
+
});
|
|
113
|
+
const token = jwt4.sign(
|
|
114
|
+
{
|
|
115
|
+
// Minimal, safe fields
|
|
116
|
+
iss: process.env.FRONTEND_URL,
|
|
117
|
+
aud: process.env.AUTH_SERVER_URL,
|
|
118
|
+
sub: payload.sub,
|
|
119
|
+
refreshToken: payload.refreshToken,
|
|
120
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
121
|
+
},
|
|
122
|
+
serviceKey,
|
|
123
|
+
{
|
|
124
|
+
expiresIn: "60s",
|
|
125
|
+
// Short-lived = safer
|
|
126
|
+
algorithm: "HS256",
|
|
127
|
+
// HMAC-based
|
|
128
|
+
keyid: "dev-main"
|
|
129
|
+
// For future rotation
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
const response = await fetch(`${authServerUrl}/refresh`, {
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
console.error(
|
|
138
|
+
"[SeamlessAuth] Refresh token request failed:",
|
|
139
|
+
response.status
|
|
140
|
+
);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
return data;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[SeamlessAuth] refreshAccessToken error:", err);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/middleware/ensureCookies.ts
|
|
152
|
+
function createEnsureCookiesMiddleware(opts) {
|
|
153
|
+
const COOKIE_REQUIREMENTS = {
|
|
154
|
+
"/webAuthn/login/finish": { name: opts.preAuthCookieName, required: true },
|
|
155
|
+
"/webAuthn/login/start": { name: opts.preAuthCookieName, required: true },
|
|
156
|
+
"/webAuthn/register/start": {
|
|
157
|
+
name: opts.registrationCookieName,
|
|
158
|
+
required: true
|
|
159
|
+
},
|
|
160
|
+
"/webAuthn/register/finish": {
|
|
161
|
+
name: opts.registrationCookieName,
|
|
162
|
+
required: true
|
|
163
|
+
},
|
|
164
|
+
"/otp/verify-email-otp": {
|
|
165
|
+
name: opts.registrationCookieName,
|
|
166
|
+
required: true
|
|
167
|
+
},
|
|
168
|
+
"/otp/verify-phone-otp": {
|
|
169
|
+
name: opts.registrationCookieName,
|
|
170
|
+
required: true
|
|
171
|
+
},
|
|
172
|
+
"/logout": { name: opts.accesscookieName, required: true },
|
|
173
|
+
"/users/me": { name: opts.accesscookieName, required: true }
|
|
174
|
+
};
|
|
175
|
+
return async function ensureCookies(req, res, next, cookieDomain = "") {
|
|
176
|
+
const match = Object.entries(COOKIE_REQUIREMENTS).find(
|
|
177
|
+
([path]) => req.path.startsWith(path)
|
|
178
|
+
);
|
|
179
|
+
if (!match) return next();
|
|
180
|
+
const [, { name, required }] = match;
|
|
181
|
+
const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL;
|
|
182
|
+
const cookieValue = req.cookies?.[name];
|
|
183
|
+
const refreshCookieValue = req.cookies?.[opts.refreshCookieName];
|
|
184
|
+
if (required && !cookieValue) {
|
|
185
|
+
if (refreshCookieValue) {
|
|
186
|
+
console.log("[SeamlessAuth] Access token expired \u2014 attempting refresh");
|
|
187
|
+
const refreshed = await refreshAccessToken(
|
|
188
|
+
req,
|
|
189
|
+
AUTH_SERVER_URL,
|
|
190
|
+
refreshCookieValue
|
|
191
|
+
);
|
|
192
|
+
if (!refreshed?.token) {
|
|
193
|
+
clearAllCookies(
|
|
194
|
+
res,
|
|
195
|
+
cookieDomain,
|
|
196
|
+
name,
|
|
197
|
+
opts.registrationCookieName,
|
|
198
|
+
opts.refreshCookieName
|
|
199
|
+
);
|
|
200
|
+
res.status(401).json({ error: "Refresh failed" });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
setSessionCookie(
|
|
204
|
+
res,
|
|
205
|
+
{
|
|
206
|
+
sub: refreshed.sub,
|
|
207
|
+
token: refreshed.token,
|
|
208
|
+
roles: refreshed.roles
|
|
209
|
+
},
|
|
210
|
+
cookieDomain,
|
|
211
|
+
refreshed.ttl,
|
|
212
|
+
name
|
|
213
|
+
);
|
|
214
|
+
setSessionCookie(
|
|
215
|
+
res,
|
|
216
|
+
{ sub: refreshed.sub, refreshToken: refreshed.refreshToken },
|
|
217
|
+
cookieDomain,
|
|
218
|
+
refreshed.refreshTtl,
|
|
219
|
+
opts.refreshCookieName
|
|
220
|
+
);
|
|
221
|
+
req.cookiePayload = {
|
|
222
|
+
sub: refreshed.sub,
|
|
223
|
+
roles: refreshed.roles
|
|
224
|
+
};
|
|
225
|
+
return next();
|
|
226
|
+
}
|
|
227
|
+
return res.status(400).json({
|
|
228
|
+
error: `Missing required cookie "${name}" for route ${req.path}`,
|
|
229
|
+
hint: "Did you forget to call /auth/login/start first?"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (cookieValue) {
|
|
233
|
+
const payload = verifyCookieJwt(cookieValue);
|
|
234
|
+
if (!payload) {
|
|
235
|
+
return res.status(401).json({ error: `Invalid or expired ${name} cookie` });
|
|
236
|
+
}
|
|
237
|
+
req.cookiePayload = payload;
|
|
238
|
+
}
|
|
239
|
+
next();
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/internal/verifySignedAuthResponse.ts
|
|
244
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
245
|
+
async function verifySignedAuthResponse(token, authServerUrl) {
|
|
246
|
+
try {
|
|
247
|
+
const jwksUrl = new URL("/.well-known/jwks.json", authServerUrl).toString();
|
|
248
|
+
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
249
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
250
|
+
algorithms: ["RS256"],
|
|
251
|
+
issuer: authServerUrl
|
|
252
|
+
});
|
|
253
|
+
return payload;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error("[SeamlessAuth] Failed to verify signed auth response:", err);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/createServer.ts
|
|
261
|
+
function createSeamlessAuthServer(opts) {
|
|
262
|
+
const r = express.Router();
|
|
263
|
+
r.use(express.json());
|
|
264
|
+
r.use(cookieParser());
|
|
265
|
+
const {
|
|
266
|
+
authServerUrl,
|
|
267
|
+
cookieDomain = "",
|
|
268
|
+
accesscookieName = "seamless-access",
|
|
269
|
+
registrationCookieName = "seamless-ephemeral",
|
|
270
|
+
refreshCookieName = "seamless-refresh",
|
|
271
|
+
preAuthCookieName = "seamless-ephemeral"
|
|
272
|
+
} = opts;
|
|
273
|
+
const proxy = (path, method = "POST") => async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const response = await authFetch(req, `${authServerUrl}/${path}`, {
|
|
276
|
+
method,
|
|
277
|
+
body: req.body
|
|
278
|
+
});
|
|
279
|
+
res.status(response.status).json(await response.json());
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(`Failed to proxy to route. Error: ${error}`);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
r.use(
|
|
285
|
+
createEnsureCookiesMiddleware({
|
|
286
|
+
authServerUrl,
|
|
287
|
+
cookieDomain,
|
|
288
|
+
accesscookieName,
|
|
289
|
+
registrationCookieName,
|
|
290
|
+
refreshCookieName,
|
|
291
|
+
preAuthCookieName
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
r.post("/webAuthn/login/start", proxy("webAuthn/login/start"));
|
|
295
|
+
r.post("/webAuthn/login/finish", finishLogin);
|
|
296
|
+
r.get("/webAuthn/register/start", proxy("webAuthn/register/start", "GET"));
|
|
297
|
+
r.post("/webAuthn/register/finish", finishRegister);
|
|
298
|
+
r.post("/otp/verify-phone-otp", proxy("otp/verify-phone-otp"));
|
|
299
|
+
r.post("/otp/verify-email-otp", proxy("otp/verify-email-otp"));
|
|
300
|
+
r.post("/login", login);
|
|
301
|
+
r.post("/users/update", proxy("users/update"));
|
|
302
|
+
r.post("/registration/register", register);
|
|
303
|
+
r.get("/users/me", me);
|
|
304
|
+
r.get("/logout", logout);
|
|
305
|
+
return r;
|
|
306
|
+
async function login(req, res) {
|
|
307
|
+
const up = await authFetch(req, `${authServerUrl}/login`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: req.body
|
|
310
|
+
});
|
|
311
|
+
const data = await up.json();
|
|
312
|
+
if (!up.ok) return res.status(up.status).json(data);
|
|
313
|
+
const verified = await verifySignedAuthResponse(data.token, authServerUrl);
|
|
314
|
+
if (!verified) {
|
|
315
|
+
throw new Error("Invalid signed response from Auth Server");
|
|
316
|
+
}
|
|
317
|
+
if (verified.sub !== data.sub) {
|
|
318
|
+
throw new Error("Signature mismatch with data payload");
|
|
319
|
+
}
|
|
320
|
+
setSessionCookie(
|
|
321
|
+
res,
|
|
322
|
+
{ sub: data.sub },
|
|
323
|
+
cookieDomain,
|
|
324
|
+
data.ttl,
|
|
325
|
+
preAuthCookieName
|
|
326
|
+
);
|
|
327
|
+
res.status(204).end();
|
|
328
|
+
}
|
|
329
|
+
async function register(req, res) {
|
|
330
|
+
const up = await authFetch(req, `${authServerUrl}/registration/register`, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
body: req.body
|
|
333
|
+
});
|
|
334
|
+
const data = await up.json();
|
|
335
|
+
if (!up.ok) return res.status(up.status).json(data);
|
|
336
|
+
setSessionCookie(
|
|
337
|
+
res,
|
|
338
|
+
{ sub: data.sub },
|
|
339
|
+
cookieDomain,
|
|
340
|
+
data.ttl,
|
|
341
|
+
registrationCookieName
|
|
342
|
+
);
|
|
343
|
+
res.status(200).json(data).end();
|
|
344
|
+
}
|
|
345
|
+
async function finishLogin(req, res) {
|
|
346
|
+
const up = await authFetch(req, `${authServerUrl}/webAuthn/login/finish`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
body: req.body
|
|
349
|
+
});
|
|
350
|
+
const data = await up.json();
|
|
351
|
+
if (!up.ok) return res.status(up.status).json(data);
|
|
352
|
+
const verifiedAccessToken = await verifySignedAuthResponse(
|
|
353
|
+
data.token,
|
|
354
|
+
authServerUrl
|
|
355
|
+
);
|
|
356
|
+
if (!verifiedAccessToken) {
|
|
357
|
+
throw new Error("Invalid signed response from Auth Server");
|
|
358
|
+
}
|
|
359
|
+
if (verifiedAccessToken.sub !== data.sub) {
|
|
360
|
+
throw new Error("Signature mismatch with data payload");
|
|
361
|
+
}
|
|
362
|
+
setSessionCookie(
|
|
363
|
+
res,
|
|
364
|
+
{ sub: data.sub, roles: data.roles },
|
|
365
|
+
cookieDomain,
|
|
366
|
+
data.ttl,
|
|
367
|
+
accesscookieName
|
|
368
|
+
);
|
|
369
|
+
setSessionCookie(
|
|
370
|
+
res,
|
|
371
|
+
{ sub: data.sub, refreshToken: data.refreshToken },
|
|
372
|
+
req.hostname,
|
|
373
|
+
data.refreshTtl,
|
|
374
|
+
refreshCookieName
|
|
375
|
+
);
|
|
376
|
+
res.status(200).json(data).end();
|
|
377
|
+
}
|
|
378
|
+
async function finishRegister(req, res) {
|
|
379
|
+
const up = await authFetch(
|
|
380
|
+
req,
|
|
381
|
+
`${authServerUrl}/webAuthn/register/finish`,
|
|
382
|
+
{
|
|
383
|
+
method: "POST",
|
|
384
|
+
body: req.body
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
const data = await up.json();
|
|
388
|
+
if (!up.ok) return res.status(up.status).json(data);
|
|
389
|
+
setSessionCookie(
|
|
390
|
+
res,
|
|
391
|
+
{ sub: data.sub, roles: data.roles },
|
|
392
|
+
cookieDomain,
|
|
393
|
+
data.ttl,
|
|
394
|
+
accesscookieName
|
|
395
|
+
);
|
|
396
|
+
res.status(204).end();
|
|
397
|
+
}
|
|
398
|
+
async function logout(req, res) {
|
|
399
|
+
await authFetch(req, `${authServerUrl}/logout`, {
|
|
400
|
+
method: "GET"
|
|
401
|
+
});
|
|
402
|
+
clearAllCookies(
|
|
403
|
+
res,
|
|
404
|
+
cookieDomain,
|
|
405
|
+
accesscookieName,
|
|
406
|
+
registrationCookieName,
|
|
407
|
+
refreshCookieName
|
|
408
|
+
);
|
|
409
|
+
res.status(204).end();
|
|
410
|
+
}
|
|
411
|
+
async function me(req, res) {
|
|
412
|
+
const up = await authFetch(req, `${authServerUrl}/users/me`, {
|
|
413
|
+
method: "GET"
|
|
414
|
+
});
|
|
415
|
+
const data = await up.json();
|
|
416
|
+
clearSessionCookie(res, cookieDomain, preAuthCookieName);
|
|
417
|
+
if (!data.user) return res.status(401).json({ error: "unauthenticated" });
|
|
418
|
+
res.json({ user: data.user });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/middleware/requireAuth.ts
|
|
423
|
+
import jwt5 from "jsonwebtoken";
|
|
424
|
+
function requireAuth(cookieName = "seamless-access", refreshCookieName = "seamless-refresh", cookieDomain = "/") {
|
|
425
|
+
const COOKIE_SECRET2 = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
426
|
+
if (!COOKIE_SECRET2) {
|
|
427
|
+
console.warn(
|
|
428
|
+
"[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing \u2014 requireAuth will always fail."
|
|
429
|
+
);
|
|
430
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
431
|
+
}
|
|
432
|
+
const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL;
|
|
433
|
+
return async (req, res, next) => {
|
|
434
|
+
try {
|
|
435
|
+
if (!COOKIE_SECRET2) {
|
|
436
|
+
throw new Error("Missing required SEAMLESS_COOKIE_SIGNING_KEY env");
|
|
437
|
+
}
|
|
438
|
+
const token = req.cookies?.[cookieName];
|
|
439
|
+
if (!token) {
|
|
440
|
+
res.status(401).json({ error: "Missing access cookie" });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const payload = jwt5.verify(token, COOKIE_SECRET2, {
|
|
445
|
+
algorithms: ["HS256"]
|
|
446
|
+
});
|
|
447
|
+
req.user = payload;
|
|
448
|
+
return next();
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (err.name !== "TokenExpiredError") {
|
|
451
|
+
console.warn("[SeamlessAuth] Invalid token:", err.message);
|
|
452
|
+
res.status(401).json({ error: "Invalid token" });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const refreshToken = req.cookies?.[refreshCookieName];
|
|
456
|
+
if (!refreshToken) {
|
|
457
|
+
res.status(401).json({ error: "Session expired; re-login required" });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log("[SeamlessAuth] Access token expired \u2014 attempting refresh");
|
|
461
|
+
const refreshed = await refreshAccessToken(
|
|
462
|
+
req,
|
|
463
|
+
AUTH_SERVER_URL,
|
|
464
|
+
refreshToken
|
|
465
|
+
);
|
|
466
|
+
if (!refreshed?.token) {
|
|
467
|
+
res.status(401).json({ error: "Refresh failed" });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
setSessionCookie(
|
|
471
|
+
res,
|
|
472
|
+
{
|
|
473
|
+
sub: refreshed.sub,
|
|
474
|
+
token: refreshed.token,
|
|
475
|
+
roles: refreshed.roles
|
|
476
|
+
},
|
|
477
|
+
cookieDomain,
|
|
478
|
+
refreshed.ttl,
|
|
479
|
+
cookieName
|
|
480
|
+
);
|
|
481
|
+
setSessionCookie(
|
|
482
|
+
res,
|
|
483
|
+
{ sub: refreshed.sub, refreshToken: refreshed.refreshToken },
|
|
484
|
+
req.hostname,
|
|
485
|
+
refreshed.refreshTtl,
|
|
486
|
+
refreshCookieName
|
|
487
|
+
);
|
|
488
|
+
const payload = jwt5.verify(refreshed.token, COOKIE_SECRET2, {
|
|
489
|
+
algorithms: ["HS256"]
|
|
490
|
+
});
|
|
491
|
+
req.user = payload;
|
|
492
|
+
next();
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error("[SeamlessAuth] requireAuth error:", err.message);
|
|
496
|
+
res.status(401).json({ error: "Invalid or expired access cookie" });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/middleware/requireRole.ts
|
|
503
|
+
import jwt6 from "jsonwebtoken";
|
|
504
|
+
function requireRole(role, cookieName = "seamless-access") {
|
|
505
|
+
return (req, res, next) => {
|
|
506
|
+
try {
|
|
507
|
+
const COOKIE_SECRET2 = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
508
|
+
if (!COOKIE_SECRET2) {
|
|
509
|
+
console.warn(
|
|
510
|
+
"[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing \u2014 requireRole will always fail."
|
|
511
|
+
);
|
|
512
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
513
|
+
}
|
|
514
|
+
const token = req.cookies?.[cookieName];
|
|
515
|
+
if (!token) {
|
|
516
|
+
res.status(401).json({ error: "Missing access cookie" });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const payload = jwt6.verify(token, COOKIE_SECRET2, {
|
|
520
|
+
algorithms: ["HS256"]
|
|
521
|
+
});
|
|
522
|
+
if (!payload.roles?.includes(role)) {
|
|
523
|
+
res.status(403).json({ error: `Forbidden: ${role} role required` });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
next();
|
|
527
|
+
} catch (err) {
|
|
528
|
+
console.error(`[RequireRole] requireRole(${role}) failed:`, err.message);
|
|
529
|
+
res.status(401).json({ error: "Invalid or expired access cookie" });
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/internal/getSeamlessUser.ts
|
|
535
|
+
async function getSeamlessUser(req, authServerUrl, cookieName = "seamless-access") {
|
|
536
|
+
try {
|
|
537
|
+
const payload = verifyCookieJwt(req.cookies[cookieName]);
|
|
538
|
+
if (!payload) {
|
|
539
|
+
throw new Error("Missing cookie");
|
|
540
|
+
}
|
|
541
|
+
req.cookiePayload = payload;
|
|
542
|
+
const response = await authFetch(req, `${authServerUrl}/users/me`, {
|
|
543
|
+
method: "GET"
|
|
544
|
+
});
|
|
545
|
+
if (!response.ok) {
|
|
546
|
+
console.warn(`[SeamlessAuth] Auth server responded ${response.status}`);
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const data = await response.json();
|
|
550
|
+
return data.user;
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error("[SeamlessAuth] getSeamlessUser failed:", err);
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/index.ts
|
|
558
|
+
var index_default = createSeamlessAuthServer;
|
|
559
|
+
export {
|
|
560
|
+
index_default as default,
|
|
561
|
+
getSeamlessUser,
|
|
562
|
+
requireAuth,
|
|
563
|
+
requireRole
|
|
564
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamless-auth/express",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.2",
|
|
4
4
|
"description": "Express adapter for Seamless Auth passwordless authentication",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"author": "Fells Code LLC",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "
|
|
11
|
+
"build": "tsup src/index.ts --format esm --out-dir dist --splitting",
|
|
12
12
|
"dev": "tsc --watch"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/cookie-parser": "^1.4.10",
|
|
34
34
|
"@types/jsonwebtoken": "^9.0.10",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
35
36
|
"typescript": "^5.5.0"
|
|
36
37
|
},
|
|
37
38
|
"publishConfig": {
|
package/dist/createServer.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import type { SeamlessAuthServerOptions } from "./types";
|
|
3
|
-
/**
|
|
4
|
-
* Creates an Express Router that proxies all authentication traffic to a Seamless Auth server.
|
|
5
|
-
*
|
|
6
|
-
* This helper wires your API backend to a Seamless Auth instance running in
|
|
7
|
-
* "server mode." It automatically forwards login, registration, WebAuthn,
|
|
8
|
-
* logout, token refresh, and session validation routes to the auth server
|
|
9
|
-
* and handles all cookie management required for a seamless login flow.
|
|
10
|
-
*
|
|
11
|
-
* ### Responsibilities
|
|
12
|
-
* - Proxies all `/auth/*` routes to the upstream Seamless Auth server
|
|
13
|
-
* - Manages `access`, `registration`, `pre-auth`, and `refresh` cookies
|
|
14
|
-
* - Normalizes cookie settings for cross-domain or same-domain deployments
|
|
15
|
-
* - Ensures authentication routes behave consistently across environments
|
|
16
|
-
* - Provides shared middleware for auth flows
|
|
17
|
-
*
|
|
18
|
-
* ### Cookie Types
|
|
19
|
-
* - **accessCookie** – long-lived session cookie for authenticated API requests
|
|
20
|
-
* - **registrationCookie** – ephemeral cookie used during registration and OTP/WebAuthn flows
|
|
21
|
-
* - **preAuthCookie** – short-lived cookie used during login initiation
|
|
22
|
-
* - **refreshCookie** – opaque refresh token cookie used to rotate session tokens
|
|
23
|
-
*
|
|
24
|
-
* All cookie names and their domains may be customized via the `opts` parameter.
|
|
25
|
-
*
|
|
26
|
-
* ### Example
|
|
27
|
-
* ```ts
|
|
28
|
-
* app.use("/auth", createSeamlessAuthServer({
|
|
29
|
-
* authServerUrl: "https://identifier.seamlessauth.com",
|
|
30
|
-
* cookieDomain: "mycompany.com",
|
|
31
|
-
* accesscookieName: "sa_access",
|
|
32
|
-
* registrationCookieName: "sa_registration",
|
|
33
|
-
* refreshCookieName: "sa_refresh",
|
|
34
|
-
* }));
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* @param opts - Configuration options for the Seamless Auth proxy:
|
|
38
|
-
* - `authServerUrl` — Base URL of your Seamless Auth instance (required)
|
|
39
|
-
* - `cookieDomain` — Domain attribute applied to all auth cookies
|
|
40
|
-
* - `accesscookieName` — Name of the session access cookie
|
|
41
|
-
* - `registrationCookieName` — Name of the ephemeral registration cookie
|
|
42
|
-
* - `refreshCookieName` — Name of the refresh token cookie
|
|
43
|
-
* - `preAuthCookieName` — Name of the cookie used during login initiation
|
|
44
|
-
*
|
|
45
|
-
* @returns An Express `Router` preconfigured with all Seamless Auth routes.
|
|
46
|
-
*/
|
|
47
|
-
export declare function createSeamlessAuthServer(opts: SeamlessAuthServerOptions): Router;
|
|
48
|
-
//# sourceMappingURL=createServer.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAqB,MAAM,EAAE,MAAM,SAAS,CAAC;AAQ7D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAIzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,yBAAyB,GAC9B,MAAM,CA0LR"}
|