@seamless-auth/express 0.0.2-beta.2 → 0.0.2-beta.21

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 CHANGED
@@ -2,561 +2,499 @@
2
2
  import express from "express";
3
3
  import cookieParser from "cookie-parser";
4
4
 
5
+ // src/middleware/ensureCookies.ts
6
+ import { ensureCookies } from "@seamless-auth/core";
7
+
5
8
  // src/internal/cookie.ts
6
9
  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, {
10
+ function setSessionCookie(res, opts, signer) {
11
+ const token = jwt.sign(opts.payload, signer.secret, {
14
12
  algorithm: "HS256",
15
- expiresIn: `${ttlSeconds}s`
13
+ expiresIn: `${opts.ttlSeconds}s`
16
14
  });
17
- res.cookie(name, token, {
15
+ res.cookie(opts.name, token, {
18
16
  httpOnly: true,
19
- secure: process.env.NODE_ENV === "production",
20
- sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
17
+ secure: signer.secure,
18
+ sameSite: signer.sameSite,
21
19
  path: "/",
22
- domain,
23
- maxAge: ttlSeconds * 1e3
20
+ domain: opts.domain,
21
+ maxAge: Number(opts.ttlSeconds) * 1e3
24
22
  });
25
23
  }
26
- function clearSessionCookie(res, domain, name = "sa_session") {
24
+ function clearSessionCookie(res, domain, name) {
27
25
  res.clearCookie(name, { domain, path: "/" });
28
26
  }
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: "/" });
27
+ function clearAllCookies(res, domain, ...cookieNames) {
28
+ for (const name of cookieNames) {
29
+ res.clearCookie(name, { domain, path: "/" });
30
+ }
33
31
  }
34
32
 
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"
33
+ // src/middleware/ensureCookies.ts
34
+ function createEnsureCookiesMiddleware(opts) {
35
+ if (!opts.cookieSecret) {
36
+ throw new Error("Missing cookieSecret");
37
+ }
38
+ if (!opts.serviceSecret) {
39
+ throw new Error("Missing serviceSecret");
40
+ }
41
+ const cookieSigner = {
42
+ secret: opts.cookieSecret,
43
+ secure: process.env.NODE_ENV === "production",
44
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax"
45
+ };
46
+ return async function ensureCookiesMiddleware(req, res, next) {
47
+ const result = await ensureCookies(
48
+ {
49
+ path: req.path,
50
+ cookies: req.cookies ?? {}
51
+ },
52
+ {
53
+ authServerUrl: opts.authServerUrl,
54
+ cookieDomain: opts.cookieDomain,
55
+ accessCookieName: opts.accessCookieName,
56
+ registrationCookieName: opts.registrationCookieName,
57
+ refreshCookieName: opts.refreshCookieName,
58
+ preAuthCookieName: opts.preAuthCookieName,
59
+ cookieSecret: opts.cookieSecret,
60
+ serviceSecret: opts.serviceSecret,
61
+ issuer: opts.issuer,
62
+ audience: opts.audience,
63
+ keyId: opts.keyId
64
+ }
42
65
  );
66
+ applyResult(res, req, result, opts, cookieSigner);
67
+ if (result.type === "error") return;
68
+ next();
69
+ };
70
+ }
71
+ function applyResult(res, req, result, opts, cookieSigner) {
72
+ if (result.clearCookies?.length) {
73
+ clearAllCookies(res, opts.cookieDomain, ...result.clearCookies);
43
74
  }
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,
75
+ if (result.setCookies) {
76
+ for (const c of result.setCookies) {
77
+ setSessionCookie(
78
+ res,
79
+ {
80
+ name: c.name,
81
+ payload: c.value,
82
+ domain: c.domain,
83
+ ttlSeconds: c.ttl
84
+ },
85
+ cookieSigner
86
+ );
87
+ }
88
+ }
89
+ if (result.user) {
90
+ req.cookiePayload = result.user;
91
+ }
92
+ if (result.type === "error") {
93
+ res.status(result.status ?? 401).json({ error: result.error });
94
+ }
95
+ }
96
+
97
+ // src/handlers/login.ts
98
+ import { loginHandler } from "@seamless-auth/core/handlers/login";
99
+ async function login(req, res, opts) {
100
+ const cookieSigner = {
101
+ secret: opts.cookieSecret,
102
+ secure: process.env.NODE_ENV === "production",
103
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax"
104
+ };
105
+ const result = await loginHandler(
106
+ { body: req.body },
53
107
  {
54
- expiresIn: "60s",
55
- // Short-lived
56
- algorithm: "HS256"
57
- // HMAC-based
108
+ authServerUrl: opts.authServerUrl,
109
+ cookieDomain: opts.cookieDomain,
110
+ preAuthCookieName: opts.preAuthCookieName
58
111
  }
59
112
  );
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}`;
113
+ if (!cookieSigner.secret) {
114
+ throw new Error("Missing COOKIE_SIGNING_KEY");
70
115
  }
71
- const res = await fetch(finalUrl, {
72
- method,
73
- headers: finalHeaders,
74
- ...method !== "GET" && body ? { body: JSON.stringify(body) } : {}
75
- });
76
- return res;
116
+ if (result.setCookies) {
117
+ for (const c of result.setCookies) {
118
+ setSessionCookie(
119
+ res,
120
+ {
121
+ name: c.name,
122
+ payload: c.value,
123
+ domain: c.domain,
124
+ ttlSeconds: c.ttl
125
+ },
126
+ cookieSigner
127
+ );
128
+ }
129
+ }
130
+ if (result.error) {
131
+ return res.status(result.status).json(result.error);
132
+ }
133
+ res.status(result.status).end();
77
134
  }
78
135
 
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;
136
+ // src/handlers/finishLogin.ts
137
+ import { finishLoginHandler } from "@seamless-auth/core/handlers/finishLogin";
138
+
139
+ // src/internal/buildAuthorization.ts
140
+ import { createServiceToken } from "@seamless-auth/core";
141
+ function buildServiceAuthorization(req, opts) {
142
+ if (!req.cookiePayload?.sub && !req.user.sub) {
143
+ return void 0;
90
144
  }
145
+ const token = createServiceToken({
146
+ subject: req.cookiePayload?.sub || req.user.sub,
147
+ issuer: opts.issuer,
148
+ audience: opts.audience,
149
+ serviceSecret: opts.serviceSecret,
150
+ keyId: opts.jwksKid || "dev-main"
151
+ });
152
+ return `Bearer ${token}`;
91
153
  }
92
154
 
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");
155
+ // src/handlers/finishLogin.ts
156
+ async function finishLogin(req, res, opts) {
157
+ const cookieSigner = {
158
+ secret: opts.cookieSecret,
159
+ secure: process.env.NODE_ENV === "production",
160
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax"
161
+ };
162
+ const authorization = buildServiceAuthorization(req, opts);
163
+ const result = await finishLoginHandler(
164
+ { body: req.body, authorization },
165
+ {
166
+ authServerUrl: opts.authServerUrl,
167
+ cookieDomain: opts.cookieDomain,
168
+ accessCookieName: opts.accessCookieName,
169
+ refreshCookieName: opts.refreshCookieName
103
170
  }
104
- const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
105
- if (!serviceKey) {
106
- throw new Error(
107
- "Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN"
171
+ );
172
+ if (!cookieSigner.secret) {
173
+ throw new Error("Missing COOKIE_SIGNING_KEY");
174
+ }
175
+ if (result.setCookies) {
176
+ for (const c of result.setCookies) {
177
+ setSessionCookie(
178
+ res,
179
+ {
180
+ name: c.name,
181
+ payload: c.value,
182
+ domain: c.domain,
183
+ ttlSeconds: c.ttl
184
+ },
185
+ cookieSigner
108
186
  );
109
187
  }
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
188
+ }
189
+ if (result.error) {
190
+ return res.status(result.status).json(result.error);
191
+ }
192
+ res.status(result.status).json(result.body).end();
193
+ }
194
+
195
+ // src/handlers/register.ts
196
+ import { registerHandler } from "@seamless-auth/core/handlers/register";
197
+ async function register(req, res, opts) {
198
+ const cookieSigner = {
199
+ secret: opts.cookieSecret,
200
+ secure: process.env.NODE_ENV === "production",
201
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax"
202
+ };
203
+ const result = await registerHandler(
204
+ { body: req.body },
205
+ {
206
+ authServerUrl: opts.authServerUrl,
207
+ cookieDomain: opts.cookieDomain,
208
+ registrationCookieName: opts.registrationCookieName
209
+ }
210
+ );
211
+ if (!cookieSigner.secret) {
212
+ throw new Error("Missing COOKIE_SIGNING_KEY");
213
+ }
214
+ if (result.setCookies) {
215
+ for (const c of result.setCookies) {
216
+ setSessionCookie(
217
+ res,
218
+ {
219
+ name: opts.registrationCookieName || "seamless-auth-registraion",
220
+ payload: c.value,
221
+ domain: c.domain,
222
+ ttlSeconds: c.ttl
223
+ },
224
+ cookieSigner
140
225
  );
141
- return null;
142
226
  }
143
- const data = await response.json();
144
- return data;
145
- } catch (err) {
146
- console.error("[SeamlessAuth] refreshAccessToken error:", err);
147
- return null;
148
227
  }
228
+ if (result.error) {
229
+ return res.status(result.status).json(result.error);
230
+ }
231
+ res.status(result.status).json(result.body).end();
149
232
  }
150
233
 
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 }
234
+ // src/handlers/finishRegister.ts
235
+ import { finishRegisterHandler } from "@seamless-auth/core/handlers/finishRegister";
236
+ async function finishRegister(req, res, opts) {
237
+ const cookieSigner = {
238
+ secret: opts.cookieSecret,
239
+ secure: process.env.NODE_ENV === "production",
240
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax"
174
241
  };
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
- });
242
+ const authorization = buildServiceAuthorization(req, opts);
243
+ const result = await finishRegisterHandler(
244
+ { body: req.body, authorization },
245
+ {
246
+ authServerUrl: opts.authServerUrl,
247
+ cookieDomain: opts.cookieDomain,
248
+ accessCookieName: opts.accessCookieName,
249
+ refreshCookieName: opts.refreshCookieName
231
250
  }
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;
251
+ );
252
+ if (!cookieSigner.secret) {
253
+ throw new Error("Missing COOKIE_SIGNING_KEY");
254
+ }
255
+ if (result.setCookies) {
256
+ for (const c of result.setCookies) {
257
+ setSessionCookie(
258
+ res,
259
+ {
260
+ name: c.name,
261
+ payload: c.value,
262
+ domain: c.domain,
263
+ ttlSeconds: c.ttl
264
+ },
265
+ cookieSigner
266
+ );
238
267
  }
239
- next();
240
- };
268
+ }
269
+ if (result.error) {
270
+ return res.status(result.status).json(result.error);
271
+ }
272
+ res.status(result.status).json({ message: "success" });
241
273
  }
242
274
 
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;
275
+ // src/handlers/me.ts
276
+ import { meHandler } from "@seamless-auth/core/handlers/me";
277
+ async function me(req, res, opts) {
278
+ const authorization = buildServiceAuthorization(req, opts);
279
+ const result = await meHandler({
280
+ authServerUrl: opts.authServerUrl,
281
+ preAuthCookieName: opts.preAuthCookieName,
282
+ authorization
283
+ });
284
+ if (result.clearCookies) {
285
+ for (const name of result.clearCookies) {
286
+ clearSessionCookie(res, opts.cookieDomain || "", name);
287
+ }
288
+ }
289
+ if (result.error) {
290
+ return res.status(result.status).json({ error: result.error });
257
291
  }
292
+ res.status(result.status).json(result.body);
293
+ }
294
+
295
+ // src/handlers/logout.ts
296
+ import { logoutHandler } from "@seamless-auth/core/handlers/logout";
297
+ async function logout(req, res, opts) {
298
+ const result = await logoutHandler({
299
+ authServerUrl: opts.authServerUrl,
300
+ accessCookieName: opts.accessCookieName,
301
+ registrationCookieName: opts.registrationCookieName,
302
+ refreshCookieName: opts.refreshCookieName
303
+ });
304
+ clearAllCookies(res, opts.cookieDomain || "", ...result.clearCookies);
305
+ res.status(result.status).end();
258
306
  }
259
307
 
260
308
  // src/createServer.ts
309
+ import {
310
+ authFetch
311
+ } from "@seamless-auth/core";
261
312
  function createSeamlessAuthServer(opts) {
262
313
  const r = express.Router();
263
314
  r.use(express.json());
264
315
  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}`);
316
+ const resolvedOpts = {
317
+ authServerUrl: opts.authServerUrl,
318
+ issuer: opts.issuer,
319
+ audience: opts.audience,
320
+ cookieSecret: opts.cookieSecret,
321
+ serviceSecret: opts.serviceSecret,
322
+ jwksKid: opts.jwksKid ?? "dev-main",
323
+ cookieDomain: opts.cookieDomain ?? "",
324
+ accessCookieName: opts.accessCookieName ?? "seamless-access",
325
+ registrationCookieName: opts.registrationCookieName ?? "seamless-ephemeral",
326
+ refreshCookieName: opts.refreshCookieName ?? "seamless-refresh",
327
+ preAuthCookieName: opts.preAuthCookieName ?? "seamless-ephemeral"
328
+ };
329
+ const proxyWithIdentity = (path, identity, method = "POST") => async (req, res) => {
330
+ if (!req.cookiePayload?.sub) {
331
+ res.status(401).json({ error: "unauthenticated" });
332
+ return;
282
333
  }
334
+ if (identity === "access" && !req.cookies[resolvedOpts.accessCookieName]) {
335
+ res.status(401).json({ error: "access session required" });
336
+ return;
337
+ }
338
+ if (identity === "preAuth" && !req.cookies[resolvedOpts.preAuthCookieName]) {
339
+ res.status(401).json({ error: "pre-auth session required" });
340
+ return;
341
+ }
342
+ if (identity === "register" && !req.cookies[resolvedOpts.registrationCookieName]) {
343
+ res.status(401).json({ error: "registeration session required" });
344
+ return;
345
+ }
346
+ const authorization = buildServiceAuthorization(req, resolvedOpts);
347
+ const options = method == "GET" ? { method, authorization } : { method, authorization, body: req.body };
348
+ const upstream = await authFetch(
349
+ `${resolvedOpts.authServerUrl}/${path}`,
350
+ options
351
+ );
352
+ const data = await upstream.json();
353
+ res.status(upstream.status).json(data);
283
354
  };
284
355
  r.use(
285
356
  createEnsureCookiesMiddleware({
286
- authServerUrl,
287
- cookieDomain,
288
- accesscookieName,
289
- registrationCookieName,
290
- refreshCookieName,
291
- preAuthCookieName
357
+ authServerUrl: resolvedOpts.authServerUrl,
358
+ cookieDomain: resolvedOpts.cookieDomain,
359
+ accessCookieName: resolvedOpts.accessCookieName,
360
+ registrationCookieName: resolvedOpts.registrationCookieName,
361
+ refreshCookieName: resolvedOpts.refreshCookieName,
362
+ preAuthCookieName: resolvedOpts.preAuthCookieName,
363
+ cookieSecret: resolvedOpts.cookieSecret,
364
+ serviceSecret: resolvedOpts.serviceSecret,
365
+ issuer: resolvedOpts.issuer,
366
+ audience: resolvedOpts.authServerUrl,
367
+ keyId: resolvedOpts.jwksKid
292
368
  })
293
369
  );
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);
370
+ r.post(
371
+ "/webAuthn/login/start",
372
+ proxyWithIdentity("webAuthn/login/start", "preAuth")
373
+ );
374
+ r.post(
375
+ "/webAuthn/login/finish",
376
+ (req, res) => finishLogin(req, res, resolvedOpts)
377
+ );
378
+ r.get(
379
+ "/webAuthn/register/start",
380
+ proxyWithIdentity("webAuthn/register/start", "preAuth", "GET")
381
+ );
382
+ r.post(
383
+ "/webAuthn/register/finish",
384
+ (req, res) => finishRegister(req, res, resolvedOpts)
385
+ );
386
+ r.post(
387
+ "/otp/verify-phone-otp",
388
+ proxyWithIdentity("otp/verify-phone-otp", "preAuth")
389
+ );
390
+ r.post(
391
+ "/otp/verify-email-otp",
392
+ proxyWithIdentity("otp/verify-email-otp", "preAuth")
393
+ );
394
+ r.post("/login", (req, res) => login(req, res, resolvedOpts));
395
+ r.post(
396
+ "/registration/register",
397
+ (req, res) => register(req, res, resolvedOpts)
398
+ );
399
+ r.get("/users/me", (req, res) => me(req, res, resolvedOpts));
400
+ r.get("/logout", (req, res) => logout(req, res, resolvedOpts));
401
+ r.post("/users/update", proxyWithIdentity("users/update", "access"));
402
+ r.post(
403
+ "/users/credentials",
404
+ proxyWithIdentity("users/credentials", "access")
405
+ );
406
+ r.delete(
407
+ "/users/credentials",
408
+ proxyWithIdentity("users/credentials", "access")
409
+ );
305
410
  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
