@coreiam/auth 1.0.392-beta.1 → 1.0.392-beta.12
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/dist/core/index.d.ts +63 -3
- package/dist/core/index.js +167 -11
- package/dist/express/index.js +8 -8
- package/dist/hono/index.d.ts +33 -0
- package/dist/hono/index.js +195 -0
- package/dist/nextjs/actions.d.ts +12 -3
- package/dist/nextjs/actions.js +130 -29
- package/dist/nextjs/index.d.ts +1 -1
- package/dist/nextjs/index.js +55 -11
- package/dist/nextjs/middleware.js +127 -32
- package/dist/nextjs/provider.d.ts +17 -1
- package/dist/nextjs/provider.js +136 -4
- package/dist/react/provider.js +1 -0
- package/package.json +28 -7
package/dist/nextjs/actions.js
CHANGED
|
@@ -99,11 +99,19 @@ exports.loginActionSdk = loginActionSdk;
|
|
|
99
99
|
exports.registerActionSdk = registerActionSdk;
|
|
100
100
|
exports.logoutActionSdk = logoutActionSdk;
|
|
101
101
|
exports.updateProfileActionSdk = updateProfileActionSdk;
|
|
102
|
+
exports.changePasswordActionSdk = changePasswordActionSdk;
|
|
102
103
|
exports.setupMfaActionSdk = setupMfaActionSdk;
|
|
103
104
|
exports.verifyMfaActionSdk = verifyMfaActionSdk;
|
|
104
105
|
exports.disableMfaActionSdk = disableMfaActionSdk;
|
|
105
106
|
exports.requestOtpActionSdk = requestOtpActionSdk;
|
|
106
107
|
exports.verifyOtpActionSdk = verifyOtpActionSdk;
|
|
108
|
+
exports.verifyEmailActionSdk = verifyEmailActionSdk;
|
|
109
|
+
exports.resendVerificationActionSdk = resendVerificationActionSdk;
|
|
110
|
+
exports.forgotPasswordActionSdk = forgotPasswordActionSdk;
|
|
111
|
+
exports.resetPasswordActionSdk = resetPasswordActionSdk;
|
|
112
|
+
exports.listSessionsActionSdk = listSessionsActionSdk;
|
|
113
|
+
exports.revokeSessionActionSdk = revokeSessionActionSdk;
|
|
114
|
+
exports.revokeAllSessionsActionSdk = revokeAllSessionsActionSdk;
|
|
107
115
|
const headers_1 = require("next/headers");
|
|
108
116
|
const core_1 = require("../core");
|
|
109
117
|
// FIX: lazy singleton instead of `const iam = new CoreIAM();` at module
|
|
@@ -210,34 +218,13 @@ async function getSession() {
|
|
|
210
218
|
return null;
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
const authHeader = reqHeaders.get('Authorization') || reqHeaders.get('authorization');
|
|
218
|
-
const cookieHeader = reqHeaders.get('cookie');
|
|
219
|
-
let token = undefined;
|
|
220
|
-
if (authHeader?.startsWith('Bearer ')) {
|
|
221
|
-
token = authHeader.substring(7);
|
|
222
|
-
}
|
|
223
|
-
else if (cookieHeader) {
|
|
224
|
-
const match = cookieHeader.match(/jwt=([^;]+)/);
|
|
225
|
-
if (match)
|
|
226
|
-
token = match[1];
|
|
227
|
-
}
|
|
228
|
-
if (!token)
|
|
229
|
-
return null;
|
|
221
|
+
// FIX (SDK-1): delegate to the SAME verification funnel coreIamMiddleware
|
|
222
|
+
// uses (CoreIAM.verifySession — jose, RS256-pinned, issuer-checked) instead
|
|
223
|
+
// of decoding the payload unverified. One implementation, zero drift,
|
|
224
|
+
// no new env vars. Forged tokens -> null.
|
|
230
225
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const payloadStr = Buffer.from(payloadB64, 'base64').toString('utf8');
|
|
234
|
-
const payload = JSON.parse(payloadStr);
|
|
235
|
-
return {
|
|
236
|
-
sub: payload.sub,
|
|
237
|
-
email: payload.email,
|
|
238
|
-
tenantId: payload.tenantId,
|
|
239
|
-
roles: payload.roles || []
|
|
240
|
-
};
|
|
226
|
+
const result = await getIam().verifySession(reqHeaders);
|
|
227
|
+
return result.identity ?? null;
|
|
241
228
|
}
|
|
242
229
|
catch {
|
|
243
230
|
return null;
|
|
@@ -291,6 +278,16 @@ async function updateProfileActionSdk(updateData) {
|
|
|
291
278
|
return { ok: false, status: 500, data: { error: 'Profile update failed' } };
|
|
292
279
|
}
|
|
293
280
|
}
|
|
281
|
+
async function changePasswordActionSdk(currentPassword, newPassword) {
|
|
282
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
283
|
+
try {
|
|
284
|
+
const response = await getIam().changePassword(currentPassword, newPassword, reqHeaders);
|
|
285
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
return { ok: false, status: 500, data: { error: 'Password change failed' } };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
294
291
|
async function setupMfaActionSdk() {
|
|
295
292
|
const reqHeaders = await (0, headers_1.headers)();
|
|
296
293
|
try {
|
|
@@ -325,7 +322,12 @@ async function requestOtpActionSdk(body) {
|
|
|
325
322
|
const reqHeaders = await (0, headers_1.headers)();
|
|
326
323
|
try {
|
|
327
324
|
const response = await getIam().requestOtp(body, reqHeaders);
|
|
328
|
-
|
|
325
|
+
// FIX (SDK-3, uniformity): request-otp currently sets no cookies, but routing
|
|
326
|
+
// any Set-Cookie through the same sync as every other auth action keeps the
|
|
327
|
+
// invariant above true if the backend ever starts setting one.
|
|
328
|
+
await syncCookiesToNext(response);
|
|
329
|
+
const data = await response.json().catch(() => ({}));
|
|
330
|
+
return { ok: response.ok, status: response.status, data, retryAfter: (0, core_1.retryAfterSeconds)(data) };
|
|
329
331
|
}
|
|
330
332
|
catch (error) {
|
|
331
333
|
return { ok: false, status: 500, data: { error: 'OTP request failed' } };
|
|
@@ -335,9 +337,108 @@ async function verifyOtpActionSdk(body) {
|
|
|
335
337
|
const reqHeaders = await (0, headers_1.headers)();
|
|
336
338
|
try {
|
|
337
339
|
const response = await getIam().verifyOtp(body, reqHeaders);
|
|
338
|
-
|
|
340
|
+
// FIX (SDK-3): sync Set-Cookie to the Next.js cookie store before returning.
|
|
341
|
+
// W2's POST /auth/verify-otp mints an OTP session and Set-Cookies the jwt
|
|
342
|
+
// cookie (Max-Age 900 — verified live in acceptance). Without this line the
|
|
343
|
+
// action silently dropped the session cookie, so passwordless login via
|
|
344
|
+
// server action never established a browser session. Mirrors the
|
|
345
|
+
// loginActionSdk / registerActionSdk pattern exactly.
|
|
346
|
+
await syncCookiesToNext(response);
|
|
347
|
+
const data = await response.json().catch(() => ({}));
|
|
348
|
+
return { ok: response.ok, status: response.status, data, retryAfter: (0, core_1.retryAfterSeconds)(data) };
|
|
339
349
|
}
|
|
340
350
|
catch (error) {
|
|
341
351
|
return { ok: false, status: 500, data: { error: 'OTP verification failed' } };
|
|
342
352
|
}
|
|
343
353
|
}
|
|
354
|
+
// W5 (beta.9): signup email verification — register fires the verification
|
|
355
|
+
// email (emailVerificationRequired: true); these close the loop so the SDK
|
|
356
|
+
// surface matches the server's /auth/verify-email + resend-verification pair.
|
|
357
|
+
async function verifyEmailActionSdk(body) {
|
|
358
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
359
|
+
try {
|
|
360
|
+
const response = await getIam().verifyEmail(body, reqHeaders);
|
|
361
|
+
await syncCookiesToNext(response);
|
|
362
|
+
const data = await response.json().catch(() => ({}));
|
|
363
|
+
return { ok: response.ok, status: response.status, data, retryAfter: (0, core_1.retryAfterSeconds)(data) };
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
return { ok: false, status: 500, data: { error: 'Email verification failed' } };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function resendVerificationActionSdk(email) {
|
|
370
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
371
|
+
try {
|
|
372
|
+
const response = await getIam().resendVerification(email, reqHeaders);
|
|
373
|
+
await syncCookiesToNext(response);
|
|
374
|
+
const data = await response.json().catch(() => ({}));
|
|
375
|
+
return { ok: response.ok, status: response.status, data, retryAfter: (0, core_1.retryAfterSeconds)(data) };
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
return { ok: false, status: 500, data: { error: 'Verification resend failed' } };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function forgotPasswordActionSdk(email) {
|
|
382
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
383
|
+
try {
|
|
384
|
+
const response = await getIam().forgotPassword(email, reqHeaders);
|
|
385
|
+
await syncCookiesToNext(response);
|
|
386
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return { ok: false, status: 500, data: { error: 'Password reset request failed' } };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function resetPasswordActionSdk(email, otp, newPassword) {
|
|
393
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
394
|
+
try {
|
|
395
|
+
const response = await getIam().resetPassword(email, otp, newPassword, reqHeaders);
|
|
396
|
+
// W2's reset-password revokes ALL session families server-side
|
|
397
|
+
// (revokeAllUserSessions) and clears cookies via Set-Cookie deletions —
|
|
398
|
+
// this sync applies them to the browser. Without it the client would sit
|
|
399
|
+
// on dead cookies after a successful reset.
|
|
400
|
+
await syncCookiesToNext(response);
|
|
401
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
return { ok: false, status: 500, data: { error: 'Password reset failed' } };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// --- W4 (beta.7): session management. listSessions in a Server Component,
|
|
408
|
+
// revoke calls from client components via server actions — same funnel and
|
|
409
|
+
// same AuthResult contract as every action above. The backend sets no
|
|
410
|
+
// cookies on these routes, but Set-Cookie sync stays wired for uniformity
|
|
411
|
+
// (SDK-3 pattern).
|
|
412
|
+
async function listSessionsActionSdk() {
|
|
413
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
414
|
+
try {
|
|
415
|
+
const response = await getIam().listSessions(reqHeaders);
|
|
416
|
+
await syncCookiesToNext(response);
|
|
417
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
return { ok: false, status: 500, data: { error: 'Failed to list sessions' } };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function revokeSessionActionSdk(sessionId) {
|
|
424
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
425
|
+
try {
|
|
426
|
+
const response = await getIam().revokeSession(sessionId, reqHeaders);
|
|
427
|
+
await syncCookiesToNext(response);
|
|
428
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
return { ok: false, status: 500, data: { error: error instanceof Error ? error.message : 'Session revoke failed' } };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function revokeAllSessionsActionSdk() {
|
|
435
|
+
const reqHeaders = await (0, headers_1.headers)();
|
|
436
|
+
try {
|
|
437
|
+
const response = await getIam().revokeAllSessions(reqHeaders);
|
|
438
|
+
await syncCookiesToNext(response);
|
|
439
|
+
return { ok: response.ok, status: response.status, data: await response.json().catch(() => ({})) };
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return { ok: false, status: 500, data: { error: 'Revoke-all failed' } };
|
|
443
|
+
}
|
|
444
|
+
}
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { CoreIAM, CoreIAMIdentity } from '../core';
|
|
3
3
|
export { CoreIAMProvider, useAuth } from './provider';
|
|
4
4
|
export { coreIamMiddleware, coreIamAuth } from './middleware';
|
|
5
|
-
export { getSession, loginActionSdk, registerActionSdk, logoutActionSdk, updateProfileActionSdk, setupMfaActionSdk, verifyMfaActionSdk, disableMfaActionSdk, requestOtpActionSdk, verifyOtpActionSdk } from './actions';
|
|
5
|
+
export { getSession, loginActionSdk, registerActionSdk, logoutActionSdk, updateProfileActionSdk, setupMfaActionSdk, verifyMfaActionSdk, disableMfaActionSdk, requestOtpActionSdk, verifyOtpActionSdk, forgotPasswordActionSdk, resetPasswordActionSdk, listSessionsActionSdk, revokeSessionActionSdk, revokeAllSessionsActionSdk } from './actions';
|
|
6
6
|
export declare class NextHandlers {
|
|
7
7
|
private iam;
|
|
8
8
|
constructor(iam?: CoreIAM);
|
package/dist/nextjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OAuthHandlers = exports.NextHandlers = exports.verifyOtpActionSdk = exports.requestOtpActionSdk = exports.disableMfaActionSdk = exports.verifyMfaActionSdk = exports.setupMfaActionSdk = exports.updateProfileActionSdk = exports.logoutActionSdk = exports.registerActionSdk = exports.loginActionSdk = exports.getSession = exports.coreIamAuth = exports.coreIamMiddleware = exports.useAuth = exports.CoreIAMProvider = void 0;
|
|
3
|
+
exports.OAuthHandlers = exports.NextHandlers = exports.revokeAllSessionsActionSdk = exports.revokeSessionActionSdk = exports.listSessionsActionSdk = exports.resetPasswordActionSdk = exports.forgotPasswordActionSdk = exports.verifyOtpActionSdk = exports.requestOtpActionSdk = exports.disableMfaActionSdk = exports.verifyMfaActionSdk = exports.setupMfaActionSdk = exports.updateProfileActionSdk = exports.logoutActionSdk = exports.registerActionSdk = exports.loginActionSdk = exports.getSession = exports.coreIamAuth = exports.coreIamMiddleware = exports.useAuth = exports.CoreIAMProvider = void 0;
|
|
4
4
|
exports.auth = auth;
|
|
5
5
|
// src/nextjs/index.ts
|
|
6
6
|
const server_1 = require("next/server");
|
|
@@ -23,6 +23,11 @@ Object.defineProperty(exports, "verifyMfaActionSdk", { enumerable: true, get: fu
|
|
|
23
23
|
Object.defineProperty(exports, "disableMfaActionSdk", { enumerable: true, get: function () { return actions_1.disableMfaActionSdk; } });
|
|
24
24
|
Object.defineProperty(exports, "requestOtpActionSdk", { enumerable: true, get: function () { return actions_1.requestOtpActionSdk; } });
|
|
25
25
|
Object.defineProperty(exports, "verifyOtpActionSdk", { enumerable: true, get: function () { return actions_1.verifyOtpActionSdk; } });
|
|
26
|
+
Object.defineProperty(exports, "forgotPasswordActionSdk", { enumerable: true, get: function () { return actions_1.forgotPasswordActionSdk; } });
|
|
27
|
+
Object.defineProperty(exports, "resetPasswordActionSdk", { enumerable: true, get: function () { return actions_1.resetPasswordActionSdk; } });
|
|
28
|
+
Object.defineProperty(exports, "listSessionsActionSdk", { enumerable: true, get: function () { return actions_1.listSessionsActionSdk; } });
|
|
29
|
+
Object.defineProperty(exports, "revokeSessionActionSdk", { enumerable: true, get: function () { return actions_1.revokeSessionActionSdk; } });
|
|
30
|
+
Object.defineProperty(exports, "revokeAllSessionsActionSdk", { enumerable: true, get: function () { return actions_1.revokeAllSessionsActionSdk; } });
|
|
26
31
|
class NextHandlers {
|
|
27
32
|
iam;
|
|
28
33
|
constructor(iam) {
|
|
@@ -62,6 +67,10 @@ class OAuthHandlers {
|
|
|
62
67
|
}
|
|
63
68
|
// Handles the initial redirect to Google/GitHub
|
|
64
69
|
async handleInitiate(req, provider) {
|
|
70
|
+
// FIX (SDK-5): validate the provider segment before interpolation.
|
|
71
|
+
if (!core_1.OAUTH_PROVIDERS.has(provider)) {
|
|
72
|
+
return server_1.NextResponse.redirect(new URL('/auth/login', req.url));
|
|
73
|
+
}
|
|
65
74
|
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/${provider}`;
|
|
66
75
|
const response = await this.iam.proxy(`/auth/oauth/initiate/${provider}?redirectUri=${encodeURIComponent(redirectUri)}&scopes=email,profile`, { method: 'GET' }, req.headers);
|
|
67
76
|
const data = await response.json();
|
|
@@ -72,11 +81,19 @@ class OAuthHandlers {
|
|
|
72
81
|
}
|
|
73
82
|
// Handles the callback from Google/GitHub
|
|
74
83
|
async handleCallback(req, provider) {
|
|
84
|
+
// FIX (SDK-5): validate the provider segment before interpolation.
|
|
85
|
+
if (!core_1.OAUTH_PROVIDERS.has(provider)) {
|
|
86
|
+
return server_1.NextResponse.redirect(new URL('/auth/login', req.url));
|
|
87
|
+
}
|
|
75
88
|
const { searchParams } = new URL(req.url);
|
|
76
89
|
const code = searchParams.get('code');
|
|
77
90
|
const state = searchParams.get('state');
|
|
78
91
|
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/${provider}`;
|
|
79
|
-
|
|
92
|
+
// FIX (SDK-5): raw code/state/redirectUri here was a query-injection surface.
|
|
93
|
+
const targetUrl = `/auth/oauth/callback/${provider}`
|
|
94
|
+
+ `?code=${encodeURIComponent(code ?? '')}`
|
|
95
|
+
+ `&state=${encodeURIComponent(state ?? '')}`
|
|
96
|
+
+ `&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
80
97
|
const response = await this.iam.proxy(targetUrl, { method: 'GET' }, req.headers);
|
|
81
98
|
const res = server_1.NextResponse.redirect(new URL('/', req.url));
|
|
82
99
|
const setCookies = response.headers.getSetCookie();
|
|
@@ -89,8 +106,39 @@ class OAuthHandlers {
|
|
|
89
106
|
}
|
|
90
107
|
}
|
|
91
108
|
exports.OAuthHandlers = OAuthHandlers;
|
|
109
|
+
// FIX (SDK-7): bounded LRU instead of wholesale .clear() at 10k. Flushing the
|
|
110
|
+
// whole map on overflow let an attacker cycle 10,001 distinct fake Bearer
|
|
111
|
+
// tokens and evict every LEGIT entry — all real users' next requests re-hit
|
|
112
|
+
// the backend verify (cache-stampede amplifier). LRU keeps hot legit entries
|
|
113
|
+
// alive under exactly that attack. Hand-rolled on purpose: zero new
|
|
114
|
+
// dependencies for a published SDK; semantics fully covered by vitest.
|
|
92
115
|
const MAX_CACHE_SIZE = 10000;
|
|
116
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
93
117
|
const apiKeyCache = new Map();
|
|
118
|
+
// LRU read: drop stale entries, and delete+re-set moves the entry to the
|
|
119
|
+
// Map's tail — JavaScript Maps preserve insertion order, so the tail is
|
|
120
|
+
// "most recently used" and the head is "least recently used".
|
|
121
|
+
function cacheGet(token) {
|
|
122
|
+
const entry = apiKeyCache.get(token);
|
|
123
|
+
if (!entry)
|
|
124
|
+
return undefined;
|
|
125
|
+
if (entry.expires <= Date.now()) {
|
|
126
|
+
apiKeyCache.delete(token);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
apiKeyCache.delete(token);
|
|
130
|
+
apiKeyCache.set(token, entry);
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
// LRU write: insert at tail; if over capacity, evict the head (oldest use).
|
|
134
|
+
function cacheSet(token, entry) {
|
|
135
|
+
apiKeyCache.set(token, entry);
|
|
136
|
+
if (apiKeyCache.size > MAX_CACHE_SIZE) {
|
|
137
|
+
const lru = apiKeyCache.keys().next().value;
|
|
138
|
+
if (lru !== undefined)
|
|
139
|
+
apiKeyCache.delete(lru);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
94
142
|
async function auth(request) {
|
|
95
143
|
const authHeader = request.headers.get('authorization');
|
|
96
144
|
if (!authHeader?.startsWith('Bearer '))
|
|
@@ -99,15 +147,11 @@ async function auth(request) {
|
|
|
99
147
|
const gatewayUrl = process.env.COREIAM_API_URL;
|
|
100
148
|
if (!gatewayUrl)
|
|
101
149
|
return null;
|
|
102
|
-
// 1. Check Cache
|
|
103
|
-
const cached =
|
|
104
|
-
if (cached
|
|
150
|
+
// 1. Check Cache (was: raw .get() + expiry check + wholesale .clear() on size)
|
|
151
|
+
const cached = cacheGet(token);
|
|
152
|
+
if (cached) {
|
|
105
153
|
return cached.valid ? cached.identity : null;
|
|
106
154
|
}
|
|
107
|
-
// FIX: Prevent memory leak by clearing cache if it gets too large
|
|
108
|
-
if (apiKeyCache.size >= MAX_CACHE_SIZE) {
|
|
109
|
-
apiKeyCache.clear(); // Simple crude eviction. In production SDK, use lru-cache.
|
|
110
|
-
}
|
|
111
155
|
// 2. Call CoreIAM to verify
|
|
112
156
|
try {
|
|
113
157
|
const res = await fetch(`${gatewayUrl}/auth/api-keys/verify`, {
|
|
@@ -124,11 +168,11 @@ async function auth(request) {
|
|
|
124
168
|
sub: data.sub,
|
|
125
169
|
tenantId: data.tenantId,
|
|
126
170
|
};
|
|
127
|
-
|
|
171
|
+
cacheSet(token, { valid: true, identity, expires: Date.now() + CACHE_TTL_MS });
|
|
128
172
|
return identity;
|
|
129
173
|
}
|
|
130
174
|
else {
|
|
131
|
-
|
|
175
|
+
cacheSet(token, { valid: false, expires: Date.now() + CACHE_TTL_MS });
|
|
132
176
|
return null;
|
|
133
177
|
}
|
|
134
178
|
}
|
|
@@ -122,11 +122,11 @@ exports.coreIamMiddleware = coreIamMiddleware;
|
|
|
122
122
|
// }
|
|
123
123
|
// return { response: NextResponse.next(), isAuthenticated };
|
|
124
124
|
// }
|
|
125
|
-
// src/nextjs/middleware.ts
|
|
125
|
+
// sdk/javascript/src/nextjs/middleware.ts
|
|
126
126
|
const server_1 = require("next/server");
|
|
127
127
|
//import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
128
128
|
const core_1 = require("../core");
|
|
129
|
-
const gatewayUrl = process.env.
|
|
129
|
+
const gatewayUrl = (0, core_1.resolveGatewayUrl)({ apiKey: process.env.COREIAM_API_KEY || '' });
|
|
130
130
|
/**
|
|
131
131
|
const JWKS = gatewayUrl
|
|
132
132
|
? createRemoteJWKSet(new URL(`${gatewayUrl}/.well-known/jwks.json`))
|
|
@@ -153,20 +153,56 @@ async function performRefresh(refreshTokenCookie, jwtCookie) {
|
|
|
153
153
|
});
|
|
154
154
|
if (!refreshRes.ok)
|
|
155
155
|
return null;
|
|
156
|
+
// FIX (SDK-11): derive Max-Age from the backend's own Set-Cookie instead of
|
|
157
|
+
// hardcoding 24h/7d — the hardcoded values silently overrode the backend's
|
|
158
|
+
// post-W1 policy (jwt Max-Age 900). The backend is the source of truth.
|
|
156
159
|
const setCookies = refreshRes.headers.getSetCookie();
|
|
157
160
|
let newJwt = '', newRefreshToken = '';
|
|
161
|
+
let jwtMaxAge = 900, refreshMaxAge = 7 * 24 * 60 * 60; // fallbacks if backend sends no Max-Age
|
|
158
162
|
setCookies.forEach(cookie => {
|
|
159
|
-
|
|
163
|
+
const maxAgeMatch = cookie.match(/max-age=(-?\d+)/i);
|
|
164
|
+
if (cookie.startsWith('jwt=')) {
|
|
160
165
|
newJwt = cookie.split(';')[0].split('=')[1];
|
|
161
|
-
|
|
166
|
+
if (maxAgeMatch)
|
|
167
|
+
jwtMaxAge = parseInt(maxAgeMatch[1], 10);
|
|
168
|
+
}
|
|
169
|
+
if (cookie.startsWith('refreshToken=')) {
|
|
162
170
|
newRefreshToken = cookie.split(';')[0].split('=')[1];
|
|
171
|
+
if (maxAgeMatch)
|
|
172
|
+
refreshMaxAge = parseInt(maxAgeMatch[1], 10);
|
|
173
|
+
}
|
|
163
174
|
});
|
|
164
|
-
return newJwt ? { jwt: newJwt, refreshToken: newRefreshToken } : null;
|
|
175
|
+
return newJwt ? { jwt: newJwt, refreshToken: newRefreshToken, jwtMaxAge, refreshMaxAge } : null;
|
|
165
176
|
}
|
|
166
177
|
catch (error) {
|
|
167
178
|
return null;
|
|
168
179
|
}
|
|
169
180
|
}
|
|
181
|
+
// async function performRefresh(
|
|
182
|
+
// refreshTokenCookie: { value: string } | undefined,
|
|
183
|
+
// jwtCookie: { value: string } | undefined,
|
|
184
|
+
// ): Promise<{ jwt: string; refreshToken: string } | null> {
|
|
185
|
+
// if (!gatewayUrl) return null;
|
|
186
|
+
// try {
|
|
187
|
+
// const refreshRes = await fetch(`${gatewayUrl}/auth/refresh-token`, {
|
|
188
|
+
// method: 'POST',
|
|
189
|
+
// headers: {
|
|
190
|
+
// 'X-API-Key': process.env.COREIAM_API_KEY!,
|
|
191
|
+
// 'cookie': `refreshToken=${refreshTokenCookie?.value || ''}; jwt=${jwtCookie?.value || ''}`,
|
|
192
|
+
// },
|
|
193
|
+
// });
|
|
194
|
+
// if (!refreshRes.ok) return null;
|
|
195
|
+
// const setCookies = refreshRes.headers.getSetCookie();
|
|
196
|
+
// let newJwt = '', newRefreshToken = '';
|
|
197
|
+
// setCookies.forEach(cookie => {
|
|
198
|
+
// if (cookie.startsWith('jwt=')) newJwt = cookie.split(';')[0].split('=')[1];
|
|
199
|
+
// if (cookie.startsWith('refreshToken=')) newRefreshToken = cookie.split(';')[0].split('=')[1];
|
|
200
|
+
// });
|
|
201
|
+
// return newJwt ? { jwt: newJwt, refreshToken: newRefreshToken } : null;
|
|
202
|
+
// } catch (error) {
|
|
203
|
+
// return null;
|
|
204
|
+
// }
|
|
205
|
+
// }
|
|
170
206
|
/**
|
|
171
207
|
* Helper function for custom middleware (proxy.ts).
|
|
172
208
|
* Returns the response and authentication state.
|
|
@@ -182,7 +218,12 @@ async function coreIamAuth(request) {
|
|
|
182
218
|
if (method === 'OPTIONS') {
|
|
183
219
|
const res = new server_1.NextResponse(null, { status: 204 }); // 204 No Content
|
|
184
220
|
const origin = request.headers.get('origin');
|
|
185
|
-
|
|
221
|
+
// FIX (SDK-14): reflect only known origins — reflecting ANY origin together
|
|
222
|
+
// with Allow-Credentials is a footgun even though this only answers
|
|
223
|
+
// preflights (actual responses carry no CORS headers).
|
|
224
|
+
const allowedOrigins = (process.env.COREIAM_ALLOWED_ORIGINS || '')
|
|
225
|
+
.split(',').map(s => s.trim()).filter(Boolean);
|
|
226
|
+
if (origin && (origin === request.nextUrl.origin || allowedOrigins.includes(origin))) {
|
|
186
227
|
res.headers.set('Access-Control-Allow-Origin', origin);
|
|
187
228
|
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
188
229
|
res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token');
|
|
@@ -194,7 +235,7 @@ async function coreIamAuth(request) {
|
|
|
194
235
|
const iam = getIam();
|
|
195
236
|
if (!iam) {
|
|
196
237
|
return {
|
|
197
|
-
response: new server_1.NextResponse('CoreIAM is not configured (
|
|
238
|
+
response: new server_1.NextResponse('CoreIAM is not configured (COREIAM_API_KEY required; gateway URL defaults by key prefix)', { status: 500 }),
|
|
198
239
|
isAuthenticated: false,
|
|
199
240
|
};
|
|
200
241
|
}
|
|
@@ -235,7 +276,7 @@ async function coreIamAuth(request) {
|
|
|
235
276
|
</head>
|
|
236
277
|
<body>
|
|
237
278
|
<p>Account linked successfully! Redirecting...</p>
|
|
238
|
-
<script>window.location.href =
|
|
279
|
+
<script>window.location.href = ${JSON.stringify(finalUrl)};</script>
|
|
239
280
|
</body>
|
|
240
281
|
</html>
|
|
241
282
|
`;
|
|
@@ -263,25 +304,46 @@ async function coreIamAuth(request) {
|
|
|
263
304
|
let targetPath = pathname.replace('/api/auth', '/auth');
|
|
264
305
|
const body = method !== 'GET' ? await request.json().catch(() => undefined) : undefined;
|
|
265
306
|
// FIX: Special routing for OAuth Initiate
|
|
307
|
+
// if (pathname.startsWith('/api/auth/oauth/') && !pathname.includes('callback') && !pathname.includes('link')) {
|
|
308
|
+
// const provider = pathname.split('/').pop();
|
|
309
|
+
// const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/${provider}`;
|
|
310
|
+
// targetPath = `/auth/oauth/initiate/${provider}?redirectUri=${encodeURIComponent(redirectUri)}&scopes=email,profile`;
|
|
311
|
+
// }
|
|
312
|
+
// FIX: Special routing for OAuth Initiate
|
|
266
313
|
if (pathname.startsWith('/api/auth/oauth/') && !pathname.includes('callback') && !pathname.includes('link')) {
|
|
267
|
-
const provider = pathname.split('/').pop();
|
|
314
|
+
const provider = pathname.split('/').pop() || '';
|
|
315
|
+
// FIX (SDK-5): reject unknown providers before URL interpolation.
|
|
316
|
+
if (!core_1.OAUTH_PROVIDERS.has(provider)) {
|
|
317
|
+
return { response: new server_1.NextResponse('Unsupported OAuth provider', { status: 400 }), isAuthenticated: false };
|
|
318
|
+
}
|
|
268
319
|
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/${provider}`;
|
|
269
320
|
targetPath = `/auth/oauth/initiate/${provider}?redirectUri=${encodeURIComponent(redirectUri)}&scopes=email,profile`;
|
|
270
321
|
}
|
|
271
|
-
// FIX: Special routing for OAuth Callback
|
|
272
322
|
if (pathname.startsWith('/api/auth/callback/')) {
|
|
273
|
-
const provider = pathname.split('/').pop();
|
|
323
|
+
const provider = pathname.split('/').pop() || '';
|
|
324
|
+
// FIX (SDK-5): reject unknown providers before URL interpolation.
|
|
325
|
+
if (!core_1.OAUTH_PROVIDERS.has(provider)) {
|
|
326
|
+
return { response: new server_1.NextResponse('Unsupported OAuth provider', { status: 400 }), isAuthenticated: false };
|
|
327
|
+
}
|
|
274
328
|
const searchParams = new URL(request.url).searchParams;
|
|
275
329
|
const code = searchParams.get('code');
|
|
276
330
|
const state = searchParams.get('state');
|
|
277
331
|
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/${provider}`;
|
|
278
|
-
targetPath = `/auth/oauth/callback/${provider}?code=${code}&state=${state}&redirectUri=${redirectUri}`;
|
|
332
|
+
// targetPath = `/auth/oauth/callback/${provider}?code=${code}&state=${state}&redirectUri=${redirectUri}`;
|
|
333
|
+
// OAuth callback targetPath — encode all three:
|
|
334
|
+
targetPath = `/auth/oauth/callback/${provider}`
|
|
335
|
+
+ `?code=${encodeURIComponent(code ?? '')}`
|
|
336
|
+
+ `&state=${encodeURIComponent(state ?? '')}`
|
|
337
|
+
+ `&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
279
338
|
}
|
|
280
|
-
const
|
|
339
|
+
const init = {
|
|
281
340
|
method,
|
|
282
341
|
body: body ? JSON.stringify(body) : undefined,
|
|
283
342
|
headers: { 'Content-Type': 'application/json' },
|
|
284
|
-
}
|
|
343
|
+
};
|
|
344
|
+
const response = core_1.CSRF_PROTECTED_PATHS.has(targetPath)
|
|
345
|
+
? await iam.proxyWithCsrfProtection(targetPath, init, request.headers)
|
|
346
|
+
: await iam.proxy(targetPath, init, request.headers);
|
|
285
347
|
// Handle OAuth Initiate Redirects
|
|
286
348
|
if (pathname.startsWith('/api/auth/oauth/') && !pathname.includes('callback') && !pathname.includes('link')) {
|
|
287
349
|
const data = await response.json().catch(() => ({}));
|
|
@@ -349,6 +411,13 @@ async function coreIamAuth(request) {
|
|
|
349
411
|
|
|
350
412
|
*/
|
|
351
413
|
// 2. LOCAL JWT VERIFICATION & REFRESH
|
|
414
|
+
// FIX (SDK-9): strip any client-supplied `x-coreiam-identity` header BEFORE
|
|
415
|
+
// any pass-through. getSession() (actions.ts) trusts this header verbatim,
|
|
416
|
+
// and on unauthenticated paths the original request headers flow straight
|
|
417
|
+
// into Server Components — a forged header manufactured an identity there.
|
|
418
|
+
// Only branches that complete verification re-attach it, to this copy.
|
|
419
|
+
const sanitizedHeaders = new Headers(request.headers);
|
|
420
|
+
sanitizedHeaders.delete('x-coreiam-identity');
|
|
352
421
|
// Now delegates to CoreIAM.verifySession — the same code path Express
|
|
353
422
|
// users get, so the two implementations can never drift.
|
|
354
423
|
const jwtCookie = request.cookies.get("jwt");
|
|
@@ -358,7 +427,7 @@ async function coreIamAuth(request) {
|
|
|
358
427
|
if (client) {
|
|
359
428
|
const result = await client.verifySession(request.headers);
|
|
360
429
|
if (result.identity) {
|
|
361
|
-
const requestHeaders = new Headers(
|
|
430
|
+
const requestHeaders = new Headers(sanitizedHeaders);
|
|
362
431
|
requestHeaders.set('x-coreiam-identity', JSON.stringify(result.identity));
|
|
363
432
|
return {
|
|
364
433
|
response: server_1.NextResponse.next({ request: { headers: requestHeaders } }),
|
|
@@ -380,33 +449,56 @@ async function coreIamAuth(request) {
|
|
|
380
449
|
}
|
|
381
450
|
const result = await refreshInFlight;
|
|
382
451
|
if (result) {
|
|
383
|
-
const requestHeaders = new Headers(
|
|
452
|
+
const requestHeaders = new Headers(sanitizedHeaders);
|
|
384
453
|
requestHeaders.set('cookie', `jwt=${result.jwt}; refreshToken=${result.refreshToken}`);
|
|
385
454
|
requestHeaders.set('Authorization', `Bearer ${result.jwt}`);
|
|
386
|
-
try {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
455
|
+
// try {
|
|
456
|
+
// const payloadB64 = result.jwt.split('.')[1];
|
|
457
|
+
// const payloadStr = Buffer.from(payloadB64, 'base64').toString('utf8');
|
|
458
|
+
// const payload = JSON.parse(payloadStr);
|
|
459
|
+
// requestHeaders.set('x-coreiam-identity', JSON.stringify({
|
|
460
|
+
// sub: payload.sub,
|
|
461
|
+
// email: payload.email,
|
|
462
|
+
// tenantId: payload.tenantId,
|
|
463
|
+
// roles: payload.roles || []
|
|
464
|
+
// }));
|
|
465
|
+
// } catch {}
|
|
466
|
+
// // FIX: forward requestHeaders (fresh cookie + identity) — NOT sanitizedHeaders,
|
|
467
|
+
// // which still carries the old expired cookie.
|
|
468
|
+
// const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
469
|
+
// const isProd = process.env.NODE_ENV === 'production';
|
|
470
|
+
// response.cookies.set('jwt', result.jwt, { httpOnly: true, secure: isProd, sameSite: 'strict', maxAge: 24 * 60 * 60 });
|
|
471
|
+
// response.cookies.set('refreshToken', result.refreshToken, { httpOnly: true, secure: isProd, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 });
|
|
472
|
+
// return { response, isAuthenticated: true };
|
|
473
|
+
// FIX (SDK-12): don't decode the JWT by hand. Buffer isn't available in
|
|
474
|
+
// the Edge runtime (Next middleware's default) — that throw was silently
|
|
475
|
+
// swallowed by the empty catch, leaving x-coreiam-identity missing on Edge.
|
|
476
|
+
// Verifying through the same funnel everything else uses fixes that and
|
|
477
|
+
// keeps ONE verification implementation. verifySession accepts a
|
|
478
|
+
// cookie-header string.
|
|
479
|
+
const verified = await client?.verifySession(`jwt=${result.jwt}`);
|
|
480
|
+
if (verified?.identity) {
|
|
481
|
+
requestHeaders.set('x-coreiam-identity', JSON.stringify(verified.identity));
|
|
396
482
|
}
|
|
397
|
-
|
|
483
|
+
// FIX: forward requestHeaders (fresh cookie + identity) — NOT sanitizedHeaders,
|
|
484
|
+
// which still carries the old expired cookie.
|
|
398
485
|
const response = server_1.NextResponse.next({ request: { headers: requestHeaders } });
|
|
399
486
|
const isProd = process.env.NODE_ENV === 'production';
|
|
400
|
-
|
|
401
|
-
|
|
487
|
+
// FIX (SDK-11): Max-Age derived from the backend's own Set-Cookie (see
|
|
488
|
+
// performRefresh) instead of hardcoded 24h/7d, and sameSite 'lax' to match
|
|
489
|
+
// the link-confirm handler — previously the only lax/strict mismatch.
|
|
490
|
+
response.cookies.set('jwt', result.jwt, { httpOnly: true, secure: isProd, sameSite: 'lax', maxAge: result.jwtMaxAge });
|
|
491
|
+
response.cookies.set('refreshToken', result.refreshToken, { httpOnly: true, secure: isProd, sameSite: 'lax', maxAge: result.refreshMaxAge });
|
|
402
492
|
return { response, isAuthenticated: true };
|
|
403
493
|
}
|
|
404
|
-
|
|
494
|
+
// FIX (SDK-9): failure branch must forward sanitizedHeaders too — bare next()
|
|
495
|
+
// passes the original headers through, re-opening the forged-header hole.
|
|
496
|
+
const response = server_1.NextResponse.next({ request: { headers: sanitizedHeaders } });
|
|
405
497
|
response.cookies.delete('jwt');
|
|
406
498
|
response.cookies.delete('refreshToken');
|
|
407
499
|
return { response, isAuthenticated: false };
|
|
408
500
|
}
|
|
409
|
-
return { response: server_1.NextResponse.next(), isAuthenticated: false };
|
|
501
|
+
return { response: server_1.NextResponse.next({ request: { headers: sanitizedHeaders } }), isAuthenticated: false };
|
|
410
502
|
}
|
|
411
503
|
/**
|
|
412
504
|
* 1-line middleware factory for developers who don't need custom routing.
|
|
@@ -416,7 +508,10 @@ function coreIamMiddleware(options = {}) {
|
|
|
416
508
|
return async (request) => {
|
|
417
509
|
const { response, isAuthenticated } = await coreIamAuth(request);
|
|
418
510
|
const pathname = request.nextUrl.pathname;
|
|
419
|
-
const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route));
|
|
511
|
+
// const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route));
|
|
512
|
+
// FIX (SDK-15): exact-segment prefix match — '/blog' must not also make
|
|
513
|
+
// '/blog-admin' public. Tolerates routes declared with a trailing slash.
|
|
514
|
+
const isPublicRoute = publicRoutes.some(route => pathname === route || pathname.startsWith(route.endsWith('/') ? route : route + '/'));
|
|
420
515
|
// If not authenticated and trying to access a protected route
|
|
421
516
|
if (!isAuthenticated && !isPublicRoute) {
|
|
422
517
|
const redirect = server_1.NextResponse.redirect(new URL(signInUrl, request.url));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type
|
|
2
|
+
import { type CoreIAMIdentity } from '../core';
|
|
3
3
|
interface AuthContextType {
|
|
4
4
|
identity: CoreIAMIdentity | null;
|
|
5
5
|
isLoading: boolean;
|
|
@@ -21,6 +21,22 @@ interface AuthContextType {
|
|
|
21
21
|
error?: string;
|
|
22
22
|
}>;
|
|
23
23
|
refetchIdentity: () => Promise<void>;
|
|
24
|
+
requestOtp: (email: string) => Promise<{
|
|
25
|
+
success?: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}>;
|
|
28
|
+
verifyOtp: (email: string, otp: string) => Promise<{
|
|
29
|
+
success?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
forgotPassword: (email: string) => Promise<{
|
|
33
|
+
success?: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
}>;
|
|
36
|
+
resetPassword: (email: string, otp: string, newPassword: string) => Promise<{
|
|
37
|
+
success?: boolean;
|
|
38
|
+
error?: string;
|
|
39
|
+
}>;
|
|
24
40
|
}
|
|
25
41
|
export declare function CoreIAMProvider({ children, initialIdentity }: {
|
|
26
42
|
children: React.ReactNode;
|