@seamless-auth/express 0.0.1-beta.1 → 0.0.1
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 +40 -36
- package/dist/createServer.d.ts +1 -0
- package/dist/createServer.d.ts.map +1 -0
- package/dist/createServer.js +18 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -4
- package/dist/internal/authFetch.d.ts +2 -1
- package/dist/internal/authFetch.d.ts.map +1 -0
- package/dist/internal/authFetch.js +21 -11
- package/dist/internal/cookie.d.ts +2 -0
- package/dist/internal/cookie.d.ts.map +1 -0
- package/dist/internal/cookie.js +8 -7
- package/dist/internal/getSeamlessUser.d.ts +3 -2
- package/dist/internal/getSeamlessUser.d.ts.map +1 -0
- package/dist/internal/getSeamlessUser.js +7 -9
- package/dist/internal/refreshAccessToken.d.ts +10 -0
- package/dist/internal/refreshAccessToken.d.ts.map +1 -0
- package/dist/internal/refreshAccessToken.js +44 -0
- package/dist/internal/verifyCookieJwt.d.ts +1 -0
- package/dist/internal/verifyCookieJwt.d.ts.map +1 -0
- package/dist/internal/verifySignedAuthResponse.d.ts +1 -0
- package/dist/internal/verifySignedAuthResponse.d.ts.map +1 -0
- package/dist/internal/verifySignedAuthResponse.js +1 -0
- package/dist/middleware/ensureCookies.d.ts +2 -1
- package/dist/middleware/ensureCookies.d.ts.map +1 -0
- package/dist/middleware/ensureCookies.js +61 -9
- package/dist/middleware/requireAuth.d.ts +2 -1
- package/dist/middleware/requireAuth.d.ts.map +1 -0
- package/dist/middleware/requireAuth.js +53 -12
- package/dist/middleware/requireRole.d.ts +1 -0
- package/dist/middleware/requireRole.d.ts.map +1 -0
- package/dist/middleware/requireRole.js +7 -6
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@ It proxies all authentication flows, manages signed cookies, and gives you out-o
|
|
|
10
10
|
> **Docs:** https://docs.seamlessauth.com
|
|
11
11
|
> **Repo:** https://github.com/fells-code/seamless-auth-server
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
> Couple with https://github.com/fells-code/seamless-auth/react for an end to end seamless experience
|
|
15
14
|
|
|
16
15
|
> Or get a full starter application with https://github.com/fells-code/create-seamless
|
|
@@ -27,25 +26,32 @@ npm install @seamless-auth/server-express
|
|
|
27
26
|
yarn add @seamless-auth/server-express
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
|
|
31
29
|
## Quick Example
|
|
32
30
|
|
|
33
31
|
```ts
|
|
34
32
|
import express from "express";
|
|
35
33
|
import cookieParser from "cookie-parser";
|
|
36
|
-
import createSeamlessAuthServer, {
|
|
34
|
+
import createSeamlessAuthServer, {
|
|
35
|
+
requireAuth,
|
|
36
|
+
requireRole,
|
|
37
|
+
} from "@seamless-auth/server-express";
|
|
37
38
|
|
|
38
39
|
const app = express();
|
|
39
40
|
app.use(cookieParser());
|
|
40
41
|
|
|
41
42
|
// Public Seamless Auth endpoints
|
|
42
|
-
app.use(
|
|
43
|
+
app.use(
|
|
44
|
+
"/auth",
|
|
45
|
+
createSeamlessAuthServer({ authServerUrl: process.env.AUTH_SERVER_URL! })
|
|
46
|
+
);
|
|
43
47
|
|
|
44
48
|
// Everything after this line requires authentication
|
|
45
49
|
app.use(requireAuth());
|
|
46
50
|
|
|
47
51
|
app.get("/api/me", (req, res) => res.json({ user: (req as any).user }));
|
|
48
|
-
app.get("/admin", requireRole("admin"), (req, res) =>
|
|
52
|
+
app.get("/admin", requireRole("admin"), (req, res) =>
|
|
53
|
+
res.json({ message: "Welcome admin!" })
|
|
54
|
+
);
|
|
49
55
|
|
|
50
56
|
app.listen(5000, () => console.log("Portal API running on :5000"));
|
|
51
57
|
```
|
|
@@ -60,16 +66,14 @@ app.listen(5000, () => console.log("Portal API running on :5000"));
|
|
|
60
66
|
|
|
61
67
|
It transparently proxies and validates authentication flows so your frontend can use a single API endpoint for:
|
|
62
68
|
|
|
63
|
-
- Login / Registration / Logout
|
|
64
|
-
- User introspection (`/auth/me`)
|
|
65
|
-
- Session cookies (signed JWTs)
|
|
66
|
-
- Role & permission guards
|
|
69
|
+
- Login / Registration / Logout
|
|
70
|
+
- User introspection (`/auth/me`)
|
|
71
|
+
- Session cookies (signed JWTs)
|
|
72
|
+
- Role & permission guards
|
|
67
73
|
- Internal Auth Server communication (JWKS + service tokens)
|
|
68
74
|
|
|
69
75
|
Everything happens securely between your API and a private Seamless Auth Server.
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
77
|
---
|
|
74
78
|
|
|
75
79
|
## Architecture
|
|
@@ -92,13 +96,13 @@ Everything happens securely between your API and a private Seamless Auth Server.
|
|
|
92
96
|
|
|
93
97
|
## Environment Variables
|
|
94
98
|
|
|
95
|
-
| Variable
|
|
96
|
-
|
|
97
|
-
| `AUTH_SERVER_URL`
|
|
98
|
-
| `SEAMLESS_COOKIE_SIGNING_KEY` | Secret key for signing JWT cookies
|
|
99
|
-
| `
|
|
100
|
-
| `SERVICE_JWT_KEYID`
|
|
101
|
-
| `COOKIE_DOMAIN`
|
|
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` |
|
|
102
106
|
|
|
103
107
|
---
|
|
104
108
|
|
|
@@ -108,11 +112,11 @@ Everything happens securely between your API and a private Seamless Auth Server.
|
|
|
108
112
|
|
|
109
113
|
Mounts an Express router exposing the full Seamless Auth flow:
|
|
110
114
|
|
|
111
|
-
- `/auth/login/start`
|
|
112
|
-
- `/auth/login/finish`
|
|
113
|
-
- `/auth/webauthn/...`
|
|
114
|
-
- `/auth/registration/...`
|
|
115
|
-
- `/auth/me`
|
|
115
|
+
- `/auth/login/start`
|
|
116
|
+
- `/auth/login/finish`
|
|
117
|
+
- `/auth/webauthn/...`
|
|
118
|
+
- `/auth/registration/...`
|
|
119
|
+
- `/auth/me`
|
|
116
120
|
- `/auth/logout`
|
|
117
121
|
|
|
118
122
|
**Options**
|
|
@@ -167,6 +171,7 @@ const user = await getSeamlessUser(req, process.env.AUTH_SERVER_URL!);
|
|
|
167
171
|
```
|
|
168
172
|
|
|
169
173
|
User shape
|
|
174
|
+
|
|
170
175
|
```ts
|
|
171
176
|
{
|
|
172
177
|
id: string;
|
|
@@ -212,7 +217,7 @@ SEAMLESS_COOKIE_SIGNING_KEY=local-secret-key # Found in the portal
|
|
|
212
217
|
|
|
213
218
|
```ts
|
|
214
219
|
const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL!;
|
|
215
|
-
app.use(cors({ origin: "
|
|
220
|
+
app.use(cors({ origin: "http://localhost:5001", credentials: true }));
|
|
216
221
|
app.use(express.json());
|
|
217
222
|
app.use(cookieParser());
|
|
218
223
|
app.use("/auth", createSeamlessAuthServer({ authServerUrl: AUTH_SERVER_URL }));
|
|
@@ -223,17 +228,17 @@ app.use(requireAuth());
|
|
|
223
228
|
|
|
224
229
|
## Security Model
|
|
225
230
|
|
|
226
|
-
| Layer
|
|
227
|
-
|
|
228
|
-
| **Frontend ↔ API**
|
|
229
|
-
| **API ↔ Auth Server** | Bearer Service JWT (RS256)
|
|
230
|
-
| **Auth Server**
|
|
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 |
|
|
231
236
|
|
|
232
237
|
All tokens and cookies are stateless and cryptographically verifiable.
|
|
233
238
|
|
|
234
239
|
---
|
|
235
240
|
|
|
236
|
-
##
|
|
241
|
+
## Testing
|
|
237
242
|
|
|
238
243
|
You can mock `requireAuth` and test Express routes via `supertest`.
|
|
239
244
|
|
|
@@ -248,11 +253,11 @@ app.get("/api/test", requireAuth(), (req, res) => res.json({ ok: true }));
|
|
|
248
253
|
|
|
249
254
|
## Roadmap
|
|
250
255
|
|
|
251
|
-
| Feature
|
|
252
|
-
|
|
253
|
-
| JWKS-verified response signing
|
|
254
|
-
| OIDC discovery & SSO readiness
|
|
255
|
-
| Federation (Google / Okta)
|
|
256
|
+
| Feature | Status |
|
|
257
|
+
| -------------------------------------------- | ----------- |
|
|
258
|
+
| JWKS-verified response signing | ✅ |
|
|
259
|
+
| OIDC discovery & SSO readiness | planned |
|
|
260
|
+
| Federation (Google / Okta) | future |
|
|
256
261
|
| Multi-framework adapters (Next.js / Fastify) | coming soon |
|
|
257
262
|
|
|
258
263
|
---
|
|
@@ -261,4 +266,3 @@ app.get("/api/test", requireAuth(), (req, res) => res.json({ ok: true }));
|
|
|
261
266
|
|
|
262
267
|
MIT © 2025 Fells Code LLC
|
|
263
268
|
Part of the **Seamless Auth** ecosystem.
|
|
264
|
-
|
package/dist/createServer.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/dist/createServer.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import cookieParser from "cookie-parser";
|
|
3
|
-
import { setSessionCookie, clearAllCookies, clearSessionCookie, } from
|
|
4
|
-
import { authFetch } from
|
|
5
|
-
import { createEnsureCookiesMiddleware } from
|
|
6
|
-
import { verifySignedAuthResponse } from
|
|
3
|
+
import { setSessionCookie, clearAllCookies, clearSessionCookie, } from "./internal/cookie";
|
|
4
|
+
import { authFetch } from "./internal/authFetch";
|
|
5
|
+
import { createEnsureCookiesMiddleware } from "./middleware/ensureCookies";
|
|
6
|
+
import { verifySignedAuthResponse } from "./internal/verifySignedAuthResponse";
|
|
7
7
|
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-
|
|
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}`, {
|
|
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
|
-
|
|
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
|
|
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 },
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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" });
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAC5D,YAAY,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEzD,eAAe,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createSeamlessAuthServer } from
|
|
2
|
-
export { requireAuth } from
|
|
3
|
-
export { requireRole } from
|
|
4
|
-
export { getSeamlessUser } from
|
|
1
|
+
import { createSeamlessAuthServer } from "./createServer";
|
|
2
|
+
export { requireAuth } from "./middleware/requireAuth";
|
|
3
|
+
export { requireRole } from "./middleware/requireRole";
|
|
4
|
+
export { getSeamlessUser } from "./internal/getSeamlessUser";
|
|
5
5
|
export default createSeamlessAuthServer;
|
|
@@ -5,4 +5,5 @@ 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<
|
|
8
|
+
export declare function authFetch(req: CookieRequest, url: string, { method, body, cookies, headers }?: AuthFetchOptions): Promise<Response>;
|
|
9
|
+
//# sourceMappingURL=authFetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authFetch.d.ts","sourceRoot":"","sources":["../../src/internal/authFetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrD,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,EACX,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,EAAE,OAAY,EAAE,GAAE,gBAAqB,qBAuDxE"}
|
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
import fetch from "node-fetch";
|
|
2
1
|
import jwt from "jsonwebtoken";
|
|
3
|
-
const privateKey = process.env.SERVICE_JWT_KEY;
|
|
4
2
|
export async function authFetch(req, url, { method = "POST", body, cookies, headers = {} } = {}) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
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?");
|
|
3
|
+
const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
|
|
4
|
+
if (!serviceKey) {
|
|
5
|
+
throw new Error("Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN");
|
|
13
6
|
}
|
|
7
|
+
console.debug("[SeamlessAuth] Performing authentication fetch to Auth server");
|
|
8
|
+
// -------------------------------
|
|
9
|
+
// Issue short-lived machine token
|
|
10
|
+
// -------------------------------
|
|
11
|
+
const token = jwt.sign({
|
|
12
|
+
// Minimal, safe fields
|
|
13
|
+
iss: process.env.FRONTEND_URL,
|
|
14
|
+
aud: process.env.AUTH_SERVER_URL,
|
|
15
|
+
sub: req.cookiePayload?.sub,
|
|
16
|
+
roles: req.cookiePayload?.roles ?? [],
|
|
17
|
+
iat: Math.floor(Date.now() / 1000),
|
|
18
|
+
}, serviceKey, {
|
|
19
|
+
expiresIn: "60s", // Short-lived = safer
|
|
20
|
+
algorithm: "HS256", // HMAC-based
|
|
21
|
+
keyid: "dev-main", // For future rotation
|
|
22
|
+
});
|
|
14
23
|
const finalHeaders = {
|
|
15
24
|
...(method !== "GET" && { "Content-Type": "application/json" }),
|
|
16
25
|
...(cookies ? { Cookie: cookies.join("; ") } : {}),
|
|
17
|
-
|
|
26
|
+
Authorization: `Bearer ${serviceKey}`,
|
|
18
27
|
...headers,
|
|
19
28
|
};
|
|
20
29
|
let finalUrl = url;
|
|
30
|
+
console.debug("[SeamlessAuth] URL ...", finalUrl);
|
|
21
31
|
if (method === "GET" && body && typeof body === "object") {
|
|
22
32
|
const qs = new URLSearchParams(body).toString();
|
|
23
33
|
finalUrl += url.includes("?") ? `&${qs}` : `?${qs}`;
|
|
@@ -1,9 +1,11 @@
|
|
|
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
|
}
|
|
7
8
|
export declare function setSessionCookie(res: Response, payload: CookiePayload, domain?: string, ttlSeconds?: number, name?: string): void;
|
|
8
9
|
export declare function clearSessionCookie(res: Response, domain: string, name?: string): void;
|
|
9
10
|
export declare function clearAllCookies(res: Response, domain: string, accesscookieName: string, registrationCookieName: string, refreshCookieName: string): void;
|
|
11
|
+
//# sourceMappingURL=cookie.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../../src/internal/cookie.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,aAAa,EACtB,MAAM,CAAC,EAAE,MAAM,EACf,UAAU,SAAM,EAChB,IAAI,SAAe,QAqBpB;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,IAAI,SAAe,QAGpB;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,sBAAsB,EAAE,MAAM,EAC9B,iBAAiB,EAAE,MAAM,QAK1B"}
|
package/dist/internal/cookie.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
|
-
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
3
|
-
if (!COOKIE_SECRET) {
|
|
4
|
-
console.warn("[SeamlessAuth] Missing SEAMLESS_COOKIE_SIGNING_KEY env var!");
|
|
5
|
-
}
|
|
6
2
|
export function setSessionCookie(res, payload, domain, ttlSeconds = 300, name = "sa_session") {
|
|
3
|
+
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
4
|
+
if (!COOKIE_SECRET) {
|
|
5
|
+
console.warn("[SeamlessAuth] Missing SEAMLESS_COOKIE_SIGNING_KEY env var!");
|
|
6
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
7
|
+
}
|
|
7
8
|
const token = jwt.sign(payload, COOKIE_SECRET, {
|
|
8
9
|
algorithm: "HS256",
|
|
9
|
-
expiresIn: ttlSeconds
|
|
10
|
+
expiresIn: `${ttlSeconds}s`,
|
|
10
11
|
});
|
|
11
12
|
res.cookie(name, token, {
|
|
12
13
|
httpOnly: true,
|
|
13
|
-
secure:
|
|
14
|
-
sameSite: "lax",
|
|
14
|
+
secure: process.env.NODE_ENV === "production",
|
|
15
|
+
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
|
15
16
|
path: "/",
|
|
16
17
|
domain,
|
|
17
18
|
maxAge: ttlSeconds * 1000,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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,5 @@ 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:
|
|
10
|
+
export declare function getSeamlessUser<T = any>(req: CookieRequest, authServerUrl: string, cookieName?: string): Promise<T | null>;
|
|
11
|
+
//# sourceMappingURL=getSeamlessUser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getSeamlessUser.d.ts","sourceRoot":"","sources":["../../src/internal/getSeamlessUser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAG/D;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,GAAG,EAC3C,GAAG,EAAE,aAAa,EAClB,aAAa,EAAE,MAAM,EACrB,UAAU,GAAE,MAA+B,GAC1C,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAwBnB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { authFetch } from "./authFetch.js";
|
|
2
|
-
import {
|
|
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,10 @@
|
|
|
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>;
|
|
10
|
+
//# sourceMappingURL=refreshAccessToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refreshAccessToken.d.ts","sourceRoot":"","sources":["../../src/internal/refreshAccessToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAG/D,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAAC,CAwDR"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
export async function refreshAccessToken(req, authServerUrl, refreshToken) {
|
|
3
|
+
try {
|
|
4
|
+
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
5
|
+
if (!COOKIE_SECRET) {
|
|
6
|
+
console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
|
|
7
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
8
|
+
}
|
|
9
|
+
const serviceKey = process.env.SEAMLESS_SERVICE_TOKEN;
|
|
10
|
+
if (!serviceKey) {
|
|
11
|
+
throw new Error("Cannot sign service token. Missing SEAMLESS_SERVICE_TOKEN");
|
|
12
|
+
}
|
|
13
|
+
// unwrap token with local key and rewrap with service key
|
|
14
|
+
const payload = jwt.verify(refreshToken, COOKIE_SECRET, {
|
|
15
|
+
algorithms: ["HS256"],
|
|
16
|
+
});
|
|
17
|
+
const token = jwt.sign({
|
|
18
|
+
// Minimal, safe fields
|
|
19
|
+
iss: process.env.FRONTEND_URL,
|
|
20
|
+
aud: process.env.AUTH_SERVER,
|
|
21
|
+
sub: payload.sub,
|
|
22
|
+
refreshToken: payload.refreshToken,
|
|
23
|
+
iat: Math.floor(Date.now() / 1000),
|
|
24
|
+
}, serviceKey, {
|
|
25
|
+
expiresIn: "60s", // Short-lived = safer
|
|
26
|
+
algorithm: "HS256", // HMAC-based
|
|
27
|
+
keyid: "dev-main", // For future rotation
|
|
28
|
+
});
|
|
29
|
+
const response = await fetch(`${authServerUrl}/refresh`, {
|
|
30
|
+
method: "GET",
|
|
31
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
console.error("[SeamlessAuth] Refresh token request failed:", response.status);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error("[SeamlessAuth] refreshAccessToken error:", err);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyCookieJwt.d.ts","sourceRoot":"","sources":["../../src/internal/verifyCookieJwt.ts"],"names":[],"mappings":"AAIA,wBAAgB,eAAe,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAShE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifySignedAuthResponse.d.ts","sourceRoot":"","sources":["../../src/internal/verifySignedAuthResponse.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,CAAC,GAAG,GAAG,EACpD,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAmBnB"}
|
|
@@ -4,4 +4,5 @@ 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>>>;
|
|
8
|
+
//# sourceMappingURL=ensureCookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensureCookies.d.ts","sourceRoot":"","sources":["../../src/middleware/ensureCookies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1C,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,aAAa,CAAC,EAAE,UAAU,CAAC;CAC5B;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,yBAAyB,IA4BzE,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,MAAM,YAAY,EAClB,qBAAiB,wDA2FpB"}
|
|
@@ -1,32 +1,84 @@
|
|
|
1
1
|
import { verifyCookieJwt } from "../internal/verifyCookieJwt.js";
|
|
2
|
+
import { refreshAccessToken } from "../internal/refreshAccessToken";
|
|
3
|
+
import { clearAllCookies, setSessionCookie } from "../internal/cookie";
|
|
2
4
|
export function createEnsureCookiesMiddleware(opts) {
|
|
3
5
|
const COOKIE_REQUIREMENTS = {
|
|
4
6
|
"/webAuthn/login/finish": { name: opts.preAuthCookieName, required: true },
|
|
5
7
|
"/webAuthn/login/start": { name: opts.preAuthCookieName, required: true },
|
|
6
|
-
"/webAuthn/register/start": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
"/webAuthn/register/start": {
|
|
9
|
+
name: opts.registrationCookieName,
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
"/webAuthn/register/finish": {
|
|
13
|
+
name: opts.registrationCookieName,
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
"/otp/verify-email-otp": {
|
|
17
|
+
name: opts.registrationCookieName,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
"/otp/verify-phone-otp": {
|
|
21
|
+
name: opts.registrationCookieName,
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
10
24
|
"/logout": { name: opts.accesscookieName, required: true },
|
|
11
25
|
"/users/me": { name: opts.accesscookieName, required: true },
|
|
12
26
|
};
|
|
13
|
-
return function ensureCookies(req, res, next) {
|
|
27
|
+
return async function ensureCookies(req, res, next, cookieDomain = "") {
|
|
14
28
|
const match = Object.entries(COOKIE_REQUIREMENTS).find(([path]) => req.path.startsWith(path));
|
|
15
29
|
if (!match)
|
|
16
30
|
return next();
|
|
17
31
|
const [, { name, required }] = match;
|
|
32
|
+
const AUTH_SERVER_URL = process.env.AUTH_SERVER;
|
|
18
33
|
const cookieValue = req.cookies?.[name];
|
|
34
|
+
const refreshCookieValue = req.cookies?.[opts.refreshCookieName];
|
|
35
|
+
//
|
|
36
|
+
// --- NEW REFRESH-AWARE LOGIC ---
|
|
37
|
+
//
|
|
38
|
+
// If required cookie is missing BUT refresh cookie exists,
|
|
39
|
+
// allow request to proceed. requireAuth() will perform refresh.
|
|
40
|
+
//
|
|
19
41
|
if (required && !cookieValue) {
|
|
42
|
+
if (refreshCookieValue) {
|
|
43
|
+
console.log("[SeamlessAuth] Access token expired — attempting refresh");
|
|
44
|
+
const refreshed = await refreshAccessToken(req, AUTH_SERVER_URL, refreshCookieValue);
|
|
45
|
+
if (!refreshed?.token) {
|
|
46
|
+
clearAllCookies(res, cookieDomain, name, opts.registrationCookieName, opts.refreshCookieName);
|
|
47
|
+
res.status(401).json({ error: "Refresh failed" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Update cookie with new access token
|
|
51
|
+
setSessionCookie(res, {
|
|
52
|
+
sub: refreshed.sub,
|
|
53
|
+
token: refreshed.token,
|
|
54
|
+
roles: refreshed.roles,
|
|
55
|
+
}, cookieDomain, refreshed.ttl, name);
|
|
56
|
+
setSessionCookie(res, { sub: refreshed.sub, refreshToken: refreshed.refreshToken }, cookieDomain, refreshed.refreshTtl, opts.refreshCookieName);
|
|
57
|
+
// Let requireAuth() attempt refresh
|
|
58
|
+
req.cookiePayload = {
|
|
59
|
+
sub: refreshed.sub,
|
|
60
|
+
roles: refreshed.roles,
|
|
61
|
+
};
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
// No required cookie AND no refresh cookie → hard fail
|
|
20
65
|
return res.status(400).json({
|
|
21
66
|
error: `Missing required cookie "${name}" for route ${req.path}`,
|
|
22
67
|
hint: "Did you forget to call /auth/login/start first?",
|
|
23
68
|
});
|
|
24
69
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
70
|
+
//
|
|
71
|
+
// If cookie exists, verify it normally
|
|
72
|
+
//
|
|
73
|
+
if (cookieValue) {
|
|
74
|
+
const payload = verifyCookieJwt(cookieValue);
|
|
75
|
+
if (!payload) {
|
|
76
|
+
return res
|
|
77
|
+
.status(401)
|
|
78
|
+
.json({ error: `Invalid or expired ${name} cookie` });
|
|
79
|
+
}
|
|
80
|
+
req.cookiePayload = payload;
|
|
28
81
|
}
|
|
29
|
-
req.cookiePayload = payload;
|
|
30
82
|
next();
|
|
31
83
|
};
|
|
32
84
|
}
|
|
@@ -5,4 +5,5 @@ 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>;
|
|
9
|
+
//# sourceMappingURL=requireAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requireAuth.d.ts","sourceRoot":"","sources":["../../src/middleware/requireAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM1D;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,UAAU,SAAyB,EACnC,iBAAiB,SAA0B,EAC3C,YAAY,SAAM,IAahB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CAAC,CA+EjB"}
|
|
@@ -1,28 +1,69 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
|
|
5
|
-
}
|
|
2
|
+
import { refreshAccessToken } from "../internal/refreshAccessToken.js";
|
|
3
|
+
import { setSessionCookie } from "../internal/cookie.js";
|
|
6
4
|
/**
|
|
7
5
|
* Express middleware that verifies a Seamless Auth access cookie.
|
|
8
6
|
* - Reads and verifies signed cookie JWT
|
|
9
7
|
* - Attaches decoded payload to req.user
|
|
10
8
|
* - Returns 401 if missing/invalid/expired
|
|
11
9
|
*/
|
|
12
|
-
export function requireAuth(cookieName = "seamless-auth-access") {
|
|
13
|
-
|
|
10
|
+
export function requireAuth(cookieName = "seamless-auth-access", refreshCookieName = "seamless-auth-refresh", cookieDomain = "/") {
|
|
11
|
+
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
12
|
+
if (!COOKIE_SECRET) {
|
|
13
|
+
console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireAuth will always fail.");
|
|
14
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
15
|
+
}
|
|
16
|
+
const AUTH_SERVER_URL = process.env.AUTH_SERVER;
|
|
17
|
+
return async (req, res, next) => {
|
|
14
18
|
try {
|
|
19
|
+
if (!COOKIE_SECRET) {
|
|
20
|
+
throw new Error("Missing required SEAMLESS_COOKIE_SIGNING_KEY env");
|
|
21
|
+
}
|
|
15
22
|
const token = req.cookies?.[cookieName];
|
|
16
23
|
if (!token) {
|
|
17
24
|
res.status(401).json({ error: "Missing access cookie" });
|
|
18
25
|
return;
|
|
19
26
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
try {
|
|
28
|
+
const payload = jwt.verify(token, COOKIE_SECRET, {
|
|
29
|
+
algorithms: ["HS256"],
|
|
30
|
+
});
|
|
31
|
+
req.user = payload;
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// expired or invalid token
|
|
36
|
+
if (err.name !== "TokenExpiredError") {
|
|
37
|
+
console.warn("[SeamlessAuth] Invalid token:", err.message);
|
|
38
|
+
res.status(401).json({ error: "Invalid token" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Try refresh
|
|
42
|
+
const refreshToken = req.cookies?.[refreshCookieName];
|
|
43
|
+
if (!refreshToken) {
|
|
44
|
+
res.status(401).json({ error: "Session expired; re-login required" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log("[SeamlessAuth] Access token expired — attempting refresh");
|
|
48
|
+
const refreshed = await refreshAccessToken(req, AUTH_SERVER_URL, refreshToken);
|
|
49
|
+
if (!refreshed?.token) {
|
|
50
|
+
res.status(401).json({ error: "Refresh failed" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Update cookie with new access token
|
|
54
|
+
setSessionCookie(res, {
|
|
55
|
+
sub: refreshed.sub,
|
|
56
|
+
token: refreshed.token,
|
|
57
|
+
roles: refreshed.roles,
|
|
58
|
+
}, cookieDomain, refreshed.ttl, cookieName);
|
|
59
|
+
setSessionCookie(res, { sub: refreshed.sub, refreshToken: refreshed.refreshToken }, req.hostname, refreshed.refreshTtl, refreshCookieName);
|
|
60
|
+
// Decode new token so downstream has user
|
|
61
|
+
const payload = jwt.verify(refreshed.token, COOKIE_SECRET, {
|
|
62
|
+
algorithms: ["HS256"],
|
|
63
|
+
});
|
|
64
|
+
req.user = payload;
|
|
65
|
+
next();
|
|
66
|
+
}
|
|
26
67
|
}
|
|
27
68
|
catch (err) {
|
|
28
69
|
console.error("[SeamlessAuth] requireAuth error:", err.message);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requireRole.d.ts","sourceRoot":"","sources":["../../src/middleware/requireRole.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAG1E;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,UAAU,SAAoB,GAC7B,cAAc,CAiChB"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
|
-
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
3
|
-
if (!COOKIE_SECRET) {
|
|
4
|
-
console.warn("[PortalAPI] Missing SEAMLESS_COOKIE_SIGNING_KEY — role checks will fail.");
|
|
5
|
-
}
|
|
6
2
|
/**
|
|
7
3
|
* Express middleware to enforce a required role from Seamless Auth cookie JWT.
|
|
8
4
|
*
|
|
9
5
|
* @param role Role name to require (e.g. 'admin')
|
|
10
6
|
* @param cookieName Cookie name containing JWT (default: 'sa_session')
|
|
11
7
|
*/
|
|
12
|
-
export function requireRole(role, cookieName = "seamless-
|
|
8
|
+
export function requireRole(role, cookieName = "seamless-access") {
|
|
13
9
|
return (req, res, next) => {
|
|
14
10
|
try {
|
|
11
|
+
const COOKIE_SECRET = process.env.SEAMLESS_COOKIE_SIGNING_KEY;
|
|
12
|
+
if (!COOKIE_SECRET) {
|
|
13
|
+
console.warn("[SeamlessAuth] SEAMLESS_COOKIE_SIGNING_KEY missing — requireRole will always fail.");
|
|
14
|
+
throw new Error("Missing required env SEAMLESS_COOKIE_SIGNING_KEY");
|
|
15
|
+
}
|
|
15
16
|
const token = req.cookies?.[cookieName];
|
|
16
17
|
if (!token) {
|
|
17
18
|
res.status(401).json({ error: "Missing access cookie" });
|
|
@@ -29,7 +30,7 @@ export function requireRole(role, cookieName = "seamless-auth-access") {
|
|
|
29
30
|
next();
|
|
30
31
|
}
|
|
31
32
|
catch (err) {
|
|
32
|
-
console.error(`[
|
|
33
|
+
console.error(`[RequireRole] requireRole(${role}) failed:`, err.message);
|
|
33
34
|
res.status(401).json({ error: "Invalid or expired access cookie" });
|
|
34
35
|
}
|
|
35
36
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamless-auth/express",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
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"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"cookie-parser": "^1.4.6",
|
|
27
|
+
"dotenv": "^17.2.3",
|
|
27
28
|
"jose": "^6.1.1",
|
|
28
29
|
"jsonwebtoken": "^9.0.2",
|
|
29
30
|
"node-fetch": "^3.3.2"
|