@seamless-auth/express 0.0.0 → 0.0.1-beta.3

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/README.md CHANGED
@@ -1,13 +1,268 @@
1
1
  # @seamless-auth/server-express
2
2
 
3
- Drop-in Express adapter for Seamless Auth “server mode” authentication.
3
+ ### Seamless Auth Express Adapter
4
4
 
5
- ```js
5
+ A secure, passwordless **server-side adapter** that connects your Express API to a private Seamless Auth Server.
6
+
7
+ It proxies all authentication flows, manages signed cookies, and gives you out-of-the-box middleware for verifying users and enforcing roles.
8
+
9
+ > **npm:** https://www.npmjs.com/package/@seamless-auth/express
10
+ > **Docs:** https://docs.seamlessauth.com
11
+ > **Repo:** https://github.com/fells-code/seamless-auth-server
12
+
13
+ > Couple with https://github.com/fells-code/seamless-auth/react for an end to end seamless experience
14
+
15
+ > Or get a full starter application with https://github.com/fells-code/create-seamless
16
+
17
+ ---
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @seamless-auth/server-express
25
+ # or
26
+ yarn add @seamless-auth/server-express
27
+ ```
28
+
29
+ ## Quick Example
30
+
31
+ ```ts
6
32
  import express from "express";
7
- import createSeamlessAuthServer from "@seamless-auth/server-express";
33
+ import cookieParser from "cookie-parser";
34
+ import createSeamlessAuthServer, {
35
+ requireAuth,
36
+ requireRole,
37
+ } from "@seamless-auth/server-express";
8
38
 
9
39
  const app = express();
10
- app.use("/auth", createSeamlessAuthServer({
11
- authServerUrl: process.env.AUTH_SERVER_URL,
12
- cookieDomain: ".myapp.com"
13
- }));
40
+ app.use(cookieParser());
41
+
42
+ // Public Seamless Auth endpoints
43
+ app.use(
44
+ "/auth",
45
+ createSeamlessAuthServer({ authServerUrl: process.env.AUTH_SERVER_URL! })
46
+ );
47
+
48
+ // Everything after this line requires authentication
49
+ app.use(requireAuth());
50
+
51
+ app.get("/api/me", (req, res) => res.json({ user: (req as any).user }));
52
+ app.get("/admin", requireRole("admin"), (req, res) =>
53
+ res.json({ message: "Welcome admin!" })
54
+ );
55
+
56
+ app.listen(5000, () => console.log("Portal API running on :5000"));
57
+ ```
58
+
59
+ ---
60
+
61
+ # Full Documentation
62
+
63
+ ## Overview
64
+
65
+ `@seamless-auth/express` lets your backend API act as an authentication and authorization server using Seamless Auth.
66
+
67
+ It transparently proxies and validates authentication flows so your frontend can use a single API endpoint for:
68
+
69
+ - Login / Registration / Logout
70
+ - User introspection (`/auth/me`)
71
+ - Session cookies (signed JWTs)
72
+ - Role & permission guards
73
+ - Internal Auth Server communication (JWKS + service tokens)
74
+
75
+ Everything happens securely between your API and a private Seamless Auth Server.
76
+
77
+ ---
78
+
79
+ ## Architecture
80
+
81
+ ```
82
+ [Frontend App]
83
+
84
+
85
+ [Your Express API]
86
+ ├─ createSeamlessAuthServer() ← mounts /auth routes
87
+ ├─ requireAuth() ← verifies signed cookie JWT
88
+ ├─ requireRole('admin') ← role-based guard
89
+ └─ getSeamlessUser() ← calls Auth Server
90
+
91
+
92
+ [Private Seamless Auth Server]
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Environment Variables
98
+
99
+ | Variable | Description | Example |
100
+ | ----------------------------- | -------------------------------------- | ------------------------- |
101
+ | `AUTH_SERVER_URL` | Base URL of your Seamless Auth Server | `https://auth.client.com` |
102
+ | `SEAMLESS_COOKIE_SIGNING_KEY` | Secret key for signing JWT cookies | `base64:...` |
103
+ | `SEAMLESS_SERVICE_TOKEN` | Private key for API → Auth Server JWTs | RSA PEM |
104
+ | `SERVICE_JWT_KEYID` | Key ID for JWKS | `service-main` |
105
+ | `COOKIE_DOMAIN` | Domain for cookies | `.client.com` |
106
+
107
+ ---
108
+
109
+ ## API Reference
110
+
111
+ ### `createSeamlessAuthServer(options)`
112
+
113
+ Mounts an Express router exposing the full Seamless Auth flow:
114
+
115
+ - `/auth/login/start`
116
+ - `/auth/login/finish`
117
+ - `/auth/webauthn/...`
118
+ - `/auth/registration/...`
119
+ - `/auth/me`
120
+ - `/auth/logout`
121
+
122
+ **Options**
123
+
124
+ ```ts
125
+ {
126
+ authServerUrl: string; // required
127
+ cookieDomain?: string;
128
+ cookieNameOverrides?: {
129
+ preauth?: string;
130
+ registration?: string;
131
+ access?: string;
132
+ };
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ### `requireAuth(cookieName?: string)`
139
+
140
+ Middleware that validates the signed access cookie (`seamless_auth_access` by default)
141
+ and attaches the decoded user payload to `req.user`.
142
+
143
+ ```ts
144
+ app.get("/api/profile", requireAuth(), (req, res) => {
145
+ res.json({ user: req.user });
146
+ });
147
+ ```
148
+
149
+ ---
150
+
151
+ ### `requireRole(role: string, cookieName?: string)`
152
+
153
+ Role-based authorization guard.
154
+ Blocks non-matching roles with HTTP 403.
155
+
156
+ ```ts
157
+ app.get("/admin", requireRole("admin"), (req, res) => {
158
+ res.json({ message: "Welcome admin!" });
159
+ });
160
+ ```
161
+
162
+ ---
163
+
164
+ ### `getSeamlessUser(req, authServerUrl, cookieName?)`
165
+
166
+ Calls the Auth Server’s `/internal/session/introspect` endpoint using a signed service JWT
167
+ and returns the Seamless user object.
168
+
169
+ ```ts
170
+ const user = await getSeamlessUser(req, process.env.AUTH_SERVER_URL!);
171
+ ```
172
+
173
+ User shape
174
+
175
+ ```ts
176
+ {
177
+ id: string;
178
+ email: string;
179
+ phone: string;
180
+ roles: string[]
181
+ }
182
+ ```
183
+
184
+ ## End-to-End Flow
185
+
186
+ 1. **Frontend** → `/auth/login/start`
187
+ → API proxies to Seamless Auth Server
188
+ → sets short-lived pre-auth cookie.
189
+
190
+ 2. **Frontend** → `/auth/webauthn/finish`
191
+ → API proxies, validates, sets access cookie (`seamless_auth_access`).
192
+
193
+ 3. **Subsequent API calls** → `/api/...`
194
+ → `requireAuth()` verifies cookie and attaches user.
195
+ → Role routes use `requireRole()`.
196
+
197
+ ---
198
+
199
+ ## Local Development
200
+
201
+ In order to develop with your Seamless Auth server instance, you will need to have:
202
+
203
+ - Created an account @ https://dashboard.seamlessauth.com
204
+ - Created a new Seamless Auth application
205
+
206
+ Example env:
207
+
208
+ ```bash
209
+ AUTH_SERVER_URL=http://https://<identifier>.seamlessauth.com # Found in the portal
210
+ COOKIE_DOMAIN=localhost # Or frontend domain in prod
211
+ SEAMLESS_COOKIE_SIGNING_KEY=local-secret-key # Found in the portal
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Example Middleware Stack
217
+
218
+ ```ts
219
+ const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL!;
220
+ app.use(cors({ origin: "https://localhost:5001", credentials: true }));
221
+ app.use(express.json());
222
+ app.use(cookieParser());
223
+ app.use("/auth", createSeamlessAuthServer({ authServerUrl: AUTH_SERVER_URL }));
224
+ app.use(requireAuth());
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Security Model
230
+
231
+ | Layer | Auth Mechanism | Signed By |
232
+ | --------------------- | ------------------------------------- | ------------------ |
233
+ | **Frontend ↔ API** | Signed JWT in HttpOnly cookie (HS256) | Client API |
234
+ | **API ↔ Auth Server** | Bearer Service JWT (RS256) | API’s private key |
235
+ | **Auth Server** | Validates service tokens via JWKS | Seamless Auth JWKS |
236
+
237
+ All tokens and cookies are stateless and cryptographically verifiable.
238
+
239
+ ---
240
+
241
+ ## Testing
242
+
243
+ You can mock `requireAuth` and test Express routes via `supertest`.
244
+
245
+ Example:
246
+
247
+ ```ts
248
+ import { requireAuth } from "@seamless-auth/server-express";
249
+ app.get("/api/test", requireAuth(), (req, res) => res.json({ ok: true }));
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Roadmap
255
+
256
+ | Feature | Status |
257
+ | -------------------------------------------- | ----------- |
258
+ | JWKS-verified response signing | ✅ |
259
+ | OIDC discovery & SSO readiness | planned |
260
+ | Federation (Google / Okta) | future |
261
+ | Multi-framework adapters (Next.js / Fastify) | coming soon |
262
+
263
+ ---
264
+
265
+ ## License
266
+
267
+ MIT © 2025 Fells Code LLC
268
+ Part of the **Seamless Auth** ecosystem.
@@ -8,10 +8,13 @@ export function createSeamlessAuthServer(opts) {
8
8
  const r = express.Router();
9
9
  r.use(express.json());
10
10
  r.use(cookieParser());
11
- const { authServerUrl, cookieDomain = "", accesscookieName = "seamless-auth-access", registrationCookieName = "seamless-auth-registration", refreshCookieName = "seamless-auth-refresh", preAuthCookieName = "seamless-auth-pre-auth" } = opts;
11
+ const { authServerUrl, cookieDomain = "", accesscookieName = "seamless-access", registrationCookieName = "seamless-ephemeral", refreshCookieName = "seamless-refresh", preAuthCookieName = "seamless-ephemeral", } = opts;
12
12
  const proxy = (path, method = "POST") => async (req, res) => {
13
13
  try {
14
- const response = await authFetch(req, `${authServerUrl}/${path}`, { method, body: req.body });
14
+ const response = await authFetch(req, `${authServerUrl}/${path}`, {
15
+ method,
16
+ body: req.body,
17
+ });
15
18
  res.status(response.status).json(await response.json());
16
19
  }
17
20
  catch (error) {
@@ -24,7 +27,7 @@ export function createSeamlessAuthServer(opts) {
24
27
  accesscookieName,
25
28
  registrationCookieName,
26
29
  refreshCookieName,
27
- preAuthCookieName
30
+ preAuthCookieName,
28
31
  }));
29
32
  r.post("/webAuthn/login/start", proxy("webAuthn/login/start"));
30
33
  r.post("/webAuthn/login/finish", finishLogin);
@@ -33,9 +36,10 @@ export function createSeamlessAuthServer(opts) {
33
36
  r.post("/otp/verify-phone-otp", proxy("otp/verify-phone-otp"));
34
37
  r.post("/otp/verify-email-otp", proxy("otp/verify-email-otp"));
35
38
  r.post("/login", login);
39
+ r.post("/users/update", proxy("users/update"));
36
40
  r.post("/registration/register", register);
37
- r.post("/logout", logout);
38
41
  r.get("/users/me", me);
42
+ r.get("/logout", logout);
39
43
  return r;
40
44
  async function login(req, res) {
41
45
  const up = await authFetch(req, `${authServerUrl}/login`, {
@@ -75,15 +79,14 @@ export function createSeamlessAuthServer(opts) {
75
79
  if (!up.ok)
76
80
  return res.status(up.status).json(data);
77
81
  const verifiedAccessToken = await verifySignedAuthResponse(data.token, authServerUrl);
78
- const verifiedRefreshToken = await verifySignedAuthResponse(data.refreshToken, authServerUrl);
79
- if (!verifiedAccessToken || !verifiedRefreshToken) {
82
+ if (!verifiedAccessToken) {
80
83
  throw new Error("Invalid signed response from Auth Server");
81
84
  }
82
- if (verifiedAccessToken.sub !== data.sub || verifiedRefreshToken.sub !== data.sub) {
85
+ if (verifiedAccessToken.sub !== data.sub) {
83
86
  throw new Error("Signature mismatch with data payload");
84
87
  }
85
88
  setSessionCookie(res, { sub: data.sub, roles: data.roles }, cookieDomain, data.ttl, accesscookieName);
86
- setSessionCookie(res, { sub: data.sub, refreshToken: data.refreshToken }, cookieDomain, data.ttl, refreshCookieName);
89
+ setSessionCookie(res, { sub: data.sub, refreshToken: data.refreshToken }, req.hostname, data.refreshTtl, refreshCookieName);
87
90
  res.status(200).json(data).end();
88
91
  }
89
92
  async function finishRegister(req, res) {
@@ -98,12 +101,9 @@ export function createSeamlessAuthServer(opts) {
98
101
  res.status(204).end();
99
102
  }
100
103
  async function logout(req, res) {
101
- const sid = req.cookies[accesscookieName];
102
- if (sid)
103
- await authFetch(req, `${authServerUrl}/logout`, {
104
- method: "POST",
105
- body: { sid },
106
- });
104
+ await authFetch(req, `${authServerUrl}/logout`, {
105
+ method: "GET",
106
+ });
107
107
  clearAllCookies(res, cookieDomain, accesscookieName, registrationCookieName, refreshCookieName);
108
108
  res.status(204).end();
109
109
  }
@@ -112,14 +112,6 @@ export function createSeamlessAuthServer(opts) {
112
112
  method: "GET",
113
113
  });
114
114
  const data = (await up.json());
115
- console.log(data.token);
116
- const verified = await verifySignedAuthResponse(data.token, authServerUrl);
117
- if (!verified) {
118
- throw new Error("Invalid signed response from Auth Server");
119
- }
120
- if (verified.sub !== data.sub) {
121
- throw new Error("Signature mismatch with data payload");
122
- }
123
115
  clearSessionCookie(res, cookieDomain, preAuthCookieName);
124
116
  if (!data.user)
125
117
  return res.status(401).json({ error: "unauthenticated" });
@@ -5,4 +5,4 @@ export interface AuthFetchOptions {
5
5
  cookies?: string[];
6
6
  headers?: Record<string, string>;
7
7
  }
8
- export declare function authFetch(req: CookieRequest, url: string, { method, body, cookies, headers }?: AuthFetchOptions): Promise<import("node-fetch").Response>;
8
+ export declare function authFetch(req: CookieRequest, url: string, { method, body, cookies, headers }?: AuthFetchOptions): Promise<Response>;
@@ -1,20 +1,28 @@
1
- import fetch from "node-fetch";
2
1
  import jwt from "jsonwebtoken";
3
- const privateKey = process.env.SERVICE_JWT_KEY;
2
+ const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
4
3
  export async function authFetch(req, url, { method = "POST", body, cookies, headers = {} } = {}) {
5
- const token = privateKey
6
- ? jwt.sign({ aud: "auth-internal", iss: "portal-api", sub: req.cookiePayload?.sub, role: req.cookiePayload?.roles }, privateKey, {
7
- expiresIn: "60s",
8
- keyid: "service-main",
9
- })
10
- : undefined;
11
- if (!token) {
12
- throw new Error("Cannot sign JWT for communications with Seamless Auth Server. Did you set your SERVICE_JWT_KEY?");
4
+ if (!serviceKey) {
5
+ throw new Error("Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN");
13
6
  }
7
+ // -------------------------------
8
+ // Issue short-lived machine token
9
+ // -------------------------------
10
+ const token = jwt.sign({
11
+ // Minimal, safe fields
12
+ iss: process.env.FRONTEND_URL,
13
+ aud: process.env.AUTH_SERVER,
14
+ sub: req.cookiePayload?.sub,
15
+ roles: req.cookiePayload?.roles ?? [],
16
+ iat: Math.floor(Date.now() / 1000),
17
+ }, serviceKey, {
18
+ expiresIn: "60s", // Short-lived = safer
19
+ algorithm: "HS256", // HMAC-based
20
+ keyid: "dev-main", // For future rotation
21
+ });
14
22
  const finalHeaders = {
15
23
  ...(method !== "GET" && { "Content-Type": "application/json" }),
16
24
  ...(cookies ? { Cookie: cookies.join("; ") } : {}),
17
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
25
+ Authorization: `Bearer ${token}`,
18
26
  ...headers,
19
27
  };
20
28
  let finalUrl = url;
@@ -1,6 +1,7 @@
1
1
  import { Response } from "express";
2
2
  export interface CookiePayload {
3
3
  sub: string;
4
+ token?: string;
4
5
  refreshToken?: string;
5
6
  roles?: string[];
6
7
  }
@@ -6,12 +6,12 @@ if (!COOKIE_SECRET) {
6
6
  export function setSessionCookie(res, payload, domain, ttlSeconds = 300, name = "sa_session") {
7
7
  const token = jwt.sign(payload, COOKIE_SECRET, {
8
8
  algorithm: "HS256",
9
- expiresIn: ttlSeconds,
9
+ expiresIn: `${ttlSeconds}s`,
10
10
  });
11
11
  res.cookie(name, token, {
12
12
  httpOnly: true,
13
- secure: true,
14
- sameSite: "lax",
13
+ secure: process.env.NODE_ENV === "production",
14
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
15
15
  path: "/",
16
16
  domain,
17
17
  maxAge: ttlSeconds * 1000,
@@ -1,4 +1,4 @@
1
- import type { Request } from "express";
1
+ import { CookieRequest } from "../middleware/ensureCookies.js";
2
2
  /**
3
3
  * Retrieves the Seamless Auth user information by calling the auth server's introspection endpoint.
4
4
  * Requires the sa_session (or custom) cookie to be present on the request.
@@ -7,4 +7,4 @@ import type { Request } from "express";
7
7
  * @param authServerUrl Base URL of the client's auth server
8
8
  * @returns The user data object if valid, or null if invalid/unauthenticated
9
9
  */
10
- export declare function getSeamlessUser<T = any>(req: Request, authServerUrl: string): Promise<T | null>;
10
+ export declare function getSeamlessUser<T = any>(req: CookieRequest, authServerUrl: string, cookieName?: string): Promise<T | null>;
@@ -1,5 +1,5 @@
1
1
  import { authFetch } from "./authFetch.js";
2
- import { verifySignedAuthResponse } from "./verifySignedAuthResponse.js";
2
+ import { verifyCookieJwt } from "./verifyCookieJwt.js";
3
3
  /**
4
4
  * Retrieves the Seamless Auth user information by calling the auth server's introspection endpoint.
5
5
  * Requires the sa_session (or custom) cookie to be present on the request.
@@ -8,8 +8,13 @@ import { verifySignedAuthResponse } from "./verifySignedAuthResponse.js";
8
8
  * @param authServerUrl Base URL of the client's auth server
9
9
  * @returns The user data object if valid, or null if invalid/unauthenticated
10
10
  */
11
- export async function getSeamlessUser(req, authServerUrl) {
11
+ export async function getSeamlessUser(req, authServerUrl, cookieName = "seamless-auth-access") {
12
12
  try {
13
+ const payload = verifyCookieJwt(req.cookies[cookieName]);
14
+ if (!payload) {
15
+ throw new Error("Missing cookie");
16
+ }
17
+ req.cookiePayload = payload;
13
18
  const response = await authFetch(req, `${authServerUrl}/users/me`, {
14
19
  method: "GET",
15
20
  });
@@ -18,13 +23,6 @@ export async function getSeamlessUser(req, authServerUrl) {
18
23
  return null;
19
24
  }
20
25
  const data = (await response.json());
21
- const verified = await verifySignedAuthResponse(data.token, authServerUrl);
22
- if (!verified) {
23
- throw new Error("Invalid signed response from Auth Server");
24
- }
25
- if (verified.sub !== data.sub) {
26
- throw new Error("Signature mismatch with data payload");
27
- }
28
26
  return data.user;
29
27
  }
30
28
  catch (err) {
@@ -0,0 +1,9 @@
1
+ import { CookieRequest } from "../middleware/ensureCookies.js";
2
+ export declare function refreshAccessToken(req: CookieRequest, authServerUrl: string, refreshToken: string): Promise<{
3
+ sub: string;
4
+ token: string;
5
+ refreshToken: string;
6
+ roles: string[];
7
+ ttl: number;
8
+ refreshTtl: number;
9
+ } | null>;
@@ -0,0 +1,43 @@
1
+ import jwt from "jsonwebtoken";
2
+ const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
3
+ if (!COOKIE_SECRET) {
4
+ console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
5
+ }
6
+ const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
7
+ export async function refreshAccessToken(req, authServerUrl, refreshToken) {
8
+ try {
9
+ if (!serviceKey) {
10
+ throw new Error("Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN");
11
+ }
12
+ // unwrap token with local key and rewrap with service key
13
+ const payload = jwt.verify(refreshToken, COOKIE_SECRET, {
14
+ algorithms: ["HS256"],
15
+ });
16
+ const token = jwt.sign({
17
+ // Minimal, safe fields
18
+ iss: process.env.FRONTEND_URL,
19
+ aud: process.env.AUTH_SERVER,
20
+ sub: payload.sub,
21
+ refreshToken: payload.refreshToken,
22
+ iat: Math.floor(Date.now() / 1000),
23
+ }, serviceKey, {
24
+ expiresIn: "60s", // Short-lived = safer
25
+ algorithm: "HS256", // HMAC-based
26
+ keyid: "dev-main", // For future rotation
27
+ });
28
+ const response = await fetch(`${authServerUrl}/refresh`, {
29
+ method: "GET",
30
+ headers: { Authorization: `Bearer ${token}` },
31
+ });
32
+ if (!response.ok) {
33
+ console.error("[SeamlessAuth] Refresh token request failed:", response.status);
34
+ return null;
35
+ }
36
+ const data = await response.json();
37
+ return data;
38
+ }
39
+ catch (err) {
40
+ console.error("[SeamlessAuth] refreshAccessToken error:", err);
41
+ return null;
42
+ }
43
+ }
@@ -12,6 +12,7 @@ export async function verifySignedAuthResponse(token, authServerUrl) {
12
12
  // Verify signature and algorithm
13
13
  const { payload } = await jwtVerify(token, JWKS, {
14
14
  algorithms: ["RS256"],
15
+ issuer: authServerUrl,
15
16
  });
16
17
  return payload;
17
18
  }
@@ -4,4 +4,4 @@ import { JwtPayload } from "jsonwebtoken";
4
4
  export interface CookieRequest extends Request {
5
5
  cookiePayload?: JwtPayload;
6
6
  }
7
- export declare function createEnsureCookiesMiddleware(opts: SeamlessAuthServerOptions): (req: CookieRequest, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
7
+ export declare function createEnsureCookiesMiddleware(opts: SeamlessAuthServerOptions): (req: CookieRequest, res: Response, next: NextFunction, cookieDomain?: string) => Promise<void | Response<any, Record<string, any>>>;
@@ -1,32 +1,88 @@
1
1
  import { verifyCookieJwt } from "../internal/verifyCookieJwt.js";
2
+ import { refreshAccessToken } from "../internal/refreshAccessToken";
3
+ import { clearAllCookies, setSessionCookie } from "../internal/cookie";
4
+ const AUTH_SERVER_URL = process.env.AUTH_SERVER;
5
+ const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
6
+ if (!COOKIE_SECRET) {
7
+ console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
8
+ }
2
9
  export function createEnsureCookiesMiddleware(opts) {
3
10
  const COOKIE_REQUIREMENTS = {
4
11
  "/webAuthn/login/finish": { name: opts.preAuthCookieName, required: true },
5
12
  "/webAuthn/login/start": { name: opts.preAuthCookieName, required: true },
6
- "/webAuthn/register/start": { name: opts.registrationCookieName, required: true },
7
- "/webAuthn/register/finish": { name: opts.registrationCookieName, required: true },
8
- "/otp/verify-email-otp": { name: opts.registrationCookieName, required: true },
9
- "/otp/verify-phone-otp": { name: opts.registrationCookieName, required: true },
13
+ "/webAuthn/register/start": {
14
+ name: opts.registrationCookieName,
15
+ required: true,
16
+ },
17
+ "/webAuthn/register/finish": {
18
+ name: opts.registrationCookieName,
19
+ required: true,
20
+ },
21
+ "/otp/verify-email-otp": {
22
+ name: opts.registrationCookieName,
23
+ required: true,
24
+ },
25
+ "/otp/verify-phone-otp": {
26
+ name: opts.registrationCookieName,
27
+ required: true,
28
+ },
10
29
  "/logout": { name: opts.accesscookieName, required: true },
11
30
  "/users/me": { name: opts.accesscookieName, required: true },
12
31
  };
13
- return function ensureCookies(req, res, next) {
32
+ return async function ensureCookies(req, res, next, cookieDomain = "") {
14
33
  const match = Object.entries(COOKIE_REQUIREMENTS).find(([path]) => req.path.startsWith(path));
15
34
  if (!match)
16
35
  return next();
17
36
  const [, { name, required }] = match;
18
37
  const cookieValue = req.cookies?.[name];
38
+ const refreshCookieValue = req.cookies?.[opts.refreshCookieName];
39
+ //
40
+ // --- NEW REFRESH-AWARE LOGIC ---
41
+ //
42
+ // If required cookie is missing BUT refresh cookie exists,
43
+ // allow request to proceed. requireAuth() will perform refresh.
44
+ //
19
45
  if (required && !cookieValue) {
46
+ if (refreshCookieValue) {
47
+ console.log("[SeamlessAuth] Access token expired — attempting refresh");
48
+ const refreshed = await refreshAccessToken(req, AUTH_SERVER_URL, refreshCookieValue);
49
+ if (!refreshed?.token) {
50
+ clearAllCookies(res, cookieDomain, name, opts.registrationCookieName, opts.refreshCookieName);
51
+ res.status(401).json({ error: "Refresh failed" });
52
+ return;
53
+ }
54
+ // Update cookie with new access token
55
+ setSessionCookie(res, {
56
+ sub: refreshed.sub,
57
+ token: refreshed.token,
58
+ roles: refreshed.roles,
59
+ }, cookieDomain, refreshed.ttl, name);
60
+ setSessionCookie(res, { sub: refreshed.sub, refreshToken: refreshed.refreshToken }, cookieDomain, refreshed.refreshTtl, opts.refreshCookieName);
61
+ // Let requireAuth() attempt refresh
62
+ req.cookiePayload = {
63
+ sub: refreshed.sub,
64
+ roles: refreshed.roles,
65
+ };
66
+ return next();
67
+ }
68
+ // No required cookie AND no refresh cookie → hard fail
20
69
  return res.status(400).json({
21
70
  error: `Missing required cookie "${name}" for route ${req.path}`,
22
71
  hint: "Did you forget to call /auth/login/start first?",
23
72
  });
24
73
  }
25
- const payload = verifyCookieJwt(cookieValue);
26
- if (!payload) {
27
- return res.status(401).json({ error: `Invalid or expired ${name} cookie` });
74
+ //
75
+ // If cookie exists, verify it normally
76
+ //
77
+ if (cookieValue) {
78
+ const payload = verifyCookieJwt(cookieValue);
79
+ if (!payload) {
80
+ return res
81
+ .status(401)
82
+ .json({ error: `Invalid or expired ${name} cookie` });
83
+ }
84
+ req.cookiePayload = payload;
28
85
  }
29
- req.cookiePayload = payload;
30
86
  next();
31
87
  };
32
88
  }
@@ -5,4 +5,4 @@ import { Request, Response, NextFunction } from "express";
5
5
  * - Attaches decoded payload to req.user
6
6
  * - Returns 401 if missing/invalid/expired
7
7
  */
8
- export declare function requireAuth(cookieName?: string): (req: Request, res: Response, next: NextFunction) => void;
8
+ export declare function requireAuth(cookieName?: string, refreshCookieName?: string, cookieDomain?: string): (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -1,28 +1,65 @@
1
1
  import jwt from "jsonwebtoken";
2
+ import { refreshAccessToken } from "../internal/refreshAccessToken.js";
3
+ import { setSessionCookie } from "../internal/cookie.js";
2
4
  const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
3
5
  if (!COOKIE_SECRET) {
4
6
  console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
5
7
  }
8
+ const AUTH_SERVER_URL = process.env.AUTH_SERVER;
6
9
  /**
7
10
  * Express middleware that verifies a Seamless Auth access cookie.
8
11
  * - Reads and verifies signed cookie JWT
9
12
  * - Attaches decoded payload to req.user
10
13
  * - Returns 401 if missing/invalid/expired
11
14
  */
12
- export function requireAuth(cookieName = "seamless-auth-access") {
13
- return (req, res, next) => {
15
+ export function requireAuth(cookieName = "seamless-auth-access", refreshCookieName = "seamless-auth-refresh", cookieDomain = "/") {
16
+ return async (req, res, next) => {
14
17
  try {
15
18
  const token = req.cookies?.[cookieName];
16
19
  if (!token) {
17
20
  res.status(401).json({ error: "Missing access cookie" });
18
21
  return;
19
22
  }
20
- const payload = jwt.verify(token, COOKIE_SECRET, {
21
- algorithms: ["HS256"],
22
- });
23
- // Attach decoded JWT claims to request for downstream handlers
24
- req.user = payload;
25
- next();
23
+ try {
24
+ const payload = jwt.verify(token, COOKIE_SECRET, {
25
+ algorithms: ["HS256"],
26
+ });
27
+ req.user = payload;
28
+ return next();
29
+ }
30
+ catch (err) {
31
+ // expired or invalid token
32
+ if (err.name !== "TokenExpiredError") {
33
+ console.warn("[SeamlessAuth] Invalid token:", err.message);
34
+ res.status(401).json({ error: "Invalid token" });
35
+ return;
36
+ }
37
+ // Try refresh
38
+ const refreshToken = req.cookies?.[refreshCookieName];
39
+ if (!refreshToken) {
40
+ res.status(401).json({ error: "Session expired; re-login required" });
41
+ return;
42
+ }
43
+ console.log("[SeamlessAuth] Access token expired — attempting refresh");
44
+ const refreshed = await refreshAccessToken(req, AUTH_SERVER_URL, refreshToken);
45
+ if (!refreshed?.token) {
46
+ res.status(401).json({ error: "Refresh failed" });
47
+ return;
48
+ }
49
+ // Update cookie with new access token
50
+ setSessionCookie(res, {
51
+ sub: refreshed.sub,
52
+ token: refreshed.token,
53
+ roles: refreshed.roles,
54
+ }, cookieDomain, refreshed.ttl, cookieName);
55
+ setSessionCookie(res, { sub: refreshed.sub, refreshToken: refreshed.refreshToken }, req.hostname, refreshed.refreshTtl, refreshCookieName);
56
+ // Decode new token so downstream has user
57
+ const payload = jwt.verify(refreshed.token, COOKIE_SECRET, {
58
+ algorithms: ["HS256"],
59
+ });
60
+ req.user = payload;
61
+ next();
62
+ }
26
63
  }
27
64
  catch (err) {
28
65
  console.error("[SeamlessAuth] requireAuth error:", err.message);
@@ -9,7 +9,7 @@ if (!COOKIE_SECRET) {
9
9
  * @param role Role name to require (e.g. 'admin')
10
10
  * @param cookieName Cookie name containing JWT (default: 'sa_session')
11
11
  */
12
- export function requireRole(role, cookieName = "seamless-auth-access") {
12
+ export function requireRole(role, cookieName = "seamless-access") {
13
13
  return (req, res, next) => {
14
14
  try {
15
15
  const token = req.cookies?.[cookieName];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamless-auth/express",
3
- "version": "0.0.0",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "Express adapter for Seamless Auth passwordless authentication",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/fells-code/seamless-auth-server"
16
+ "url": "git+https://github.com/fells-code/seamless-auth-server.git"
17
17
  },
18
18
  "files": [
19
19
  "dist"
@@ -26,12 +26,9 @@
26
26
  "cookie-parser": "^1.4.6",
27
27
  "jose": "^6.1.1",
28
28
  "jsonwebtoken": "^9.0.2",
29
- "node-fetch": "^3.3.2",
30
- "version-packages": "changeset version",
31
- "release": "changeset publish"
29
+ "node-fetch": "^3.3.2"
32
30
  },
33
31
  "devDependencies": {
34
- "@changesets/cli": "^2.29.7",
35
32
  "@types/cookie-parser": "^1.4.10",
36
33
  "@types/jsonwebtoken": "^9.0.10",
37
34
  "typescript": "^5.5.0"