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