411
  }
421
412
 
422
413
  // 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");
414
+ import { verifyCookieJwt } from "@seamless-auth/core";
415
+ function requireAuth(opts) {
416
+ const { cookieName = "seamless-access", cookieSecret } = opts;
417
+ if (!cookieSecret) {
418
+ throw new Error("requireAuth: missing cookieSecret");
431
419
  }
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" });
420
+ return function(req, res, next) {
421
+ const token = req.cookies?.[cookieName];
422
+ if (!token) {
423
+ res.status(401).json({
424
+ error: "Authentication required"
425
+ });
497
426
  return;
498
427
  }
428
+ const payload = verifyCookieJwt(token, cookieSecret);
429
+ if (!payload || !payload.sub) {
430
+ res.status(401).json({
431
+ error: "Invalid or expired session"
432
+ });
433
+ return;
434
+ }
435
+ const user = {
436
+ id: payload.sub,
437
+ sub: payload.sub,
438
+ // TODO: Silly to store the same value twice. Search every where its used and phase this out.
439
+ roles: Array.isArray(payload.roles) ? payload.roles : [],
440
+ email: payload.email,
441
+ phone: payload.phone,
442
+ iat: payload.iat,
443
+ exp: payload.exp
444
+ };
445
+ req.user = user;
446
+ next();
499
447
  };
500
448
  }
