@spfn/auth 0.2.1 → 0.3.0-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/LICENSE +1 -1
- package/README.md +1091 -2385
- package/dist/authenticate-55LeXHqZ.d.ts +1447 -0
- package/dist/client-proof.d.ts +606 -0
- package/dist/client-proof.js +1842 -0
- package/dist/client-proof.js.map +1 -0
- package/dist/config.d.ts +319 -3
- package/dist/config.js +155 -5
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +61 -0
- package/dist/crypto.js +108 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +180 -3
- package/dist/errors.js +116 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +166 -18
- package/dist/index.js +132 -8
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +404 -96
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +5 -4
- package/dist/nextjs/server.js +165 -26
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +2356 -1053
- package/dist/server.js +5153 -736
- package/dist/server.js.map +1 -1
- package/dist/session-DTHahDQ9.d.ts +53 -0
- package/dist/types-DYyhze28.d.ts +98 -0
- package/dist/wire-version-CtzMKvBB.d.ts +134 -0
- package/migrations/20251125021229_premium_famine/snapshot.json +2641 -0
- package/migrations/20260225130050_smooth_the_fury/snapshot.json +2686 -0
- package/migrations/20260308141417_deep_iceman/snapshot.json +2686 -0
- package/migrations/20260308151309_perfect_deathbird/snapshot.json +2731 -0
- package/migrations/20260308201135_concerned_rawhide_kid/snapshot.json +2786 -0
- package/migrations/20260629103209_lethal_lifeguard/migration.sql +32 -0
- package/migrations/20260629103209_lethal_lifeguard/snapshot.json +2786 -0
- package/migrations/20260709073531_easy_hardball/migration.sql +24 -0
- package/migrations/20260709073531_easy_hardball/snapshot.json +3119 -0
- package/migrations/20260714081434_glossy_major_mapleleaf/migration.sql +1 -0
- package/migrations/20260714081434_glossy_major_mapleleaf/snapshot.json +3112 -0
- package/migrations/20260804105939_amazing_bushwacker/migration.sql +3 -0
- package/migrations/20260804105939_amazing_bushwacker/snapshot.json +3112 -0
- package/migrations/20260804110033_fat_piledriver/migration.sql +2 -0
- package/migrations/20260804110033_fat_piledriver/snapshot.json +3138 -0
- package/migrations/20260805143152_vengeful_ravenous/migration.sql +4 -0
- package/migrations/20260805143152_vengeful_ravenous/snapshot.json +3190 -0
- package/migrations/20260807145911_mixed_invisible_woman/migration.sql +11 -0
- package/migrations/20260807145911_mixed_invisible_woman/snapshot.json +3334 -0
- package/package.json +59 -40
- package/dist/authenticate-eucncHxN.d.ts +0 -940
- package/migrations/meta/0000_snapshot.json +0 -1632
- package/migrations/meta/0001_snapshot.json +0 -1660
- package/migrations/meta/0002_snapshot.json +0 -1660
- package/migrations/meta/0003_snapshot.json +0 -1689
- package/migrations/meta/0004_snapshot.json +0 -1721
- package/migrations/meta/_journal.json +0 -41
- /package/migrations/{0000_premium_famine.sql → 20251125021229_premium_famine/migration.sql} +0 -0
- /package/migrations/{0001_smooth_the_fury.sql → 20260225130050_smooth_the_fury/migration.sql} +0 -0
- /package/migrations/{0002_deep_iceman.sql → 20260308141417_deep_iceman/migration.sql} +0 -0
- /package/migrations/{0003_perfect_deathbird.sql → 20260308151309_perfect_deathbird/migration.sql} +0 -0
- /package/migrations/{0004_concerned_rawhide_kid.sql → 20260308201135_concerned_rawhide_kid/migration.sql} +0 -0
package/dist/nextjs/api.js
CHANGED
|
@@ -1,8 +1,257 @@
|
|
|
1
1
|
// src/nextjs/api.ts
|
|
2
2
|
import { registerInterceptors } from "@spfn/core/nextjs/server";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import
|
|
4
|
+
// src/server/lib/crypto.ts
|
|
5
|
+
import crypto2 from "crypto";
|
|
6
|
+
import jwt from "jsonwebtoken";
|
|
7
|
+
function generateKeyPairES256() {
|
|
8
|
+
const keyId = crypto2.randomUUID();
|
|
9
|
+
const { privateKey, publicKey } = crypto2.generateKeyPairSync("ec", {
|
|
10
|
+
namedCurve: "P-256",
|
|
11
|
+
// ES256
|
|
12
|
+
publicKeyEncoding: {
|
|
13
|
+
type: "spki",
|
|
14
|
+
format: "der"
|
|
15
|
+
},
|
|
16
|
+
privateKeyEncoding: {
|
|
17
|
+
type: "pkcs8",
|
|
18
|
+
format: "der"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const privateKeyB64 = privateKey.toString("base64");
|
|
22
|
+
const publicKeyB64 = publicKey.toString("base64");
|
|
23
|
+
const fingerprint = crypto2.createHash("sha256").update(publicKey).digest("hex");
|
|
24
|
+
return {
|
|
25
|
+
privateKey: privateKeyB64,
|
|
26
|
+
publicKey: publicKeyB64,
|
|
27
|
+
keyId,
|
|
28
|
+
fingerprint,
|
|
29
|
+
algorithm: "ES256"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function generateKeyPairRS256() {
|
|
33
|
+
const keyId = crypto2.randomUUID();
|
|
34
|
+
const { privateKey, publicKey } = crypto2.generateKeyPairSync("rsa", {
|
|
35
|
+
modulusLength: 2048,
|
|
36
|
+
publicKeyEncoding: {
|
|
37
|
+
type: "spki",
|
|
38
|
+
format: "der"
|
|
39
|
+
},
|
|
40
|
+
privateKeyEncoding: {
|
|
41
|
+
type: "pkcs8",
|
|
42
|
+
format: "der"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const privateKeyB64 = privateKey.toString("base64");
|
|
46
|
+
const publicKeyB64 = publicKey.toString("base64");
|
|
47
|
+
const fingerprint = crypto2.createHash("sha256").update(publicKey).digest("hex");
|
|
48
|
+
return {
|
|
49
|
+
privateKey: privateKeyB64,
|
|
50
|
+
publicKey: publicKeyB64,
|
|
51
|
+
keyId,
|
|
52
|
+
fingerprint,
|
|
53
|
+
algorithm: "RS256"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function generateKeyPair(algorithm = "ES256") {
|
|
57
|
+
return algorithm === "ES256" ? generateKeyPairES256() : generateKeyPairRS256();
|
|
58
|
+
}
|
|
59
|
+
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
60
|
+
try {
|
|
61
|
+
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
62
|
+
const privateKeyObject = crypto2.createPrivateKey({
|
|
63
|
+
key: privateKeyDER,
|
|
64
|
+
format: "der",
|
|
65
|
+
type: "pkcs8"
|
|
66
|
+
});
|
|
67
|
+
const privateKeyPEM = privateKeyObject.export({
|
|
68
|
+
type: "pkcs8",
|
|
69
|
+
format: "pem"
|
|
70
|
+
});
|
|
71
|
+
const signOptions = {
|
|
72
|
+
algorithm,
|
|
73
|
+
issuer: options?.issuer || "spfn-client",
|
|
74
|
+
expiresIn: options?.expiresIn ?? "15m"
|
|
75
|
+
// Default to 15 minutes
|
|
76
|
+
};
|
|
77
|
+
return jwt.sign(payload, privateKeyPEM, signOptions);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Failed to generate client token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/server/lib/session.ts
|
|
86
|
+
import * as jose from "jose";
|
|
87
|
+
import { env } from "@spfn/auth/config";
|
|
88
|
+
import { env as coreEnv } from "@spfn/core/config";
|
|
89
|
+
|
|
90
|
+
// src/server/logger.ts
|
|
91
|
+
import { logger as rootLogger } from "@spfn/core/logger";
|
|
92
|
+
var authLogger = {
|
|
93
|
+
plugin: rootLogger.child("@spfn/auth:plugin"),
|
|
94
|
+
middleware: rootLogger.child("@spfn/auth:middleware"),
|
|
95
|
+
interceptor: {
|
|
96
|
+
general: rootLogger.child("@spfn/auth:interceptor:general"),
|
|
97
|
+
login: rootLogger.child("@spfn/auth:interceptor:login"),
|
|
98
|
+
keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
|
|
99
|
+
oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
|
|
100
|
+
},
|
|
101
|
+
session: rootLogger.child("@spfn/auth:session"),
|
|
102
|
+
service: rootLogger.child("@spfn/auth:service"),
|
|
103
|
+
setup: rootLogger.child("@spfn/auth:setup"),
|
|
104
|
+
email: rootLogger.child("@spfn/auth:email"),
|
|
105
|
+
sms: rootLogger.child("@spfn/auth:sms")
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/server/lib/session.ts
|
|
109
|
+
async function getSessionSecretKey() {
|
|
110
|
+
const secret = env.SPFN_AUTH_SESSION_SECRET;
|
|
111
|
+
const encoder = new TextEncoder();
|
|
112
|
+
const data = encoder.encode(secret);
|
|
113
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
114
|
+
return new Uint8Array(hashBuffer);
|
|
115
|
+
}
|
|
116
|
+
async function getSecretFingerprint() {
|
|
117
|
+
const key = await getSessionSecretKey();
|
|
118
|
+
const hash = await crypto.subtle.digest("SHA-256", key.buffer);
|
|
119
|
+
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
120
|
+
return hex.slice(0, 8);
|
|
121
|
+
}
|
|
122
|
+
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
123
|
+
const secret = await getSessionSecretKey();
|
|
124
|
+
const result = await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
|
|
125
|
+
if (coreEnv.NODE_ENV !== "production") {
|
|
126
|
+
const fingerprint = await getSecretFingerprint();
|
|
127
|
+
authLogger.session.debug(`Sealed session`, {
|
|
128
|
+
secretFingerprint: fingerprint,
|
|
129
|
+
resultLength: result.length,
|
|
130
|
+
resultPrefix: result.slice(0, 20)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
async function unsealSession(jwt2) {
|
|
136
|
+
try {
|
|
137
|
+
const secret = await getSessionSecretKey();
|
|
138
|
+
const { payload } = await jose.jwtDecrypt(jwt2, secret, {
|
|
139
|
+
issuer: "spfn-auth",
|
|
140
|
+
audience: "spfn-client"
|
|
141
|
+
});
|
|
142
|
+
return payload.data;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (err instanceof jose.errors.JWTExpired) {
|
|
145
|
+
throw new Error("Session expired");
|
|
146
|
+
}
|
|
147
|
+
if (err instanceof jose.errors.JWEDecryptionFailed) {
|
|
148
|
+
if (coreEnv.NODE_ENV !== "production") {
|
|
149
|
+
const fingerprint = await getSecretFingerprint();
|
|
150
|
+
authLogger.session.warn(`JWE decryption failed`, {
|
|
151
|
+
secretFingerprint: fingerprint,
|
|
152
|
+
jwtLength: jwt2.length,
|
|
153
|
+
jwtPrefix: jwt2.slice(0, 20),
|
|
154
|
+
jwtSuffix: jwt2.slice(-10)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw new Error("Invalid session");
|
|
158
|
+
}
|
|
159
|
+
if (err instanceof jose.errors.JWTClaimValidationFailed) {
|
|
160
|
+
throw new Error("Session validation failed");
|
|
161
|
+
}
|
|
162
|
+
throw new Error("Failed to unseal session");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function getSessionInfo(jwt2) {
|
|
166
|
+
const secret = await getSessionSecretKey();
|
|
167
|
+
try {
|
|
168
|
+
const { payload } = await jose.jwtDecrypt(jwt2, secret);
|
|
169
|
+
return {
|
|
170
|
+
issuedAt: new Date(payload.iat * 1e3),
|
|
171
|
+
expiresAt: new Date(payload.exp * 1e3),
|
|
172
|
+
issuer: payload.iss || "",
|
|
173
|
+
audience: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud || ""
|
|
174
|
+
};
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (coreEnv.NODE_ENV !== "production") {
|
|
177
|
+
authLogger.session.warn("Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function shouldRefreshSession(jwt2, thresholdHours = 24) {
|
|
183
|
+
const info = await getSessionInfo(jwt2);
|
|
184
|
+
if (!info) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const hoursRemaining = (info.expiresAt.getTime() - Date.now()) / (1e3 * 60 * 60);
|
|
188
|
+
return hoursRemaining < thresholdHours;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/server/lib/config.ts
|
|
192
|
+
import { env as env2 } from "@spfn/auth/config";
|
|
193
|
+
function getCookieSuffix() {
|
|
194
|
+
const port = process.env.PORT;
|
|
195
|
+
return port ? `_${port}` : "";
|
|
196
|
+
}
|
|
197
|
+
var COOKIE_NAMES = {
|
|
198
|
+
/** Encrypted session data (userId, privateKey, keyId, algorithm) */
|
|
199
|
+
get SESSION() {
|
|
200
|
+
return `spfn_session${getCookieSuffix()}`;
|
|
201
|
+
},
|
|
202
|
+
/** Current key ID (for key rotation) */
|
|
203
|
+
get SESSION_KEY_ID() {
|
|
204
|
+
return `spfn_session_key_id${getCookieSuffix()}`;
|
|
205
|
+
},
|
|
206
|
+
/** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
|
|
207
|
+
get OAUTH_PENDING() {
|
|
208
|
+
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
209
|
+
},
|
|
210
|
+
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
211
|
+
get OAUTH_CSRF() {
|
|
212
|
+
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function parseDuration(duration) {
|
|
216
|
+
if (typeof duration === "number") {
|
|
217
|
+
return duration;
|
|
218
|
+
}
|
|
219
|
+
const match = duration.match(/^(\d+)([dhms]?)$/);
|
|
220
|
+
if (!match) {
|
|
221
|
+
throw new Error(`Invalid duration format: ${duration}. Use format like '30d', '12h', '45m', '3600s', or plain number.`);
|
|
222
|
+
}
|
|
223
|
+
const value = parseInt(match[1], 10);
|
|
224
|
+
const unit = match[2] || "s";
|
|
225
|
+
switch (unit) {
|
|
226
|
+
case "d":
|
|
227
|
+
return value * 24 * 60 * 60;
|
|
228
|
+
case "h":
|
|
229
|
+
return value * 60 * 60;
|
|
230
|
+
case "m":
|
|
231
|
+
return value * 60;
|
|
232
|
+
case "s":
|
|
233
|
+
return value;
|
|
234
|
+
default:
|
|
235
|
+
throw new Error(`Unknown duration unit: ${unit}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
var globalConfig = {
|
|
239
|
+
sessionTtl: "7d"
|
|
240
|
+
// Default: 7 days
|
|
241
|
+
};
|
|
242
|
+
function getSessionTtl(override) {
|
|
243
|
+
if (override !== void 0) {
|
|
244
|
+
return parseDuration(override);
|
|
245
|
+
}
|
|
246
|
+
if (globalConfig.sessionTtl !== void 0) {
|
|
247
|
+
return parseDuration(globalConfig.sessionTtl);
|
|
248
|
+
}
|
|
249
|
+
const envTtl = env2.SPFN_AUTH_SESSION_TTL;
|
|
250
|
+
if (envTtl) {
|
|
251
|
+
return parseDuration(envTtl);
|
|
252
|
+
}
|
|
253
|
+
return 7 * 24 * 60 * 60;
|
|
254
|
+
}
|
|
6
255
|
|
|
7
256
|
// src/nextjs/interceptors/cookie-options.ts
|
|
8
257
|
function resolveSecure() {
|
|
@@ -16,7 +265,7 @@ var cookieSecure = resolveSecure();
|
|
|
16
265
|
|
|
17
266
|
// src/nextjs/interceptors/login-register.ts
|
|
18
267
|
var loginRegisterInterceptor = {
|
|
19
|
-
pathPattern: /^\/_auth\/(login|register)$/,
|
|
268
|
+
pathPattern: /^\/_auth\/(login|register|invitations\/accept)$/,
|
|
20
269
|
method: "POST",
|
|
21
270
|
request: async (ctx, next) => {
|
|
22
271
|
const oldKeyId = ctx.cookies.get(COOKIE_NAMES.SESSION_KEY_ID);
|
|
@@ -66,7 +315,7 @@ var loginRegisterInterceptor = {
|
|
|
66
315
|
options: {
|
|
67
316
|
httpOnly: true,
|
|
68
317
|
secure: cookieSecure,
|
|
69
|
-
sameSite: "
|
|
318
|
+
sameSite: "lax",
|
|
70
319
|
maxAge: ttl,
|
|
71
320
|
path: "/"
|
|
72
321
|
}
|
|
@@ -77,7 +326,7 @@ var loginRegisterInterceptor = {
|
|
|
77
326
|
options: {
|
|
78
327
|
httpOnly: true,
|
|
79
328
|
secure: cookieSecure,
|
|
80
|
-
sameSite: "
|
|
329
|
+
sameSite: "lax",
|
|
81
330
|
maxAge: ttl,
|
|
82
331
|
path: "/"
|
|
83
332
|
}
|
|
@@ -91,7 +340,6 @@ var loginRegisterInterceptor = {
|
|
|
91
340
|
};
|
|
92
341
|
|
|
93
342
|
// src/nextjs/interceptors/general-auth.ts
|
|
94
|
-
import { unsealSession, sealSession as sealSession2, shouldRefreshSession, generateClientToken, getSessionTtl as getSessionTtl2, COOKIE_NAMES as COOKIE_NAMES2, authLogger as authLogger2 } from "@spfn/auth/server";
|
|
95
343
|
function requiresAuth(path) {
|
|
96
344
|
const publicPaths = [
|
|
97
345
|
/^\/_auth\/login$/,
|
|
@@ -111,18 +359,18 @@ var generalAuthInterceptor = {
|
|
|
111
359
|
method: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
112
360
|
request: async (ctx, next) => {
|
|
113
361
|
if (!requiresAuth(ctx.path)) {
|
|
114
|
-
|
|
362
|
+
authLogger.interceptor.general.debug(`Public path, skipping auth: ${ctx.path}`);
|
|
115
363
|
await next();
|
|
116
364
|
return;
|
|
117
365
|
}
|
|
118
366
|
const cookieNames = Array.from(ctx.cookies.keys());
|
|
119
|
-
|
|
367
|
+
authLogger.interceptor.general.debug("Available cookies:", {
|
|
120
368
|
cookieNames,
|
|
121
369
|
totalCount: cookieNames.length,
|
|
122
|
-
lookingFor:
|
|
370
|
+
lookingFor: COOKIE_NAMES.SESSION
|
|
123
371
|
});
|
|
124
|
-
const sessionCookie = ctx.cookies.get(
|
|
125
|
-
|
|
372
|
+
const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);
|
|
373
|
+
authLogger.interceptor.general.debug("Request", {
|
|
126
374
|
method: ctx.method,
|
|
127
375
|
path: ctx.path,
|
|
128
376
|
hasSession: !!sessionCookie,
|
|
@@ -131,19 +379,19 @@ var generalAuthInterceptor = {
|
|
|
131
379
|
sessionSuffix: sessionCookie?.slice(-10) ?? ""
|
|
132
380
|
});
|
|
133
381
|
if (!sessionCookie) {
|
|
134
|
-
|
|
382
|
+
authLogger.interceptor.general.debug("No session cookie, proceeding without auth");
|
|
135
383
|
await next();
|
|
136
384
|
return;
|
|
137
385
|
}
|
|
138
386
|
try {
|
|
139
387
|
const session = await unsealSession(sessionCookie);
|
|
140
|
-
|
|
388
|
+
authLogger.interceptor.general.debug("Session valid", {
|
|
141
389
|
userId: session.userId,
|
|
142
390
|
keyId: session.keyId
|
|
143
391
|
});
|
|
144
392
|
const needsRefresh = await shouldRefreshSession(sessionCookie, 24);
|
|
145
393
|
if (needsRefresh) {
|
|
146
|
-
|
|
394
|
+
authLogger.interceptor.general.debug("Session needs refresh (within 24h of expiry)");
|
|
147
395
|
ctx.metadata.refreshSession = true;
|
|
148
396
|
ctx.metadata.sessionData = session;
|
|
149
397
|
}
|
|
@@ -157,7 +405,7 @@ var generalAuthInterceptor = {
|
|
|
157
405
|
session.algorithm,
|
|
158
406
|
{ expiresIn: "15m" }
|
|
159
407
|
);
|
|
160
|
-
|
|
408
|
+
authLogger.interceptor.general.debug("Generated JWT token (expires in 15m)");
|
|
161
409
|
ctx.headers["Authorization"] = `Bearer ${token}`;
|
|
162
410
|
ctx.headers["X-Key-Id"] = session.keyId;
|
|
163
411
|
ctx.metadata.userId = session.userId;
|
|
@@ -166,31 +414,31 @@ var generalAuthInterceptor = {
|
|
|
166
414
|
const err = error;
|
|
167
415
|
const msg = err.message.toLowerCase();
|
|
168
416
|
if (msg.includes("expired") || msg.includes("invalid")) {
|
|
169
|
-
|
|
417
|
+
authLogger.interceptor.general.warn("Session expired or invalid", {
|
|
170
418
|
message: err.message,
|
|
171
419
|
cookieLength: sessionCookie.length,
|
|
172
420
|
cookiePrefix: sessionCookie.slice(0, 20),
|
|
173
421
|
cookieSuffix: sessionCookie.slice(-10)
|
|
174
422
|
});
|
|
175
|
-
|
|
423
|
+
authLogger.interceptor.general.debug("Marking session for cleanup");
|
|
176
424
|
ctx.metadata.clearSession = true;
|
|
177
425
|
ctx.metadata.sessionValid = false;
|
|
178
426
|
} else {
|
|
179
|
-
|
|
427
|
+
authLogger.interceptor.general.error("Failed to process session", err);
|
|
180
428
|
}
|
|
181
429
|
}
|
|
182
430
|
await next();
|
|
183
431
|
},
|
|
184
432
|
response: async (ctx, next) => {
|
|
185
433
|
if (ctx.response.status === 401 && ctx.metadata.sessionValid) {
|
|
186
|
-
|
|
434
|
+
authLogger.interceptor.general.warn("Backend returned 401, clearing session");
|
|
187
435
|
ctx.setCookies.push({
|
|
188
|
-
name:
|
|
436
|
+
name: COOKIE_NAMES.SESSION,
|
|
189
437
|
value: "",
|
|
190
438
|
options: { maxAge: 0, path: "/" }
|
|
191
439
|
});
|
|
192
440
|
ctx.setCookies.push({
|
|
193
|
-
name:
|
|
441
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
194
442
|
value: "",
|
|
195
443
|
options: { maxAge: 0, path: "/" }
|
|
196
444
|
});
|
|
@@ -199,7 +447,7 @@ var generalAuthInterceptor = {
|
|
|
199
447
|
}
|
|
200
448
|
if (ctx.metadata.clearSession) {
|
|
201
449
|
ctx.setCookies.push({
|
|
202
|
-
name:
|
|
450
|
+
name: COOKIE_NAMES.SESSION,
|
|
203
451
|
value: "",
|
|
204
452
|
options: {
|
|
205
453
|
maxAge: 0,
|
|
@@ -207,7 +455,7 @@ var generalAuthInterceptor = {
|
|
|
207
455
|
}
|
|
208
456
|
});
|
|
209
457
|
ctx.setCookies.push({
|
|
210
|
-
name:
|
|
458
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
211
459
|
value: "",
|
|
212
460
|
options: {
|
|
213
461
|
maxAge: 0,
|
|
@@ -217,55 +465,60 @@ var generalAuthInterceptor = {
|
|
|
217
465
|
} else if (ctx.metadata.refreshSession && ctx.response.status === 200) {
|
|
218
466
|
try {
|
|
219
467
|
const sessionData = ctx.metadata.sessionData;
|
|
220
|
-
const ttl =
|
|
221
|
-
const sealed = await
|
|
468
|
+
const ttl = getSessionTtl();
|
|
469
|
+
const sealed = await sealSession(sessionData, ttl);
|
|
222
470
|
ctx.setCookies.push({
|
|
223
|
-
name:
|
|
471
|
+
name: COOKIE_NAMES.SESSION,
|
|
224
472
|
value: sealed,
|
|
225
473
|
options: {
|
|
226
474
|
httpOnly: true,
|
|
227
475
|
secure: cookieSecure,
|
|
228
|
-
sameSite: "
|
|
476
|
+
sameSite: "lax",
|
|
229
477
|
maxAge: ttl,
|
|
230
478
|
path: "/"
|
|
231
479
|
}
|
|
232
480
|
});
|
|
233
481
|
ctx.setCookies.push({
|
|
234
|
-
name:
|
|
482
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
235
483
|
value: sessionData.keyId,
|
|
236
484
|
options: {
|
|
237
485
|
httpOnly: true,
|
|
238
486
|
secure: cookieSecure,
|
|
239
|
-
sameSite: "
|
|
487
|
+
sameSite: "lax",
|
|
240
488
|
maxAge: ttl,
|
|
241
489
|
path: "/"
|
|
242
490
|
}
|
|
243
491
|
});
|
|
244
|
-
|
|
492
|
+
authLogger.interceptor.general.info("Session refreshed", {
|
|
245
493
|
userId: sessionData.userId,
|
|
246
494
|
sealedLength: sealed.length,
|
|
247
495
|
sealedPrefix: sealed.slice(0, 20)
|
|
248
496
|
});
|
|
249
497
|
} catch (error) {
|
|
250
498
|
const err = error;
|
|
251
|
-
|
|
499
|
+
authLogger.interceptor.general.error("Failed to refresh session", err);
|
|
252
500
|
}
|
|
253
501
|
} else if (ctx.path === "/_auth/logout" && ctx.response.ok) {
|
|
502
|
+
const base = {
|
|
503
|
+
httpOnly: true,
|
|
504
|
+
secure: cookieSecure,
|
|
505
|
+
maxAge: 0,
|
|
506
|
+
path: "/"
|
|
507
|
+
};
|
|
254
508
|
ctx.setCookies.push({
|
|
255
|
-
name:
|
|
509
|
+
name: COOKIE_NAMES.SESSION,
|
|
256
510
|
value: "",
|
|
257
|
-
options: {
|
|
258
|
-
maxAge: 0,
|
|
259
|
-
path: "/"
|
|
260
|
-
}
|
|
511
|
+
options: { ...base, sameSite: "lax" }
|
|
261
512
|
});
|
|
262
513
|
ctx.setCookies.push({
|
|
263
|
-
name:
|
|
514
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
264
515
|
value: "",
|
|
265
|
-
options: {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
516
|
+
options: { ...base, sameSite: "lax" }
|
|
517
|
+
});
|
|
518
|
+
ctx.setCookies.push({
|
|
519
|
+
name: COOKIE_NAMES.OAUTH_PENDING,
|
|
520
|
+
value: "",
|
|
521
|
+
options: { ...base, sameSite: "lax" }
|
|
269
522
|
});
|
|
270
523
|
}
|
|
271
524
|
await next();
|
|
@@ -273,19 +526,18 @@ var generalAuthInterceptor = {
|
|
|
273
526
|
};
|
|
274
527
|
|
|
275
528
|
// src/nextjs/interceptors/key-rotation.ts
|
|
276
|
-
import { generateKeyPair as generateKeyPair2, unsealSession as unsealSession2, sealSession as sealSession3, generateClientToken as generateClientToken2, getSessionTtl as getSessionTtl3, COOKIE_NAMES as COOKIE_NAMES3, authLogger as authLogger3 } from "@spfn/auth/server";
|
|
277
529
|
var keyRotationInterceptor = {
|
|
278
530
|
pathPattern: "/_auth/keys/rotate",
|
|
279
531
|
method: "POST",
|
|
280
532
|
request: async (ctx, next) => {
|
|
281
|
-
const sessionCookie = ctx.cookies.get(
|
|
533
|
+
const sessionCookie = ctx.cookies.get(COOKIE_NAMES.SESSION);
|
|
282
534
|
if (!sessionCookie) {
|
|
283
535
|
await next();
|
|
284
536
|
return;
|
|
285
537
|
}
|
|
286
538
|
try {
|
|
287
|
-
const currentSession = await
|
|
288
|
-
const newKeyPair =
|
|
539
|
+
const currentSession = await unsealSession(sessionCookie);
|
|
540
|
+
const newKeyPair = generateKeyPair("ES256");
|
|
289
541
|
if (!ctx.body) {
|
|
290
542
|
ctx.body = {};
|
|
291
543
|
}
|
|
@@ -298,7 +550,7 @@ var keyRotationInterceptor = {
|
|
|
298
550
|
console.log("publicKey:", newKeyPair.publicKey);
|
|
299
551
|
console.log("keyId:", newKeyPair.keyId);
|
|
300
552
|
console.log("fingerprint:", newKeyPair.fingerprint);
|
|
301
|
-
const token =
|
|
553
|
+
const token = generateClientToken(
|
|
302
554
|
{
|
|
303
555
|
userId: currentSession.userId,
|
|
304
556
|
keyId: currentSession.keyId,
|
|
@@ -317,7 +569,7 @@ var keyRotationInterceptor = {
|
|
|
317
569
|
ctx.metadata.userId = currentSession.userId;
|
|
318
570
|
} catch (error) {
|
|
319
571
|
const err = error;
|
|
320
|
-
|
|
572
|
+
authLogger.interceptor.keyRotation.error("Failed to prepare key rotation", err);
|
|
321
573
|
}
|
|
322
574
|
await next();
|
|
323
575
|
},
|
|
@@ -327,73 +579,90 @@ var keyRotationInterceptor = {
|
|
|
327
579
|
return;
|
|
328
580
|
}
|
|
329
581
|
if (!ctx.metadata.newPrivateKey || !ctx.metadata.userId) {
|
|
330
|
-
|
|
582
|
+
authLogger.interceptor.keyRotation.error("Missing key rotation metadata");
|
|
331
583
|
await next();
|
|
332
584
|
return;
|
|
333
585
|
}
|
|
334
586
|
try {
|
|
335
|
-
const ttl =
|
|
587
|
+
const ttl = getSessionTtl();
|
|
336
588
|
const newSessionData = {
|
|
337
589
|
userId: ctx.metadata.userId,
|
|
338
590
|
privateKey: ctx.metadata.newPrivateKey,
|
|
339
591
|
keyId: ctx.metadata.newKeyId,
|
|
340
592
|
algorithm: ctx.metadata.newAlgorithm
|
|
341
593
|
};
|
|
342
|
-
const sealed = await
|
|
594
|
+
const sealed = await sealSession(newSessionData, ttl);
|
|
343
595
|
ctx.setCookies.push({
|
|
344
|
-
name:
|
|
596
|
+
name: COOKIE_NAMES.SESSION,
|
|
345
597
|
value: sealed,
|
|
346
598
|
options: {
|
|
347
599
|
httpOnly: true,
|
|
348
600
|
secure: cookieSecure,
|
|
349
|
-
sameSite: "
|
|
601
|
+
sameSite: "lax",
|
|
350
602
|
maxAge: ttl,
|
|
351
603
|
path: "/"
|
|
352
604
|
}
|
|
353
605
|
});
|
|
354
606
|
ctx.setCookies.push({
|
|
355
|
-
name:
|
|
607
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
356
608
|
value: ctx.metadata.newKeyId,
|
|
357
609
|
options: {
|
|
358
610
|
httpOnly: true,
|
|
359
611
|
secure: cookieSecure,
|
|
360
|
-
sameSite: "
|
|
612
|
+
sameSite: "lax",
|
|
361
613
|
maxAge: ttl,
|
|
362
614
|
path: "/"
|
|
363
615
|
}
|
|
364
616
|
});
|
|
365
617
|
} catch (error) {
|
|
366
618
|
const err = error;
|
|
367
|
-
|
|
619
|
+
authLogger.interceptor.keyRotation.error("Failed to update session after rotation", err);
|
|
368
620
|
}
|
|
369
621
|
await next();
|
|
370
622
|
}
|
|
371
623
|
};
|
|
372
624
|
|
|
373
|
-
// src/
|
|
374
|
-
import
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
625
|
+
// src/server/lib/oauth/state.ts
|
|
626
|
+
import * as jose2 from "jose";
|
|
627
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
628
|
+
async function getStateKey() {
|
|
629
|
+
const secret = env3.SPFN_AUTH_SESSION_SECRET;
|
|
630
|
+
const encoder = new TextEncoder();
|
|
631
|
+
const data = encoder.encode(`oauth-state:${secret}`);
|
|
632
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
633
|
+
return new Uint8Array(hashBuffer);
|
|
634
|
+
}
|
|
635
|
+
function generateNonce() {
|
|
636
|
+
const array = new Uint8Array(16);
|
|
637
|
+
crypto.getRandomValues(array);
|
|
638
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
639
|
+
}
|
|
640
|
+
function generateOAuthNonce() {
|
|
641
|
+
return generateNonce();
|
|
642
|
+
}
|
|
643
|
+
async function createOAuthState(params) {
|
|
644
|
+
const key = await getStateKey();
|
|
645
|
+
const state = {
|
|
646
|
+
returnUrl: params.returnUrl,
|
|
647
|
+
nonce: params.nonce ?? generateNonce(),
|
|
648
|
+
provider: params.provider,
|
|
649
|
+
publicKey: params.publicKey,
|
|
650
|
+
keyId: params.keyId,
|
|
651
|
+
fingerprint: params.fingerprint,
|
|
652
|
+
algorithm: params.algorithm,
|
|
653
|
+
metadata: params.metadata
|
|
654
|
+
};
|
|
655
|
+
const jwe = await new jose2.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
|
|
656
|
+
return encodeURIComponent(jwe);
|
|
657
|
+
}
|
|
382
658
|
|
|
383
659
|
// src/nextjs/session-helpers.ts
|
|
384
|
-
import * as
|
|
660
|
+
import * as jose3 from "jose";
|
|
385
661
|
import { cookies } from "next/headers.js";
|
|
386
|
-
import {
|
|
387
|
-
sealSession as sealSession4,
|
|
388
|
-
unsealSession as unsealSession3,
|
|
389
|
-
COOKIE_NAMES as COOKIE_NAMES4,
|
|
390
|
-
getSessionTtl as getSessionTtl4,
|
|
391
|
-
parseDuration
|
|
392
|
-
} from "@spfn/auth/server";
|
|
393
|
-
import { env } from "@spfn/auth/config";
|
|
662
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
394
663
|
import { logger } from "@spfn/core/logger";
|
|
395
664
|
async function getPendingSessionKey() {
|
|
396
|
-
const secret =
|
|
665
|
+
const secret = env4.SPFN_AUTH_SESSION_SECRET;
|
|
397
666
|
const encoder = new TextEncoder();
|
|
398
667
|
const data = encoder.encode(`oauth-pending:${secret}`);
|
|
399
668
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -401,11 +670,11 @@ async function getPendingSessionKey() {
|
|
|
401
670
|
}
|
|
402
671
|
async function sealPendingSession(data, ttl = 600) {
|
|
403
672
|
const key = await getPendingSessionKey();
|
|
404
|
-
return await new
|
|
673
|
+
return await new jose3.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-oauth").encrypt(key);
|
|
405
674
|
}
|
|
406
|
-
async function unsealPendingSession(
|
|
675
|
+
async function unsealPendingSession(jwt2) {
|
|
407
676
|
const key = await getPendingSessionKey();
|
|
408
|
-
const { payload } = await
|
|
677
|
+
const { payload } = await jose3.jwtDecrypt(jwt2, key, {
|
|
409
678
|
issuer: "spfn-auth",
|
|
410
679
|
audience: "spfn-oauth"
|
|
411
680
|
});
|
|
@@ -419,14 +688,18 @@ var oauthUrlInterceptor = {
|
|
|
419
688
|
request: async (ctx, next) => {
|
|
420
689
|
const provider = ctx.path.split("/")[3];
|
|
421
690
|
const returnUrl = ctx.body?.returnUrl || "/";
|
|
422
|
-
const
|
|
691
|
+
const metadata = ctx.body?.metadata;
|
|
692
|
+
const keyPair = generateKeyPair("ES256");
|
|
693
|
+
const csrfNonce = generateOAuthNonce();
|
|
423
694
|
const state = await createOAuthState({
|
|
424
695
|
provider,
|
|
425
696
|
returnUrl,
|
|
426
697
|
publicKey: keyPair.publicKey,
|
|
427
698
|
keyId: keyPair.keyId,
|
|
428
699
|
fingerprint: keyPair.fingerprint,
|
|
429
|
-
algorithm: keyPair.algorithm
|
|
700
|
+
algorithm: keyPair.algorithm,
|
|
701
|
+
nonce: csrfNonce,
|
|
702
|
+
metadata
|
|
430
703
|
});
|
|
431
704
|
if (!ctx.body) {
|
|
432
705
|
ctx.body = {};
|
|
@@ -437,7 +710,8 @@ var oauthUrlInterceptor = {
|
|
|
437
710
|
keyId: keyPair.keyId,
|
|
438
711
|
algorithm: keyPair.algorithm
|
|
439
712
|
};
|
|
440
|
-
|
|
713
|
+
ctx.metadata.oauthCsrf = csrfNonce;
|
|
714
|
+
authLogger.interceptor.oauth?.debug?.("OAuth state created", {
|
|
441
715
|
provider,
|
|
442
716
|
keyId: keyPair.keyId
|
|
443
717
|
});
|
|
@@ -448,7 +722,7 @@ var oauthUrlInterceptor = {
|
|
|
448
722
|
try {
|
|
449
723
|
const sealed = await sealPendingSession(ctx.metadata.pendingSession);
|
|
450
724
|
ctx.setCookies.push({
|
|
451
|
-
name:
|
|
725
|
+
name: COOKIE_NAMES.OAUTH_PENDING,
|
|
452
726
|
value: sealed,
|
|
453
727
|
options: {
|
|
454
728
|
httpOnly: true,
|
|
@@ -460,17 +734,47 @@ var oauthUrlInterceptor = {
|
|
|
460
734
|
path: "/"
|
|
461
735
|
}
|
|
462
736
|
});
|
|
463
|
-
|
|
737
|
+
if (ctx.metadata.oauthCsrf) {
|
|
738
|
+
ctx.setCookies.push({
|
|
739
|
+
name: COOKIE_NAMES.OAUTH_CSRF,
|
|
740
|
+
value: ctx.metadata.oauthCsrf,
|
|
741
|
+
options: {
|
|
742
|
+
httpOnly: true,
|
|
743
|
+
secure: cookieSecure,
|
|
744
|
+
sameSite: "lax",
|
|
745
|
+
maxAge: 600,
|
|
746
|
+
path: "/"
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
authLogger.interceptor.oauth?.debug?.("Pending session cookie set", {
|
|
464
751
|
keyId: ctx.metadata.pendingSession.keyId
|
|
465
752
|
});
|
|
466
753
|
} catch (error) {
|
|
467
754
|
const err = error;
|
|
468
|
-
|
|
755
|
+
authLogger.interceptor.oauth?.error?.("Failed to set pending session", err);
|
|
469
756
|
}
|
|
470
757
|
}
|
|
471
758
|
await next();
|
|
472
759
|
}
|
|
473
760
|
};
|
|
761
|
+
function setFinalizeError(ctx, message) {
|
|
762
|
+
ctx.response.ok = false;
|
|
763
|
+
ctx.response.status = 401;
|
|
764
|
+
ctx.response.statusText = "Unauthorized";
|
|
765
|
+
ctx.response.body = { success: false, message };
|
|
766
|
+
ctx.setCookies.push({
|
|
767
|
+
name: COOKIE_NAMES.OAUTH_PENDING,
|
|
768
|
+
value: "",
|
|
769
|
+
options: {
|
|
770
|
+
httpOnly: true,
|
|
771
|
+
secure: cookieSecure,
|
|
772
|
+
sameSite: "lax",
|
|
773
|
+
maxAge: 0,
|
|
774
|
+
path: "/"
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
474
778
|
var oauthFinalizeInterceptor = {
|
|
475
779
|
pathPattern: /^\/_auth\/oauth\/finalize$/,
|
|
476
780
|
method: "POST",
|
|
@@ -479,9 +783,10 @@ var oauthFinalizeInterceptor = {
|
|
|
479
783
|
await next();
|
|
480
784
|
return;
|
|
481
785
|
}
|
|
482
|
-
const pendingCookie = ctx.cookies.get(
|
|
786
|
+
const pendingCookie = ctx.cookies.get(COOKIE_NAMES.OAUTH_PENDING);
|
|
483
787
|
if (!pendingCookie) {
|
|
484
|
-
|
|
788
|
+
authLogger.interceptor.oauth?.warn?.("No pending session cookie found");
|
|
789
|
+
setFinalizeError(ctx, "OAuth session expired. Please try again.");
|
|
485
790
|
await next();
|
|
486
791
|
return;
|
|
487
792
|
}
|
|
@@ -489,49 +794,51 @@ var oauthFinalizeInterceptor = {
|
|
|
489
794
|
const pendingSession = await unsealPendingSession(pendingCookie);
|
|
490
795
|
const { userId, keyId } = ctx.response.body || {};
|
|
491
796
|
if (!userId || !keyId) {
|
|
492
|
-
|
|
797
|
+
authLogger.interceptor.oauth?.error?.("Missing userId or keyId in response");
|
|
798
|
+
setFinalizeError(ctx, "OAuth finalize failed: missing credentials");
|
|
493
799
|
await next();
|
|
494
800
|
return;
|
|
495
801
|
}
|
|
496
802
|
if (pendingSession.keyId !== keyId) {
|
|
497
|
-
|
|
803
|
+
authLogger.interceptor.oauth?.error?.("KeyId mismatch", {
|
|
498
804
|
expected: pendingSession.keyId,
|
|
499
805
|
received: keyId
|
|
500
806
|
});
|
|
807
|
+
setFinalizeError(ctx, "OAuth session mismatch. Please try again.");
|
|
501
808
|
await next();
|
|
502
809
|
return;
|
|
503
810
|
}
|
|
504
|
-
const ttl =
|
|
505
|
-
const sessionToken = await
|
|
811
|
+
const ttl = getSessionTtl();
|
|
812
|
+
const sessionToken = await sealSession({
|
|
506
813
|
userId,
|
|
507
814
|
privateKey: pendingSession.privateKey,
|
|
508
815
|
keyId: pendingSession.keyId,
|
|
509
816
|
algorithm: pendingSession.algorithm
|
|
510
817
|
}, ttl);
|
|
511
818
|
ctx.setCookies.push({
|
|
512
|
-
name:
|
|
819
|
+
name: COOKIE_NAMES.SESSION,
|
|
513
820
|
value: sessionToken,
|
|
514
821
|
options: {
|
|
515
822
|
httpOnly: true,
|
|
516
823
|
secure: cookieSecure,
|
|
517
|
-
sameSite: "
|
|
824
|
+
sameSite: "lax",
|
|
518
825
|
maxAge: ttl,
|
|
519
826
|
path: "/"
|
|
520
827
|
}
|
|
521
828
|
});
|
|
522
829
|
ctx.setCookies.push({
|
|
523
|
-
name:
|
|
830
|
+
name: COOKIE_NAMES.SESSION_KEY_ID,
|
|
524
831
|
value: keyId,
|
|
525
832
|
options: {
|
|
526
833
|
httpOnly: true,
|
|
527
834
|
secure: cookieSecure,
|
|
528
|
-
sameSite: "
|
|
835
|
+
sameSite: "lax",
|
|
529
836
|
maxAge: ttl,
|
|
530
837
|
path: "/"
|
|
531
838
|
}
|
|
532
839
|
});
|
|
533
840
|
ctx.setCookies.push({
|
|
534
|
-
name:
|
|
841
|
+
name: COOKIE_NAMES.OAUTH_PENDING,
|
|
535
842
|
value: "",
|
|
536
843
|
options: {
|
|
537
844
|
httpOnly: true,
|
|
@@ -541,13 +848,14 @@ var oauthFinalizeInterceptor = {
|
|
|
541
848
|
path: "/"
|
|
542
849
|
}
|
|
543
850
|
});
|
|
544
|
-
|
|
851
|
+
authLogger.interceptor.oauth?.debug?.("OAuth session finalized", {
|
|
545
852
|
userId,
|
|
546
853
|
keyId
|
|
547
854
|
});
|
|
548
855
|
} catch (error) {
|
|
549
856
|
const err = error;
|
|
550
|
-
|
|
857
|
+
authLogger.interceptor.oauth?.error?.("Failed to finalize OAuth session", err);
|
|
858
|
+
setFinalizeError(ctx, err.message);
|
|
551
859
|
}
|
|
552
860
|
await next();
|
|
553
861
|
}
|