@seamless-auth/express 0.0.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/README.md +13 -12
- package/dist/index.js +564 -5
- package/package.json +3 -2
- package/dist/createServer.d.ts +0 -4
- package/dist/createServer.d.ts.map +0 -1
- package/dist/createServer.js +0 -120
- 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 -41
- 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 -11
- package/dist/internal/getSeamlessUser.d.ts.map +0 -1
- package/dist/internal/getSeamlessUser.js +0 -32
- 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 -84
- package/dist/middleware/requireAuth.d.ts +0 -9
- package/dist/middleware/requireAuth.d.ts.map +0 -1
- package/dist/middleware/requireAuth.js +0 -74
- package/dist/middleware/requireRole.d.ts +0 -9
- package/dist/middleware/requireRole.d.ts.map +0 -1
- package/dist/middleware/requireRole.js +0 -37
- package/dist/types.d.ts +0 -9
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -96,13 +96,12 @@ Everything happens securely between your API and a private Seamless Auth Server.
|
|
|
96
96
|
|
|
97
97
|
## Environment Variables
|
|
98
98
|
|
|
99
|
-
| Variable | Description
|
|
100
|
-
| ----------------------------- |
|
|
101
|
-
| `AUTH_SERVER_URL` | Base URL of your Seamless Auth Server
|
|
102
|
-
| `SEAMLESS_COOKIE_SIGNING_KEY` | Secret key for signing JWT cookies
|
|
103
|
-
| `SEAMLESS_SERVICE_TOKEN` | Private key for API → Auth Server JWTs
|
|
104
|
-
| `
|
|
105
|
-
| `COOKIE_DOMAIN` | Domain for cookies | `.client.com` |
|
|
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 | `Obtained via the auth portal` |
|
|
104
|
+
| `FRONTEND_URL` | URL of your website or https://localhost:5001 | `https://mySite.com` |
|
|
106
105
|
|
|
107
106
|
---
|
|
108
107
|
|
|
@@ -188,7 +187,7 @@ User shape
|
|
|
188
187
|
→ sets short-lived pre-auth cookie.
|
|
189
188
|
|
|
190
189
|
2. **Frontend** → `/auth/webauthn/finish`
|
|
191
|
-
→ API proxies, validates, sets access cookie (`
|
|
190
|
+
→ API proxies, validates, sets access cookie (`seamless-access`).
|
|
192
191
|
|
|
193
192
|
3. **Subsequent API calls** → `/api/...`
|
|
194
193
|
→ `requireAuth()` verifies cookie and attaches user.
|
|
@@ -206,9 +205,10 @@ In order to develop with your Seamless Auth server instance, you will need to ha
|
|
|
206
205
|
Example env:
|
|
207
206
|
|
|
208
207
|
```bash
|
|
209
|
-
AUTH_SERVER_URL=
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
AUTH_SERVER_URL=https://<identifier>.seamlessauth.com # Found in the portal
|
|
209
|
+
SEAMLESS_SERVICE_TOKEN=32byte-secret # Created and rotated in the portal
|
|
210
|
+
FRONTEND_URL=https://yourSite.com # Must match the value you have for Frontend Domain in the portal (Can be localhost:5001 when application is in demo mode)
|
|
211
|
+
SEAMLESS_COOKIE_SIGNING_KEY=local-secret-key # A key you make for signing your API's distributed cookies
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
---
|
|
@@ -217,7 +217,7 @@ SEAMLESS_COOKIE_SIGNING_KEY=local-secret-key # Found in the portal
|
|
|
217
217
|
|
|
218
218
|
```ts
|
|
219
219
|
const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL!;
|
|
220
|
-
app.use(cors({ origin: "
|
|
220
|
+
app.use(cors({ origin: "https://localhost:5001", credentials: true }));
|
|
221
221
|
app.use(express.json());
|
|
222
222
|
app.use(cookieParser());
|
|
223
223
|
app.use("/auth", createSeamlessAuthServer({ authServerUrl: AUTH_SERVER_URL }));
|
|
@@ -266,3 +266,4 @@ app.get("/api/test", requireAuth(), (req, res) => res.json({ ok: true }));
|
|
|
266
266
|
|
|
267
267
|
MIT © 2025 Fells Code LLC
|
|
268
268
|
Part of the **Seamless Auth** ecosystem.
|
|
269
|
+
https://seamlessauth.com
|
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.
|
|
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 +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,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,yBAAyB,GAC9B,MAAM,CA6LR"}
|