501
449
 
502
450
  // src/middleware/requireRole.ts
503
- import jwt6 from "jsonwebtoken";
504
- function requireRole(role, cookieName = "seamless-access") {
451
+ function requireRole(requiredRoles) {
452
+ const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
505
453
  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"]
454
+ const user = req.user;
455
+ if (!user) {
456
+ res.status(401).json({
457
+ error: "Authentication required"
521
458
  });
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" });
459
+ return;
460
+ }
461
+ if (!Array.isArray(user.roles)) {
462
+ res.status(403).json({
463
+ error: "User has no roles assigned"
464
+ });
465
+ return;
530
466
  }
467
+ const hasRole = roles.some((role) => user.roles.includes(role));
468
+ if (!hasRole) {
469
+ res.status(403).json({
470
+ error: "Insufficient role",
471
+ required: roles,
472
+ actual: user.roles
473
+ });
474
+ return;
475
+ }
476
+ next();
531
477
  };
532
478
  }
533
479
 
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
- }
480
+ // src/getSeamlessUser.ts
481
+ import {
482
+ getSeamlessUser as getSeamlessUserCore
483
+ } from "@seamless-auth/core";
484
+ async function getSeamlessUser(req, opts) {
485
+ const authorization = buildServiceAuthorization(req, opts);
486
+ return getSeamlessUserCore(req.cookies ?? {}, {
487
+ authServerUrl: opts.authServerUrl,
488
+ cookieSecret: opts.cookieSecret,
489
+ cookieName: opts.accessCookieName ?? "seamless-access",
490
+ authorization
491
+ });
555
492
  }
556
493
 
557
494
  // src/index.ts
558
495
  var index_default = createSeamlessAuthServer;
559
496
  export {
497
+ createEnsureCookiesMiddleware,
560
498
  index_default as default,
561
499
  getSeamlessUser,
562
500
  requireAuth,