@coreiam/auth 1.0.392-beta.0 → 1.0.392-beta.11
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 +53 -3
- package/dist/core/index.js +149 -10
- package/dist/express/index.js +7 -7
- 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 +125 -30
- 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 +26 -6
package/dist/core/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface RefreshSessionResult {
|
|
|
41
41
|
}
|
|
42
42
|
/** Maps gateway token claims → CoreIAMIdentity (same shape the Next.js middleware injects). */
|
|
43
43
|
export declare function toIdentity(payload: JWTPayload): CoreIAMIdentity;
|
|
44
|
+
export declare function retryAfterSeconds(body: unknown): number | undefined;
|
|
44
45
|
/**
|
|
45
46
|
export class CoreIAM {
|
|
46
47
|
private apiKey: string = '';
|
|
@@ -73,9 +74,14 @@ export declare class CoreIAM {
|
|
|
73
74
|
* this instance's API key. Server-to-server, read-only.
|
|
74
75
|
* THROWS on failure — an audit must never silently see an empty roster.
|
|
75
76
|
*/
|
|
76
|
-
listIdentities(
|
|
77
|
+
listIdentities(params?: {
|
|
78
|
+
limit?: number;
|
|
79
|
+
offset?: number;
|
|
80
|
+
roles?: string[];
|
|
81
|
+
}): Promise<{
|
|
77
82
|
users: DirectoryIdentity[];
|
|
78
83
|
total: number;
|
|
84
|
+
next_offset?: number;
|
|
79
85
|
}>;
|
|
80
86
|
private readCookieHeader;
|
|
81
87
|
private readSessionCookie;
|
|
@@ -131,9 +137,53 @@ export declare class CoreIAM {
|
|
|
131
137
|
loginWithCsrfProtection(identifier: string, password: string, incomingHeaders: Headers): Promise<Response>;
|
|
132
138
|
registerWithCsrfProtection(userData: any, incomingHeaders: Headers): Promise<Response>;
|
|
133
139
|
updateProfile(updateData: any, incomingHeaders: Headers): Promise<Response>;
|
|
140
|
+
changePassword(currentPassword: string, newPassword: string, incomingHeaders: Headers): Promise<Response>;
|
|
134
141
|
setupMfa(incomingHeaders: Headers): Promise<Response>;
|
|
135
142
|
verifyMfa(token: string, incomingHeaders: Headers): Promise<Response>;
|
|
136
143
|
disableMfa(incomingHeaders: Headers): Promise<Response>;
|
|
137
|
-
requestOtp(body:
|
|
138
|
-
verifyOtp(body:
|
|
144
|
+
requestOtp(body: RequestOtpBody, incomingHeaders: Headers): Promise<Response>;
|
|
145
|
+
verifyOtp(body: VerifyOtpBody, incomingHeaders: Headers): Promise<Response>;
|
|
146
|
+
forgotPassword(email: string, incomingHeaders: Headers): Promise<Response>;
|
|
147
|
+
verifyEmail(body: VerifyEmailBody, incomingHeaders: Headers): Promise<Response>;
|
|
148
|
+
resendVerification(email: string, incomingHeaders: Headers): Promise<Response>;
|
|
149
|
+
resetPassword(email: string, otp: string, newPassword: string, incomingHeaders: Headers): Promise<Response>;
|
|
150
|
+
listSessions(incomingHeaders: Headers): Promise<Response>;
|
|
151
|
+
revokeSession(sessionId: string, incomingHeaders: Headers): Promise<Response>;
|
|
152
|
+
revokeAllSessions(incomingHeaders: Headers): Promise<Response>;
|
|
153
|
+
}
|
|
154
|
+
export declare const CSRF_PROTECTED_PATHS: ReadonlySet<string>;
|
|
155
|
+
export declare const OAUTH_PROVIDERS: ReadonlySet<string>;
|
|
156
|
+
export interface RequestOtpBody {
|
|
157
|
+
email: string;
|
|
158
|
+
}
|
|
159
|
+
export interface VerifyOtpBody {
|
|
160
|
+
email: string;
|
|
161
|
+
otp: string;
|
|
162
|
+
}
|
|
163
|
+
export interface VerifyEmailBody {
|
|
164
|
+
email: string;
|
|
165
|
+
otp: string;
|
|
166
|
+
}
|
|
167
|
+
export interface RequestResetBody {
|
|
168
|
+
email: string;
|
|
169
|
+
}
|
|
170
|
+
export interface ResetPasswordBody {
|
|
171
|
+
email: string;
|
|
172
|
+
otp: string;
|
|
173
|
+
newPassword: string;
|
|
174
|
+
}
|
|
175
|
+
export interface SessionSummary {
|
|
176
|
+
id: string;
|
|
177
|
+
userAgent: string;
|
|
178
|
+
ipAddress: string;
|
|
179
|
+
createdAt: string;
|
|
180
|
+
lastActiveAt: string;
|
|
181
|
+
isCurrent: boolean;
|
|
182
|
+
}
|
|
183
|
+
export interface ListSessionsResponse {
|
|
184
|
+
sessions: SessionSummary[];
|
|
185
|
+
}
|
|
186
|
+
export interface RevokeAllSessionsResponse {
|
|
187
|
+
success: boolean;
|
|
188
|
+
sessionsRevoked: number;
|
|
139
189
|
}
|
package/dist/core/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CoreIAM = void 0;
|
|
3
|
+
exports.OAUTH_PROVIDERS = exports.CSRF_PROTECTED_PATHS = exports.CoreIAM = void 0;
|
|
4
4
|
exports.toIdentity = toIdentity;
|
|
5
|
+
exports.retryAfterSeconds = retryAfterSeconds;
|
|
5
6
|
// src/core/index.ts
|
|
6
7
|
const jose_1 = require("jose");
|
|
7
8
|
/** Maps gateway token claims → CoreIAMIdentity (same shape the Next.js middleware injects). */
|
|
@@ -13,6 +14,17 @@ function toIdentity(payload) {
|
|
|
13
14
|
roles: Array.isArray(payload.roles) ? payload.roles : [],
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
function retryAfterSeconds(body) {
|
|
18
|
+
const msg = body?.message;
|
|
19
|
+
for (const m of Array.isArray(msg) ? msg : [msg]) {
|
|
20
|
+
if (typeof m === 'string') {
|
|
21
|
+
const match = m.match(/Try again in (\d+) seconds?/);
|
|
22
|
+
if (match)
|
|
23
|
+
return Number(match[1]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
16
28
|
/**
|
|
17
29
|
export class CoreIAM {
|
|
18
30
|
private apiKey: string = '';
|
|
@@ -72,14 +84,30 @@ class CoreIAM {
|
|
|
72
84
|
* this instance's API key. Server-to-server, read-only.
|
|
73
85
|
* THROWS on failure — an audit must never silently see an empty roster.
|
|
74
86
|
*/
|
|
75
|
-
async listIdentities() {
|
|
76
|
-
const
|
|
87
|
+
async listIdentities(params) {
|
|
88
|
+
const search = new URLSearchParams();
|
|
89
|
+
if (params?.limit != null)
|
|
90
|
+
search.set('limit', String(params.limit));
|
|
91
|
+
if (params?.offset != null)
|
|
92
|
+
search.set('offset', String(params.offset));
|
|
93
|
+
if (params?.roles?.length)
|
|
94
|
+
search.set('roles', params.roles.join(','));
|
|
95
|
+
const qs = search.toString();
|
|
96
|
+
const response = await this.proxy(`/api/v1/directory${qs ? `?${qs}` : ''}`, { method: 'GET' }, new Headers());
|
|
77
97
|
if (!response.ok) {
|
|
78
98
|
const body = await response.text().catch(() => '');
|
|
79
99
|
throw new Error(`listIdentities failed: ${response.status} ${body}`);
|
|
80
100
|
}
|
|
81
|
-
const data = await response.json();
|
|
82
|
-
|
|
101
|
+
const data = await response.json().catch(() => null);
|
|
102
|
+
if (!data || !Array.isArray(data.users)) {
|
|
103
|
+
throw new Error(`listIdentities: unexpected response shape — HTTP 200 without a users array. ` +
|
|
104
|
+
`This is not an empty directory; verify COREIAM_API_URL points at the CoreIAM gateway.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
users: data.users,
|
|
108
|
+
total: typeof data.total === 'number' ? data.total : data.users.length,
|
|
109
|
+
...(typeof data.next_offset === 'number' ? { next_offset: data.next_offset } : {}),
|
|
110
|
+
};
|
|
83
111
|
}
|
|
84
112
|
readCookieHeader(input) {
|
|
85
113
|
if (typeof input === 'string')
|
|
@@ -88,15 +116,33 @@ class CoreIAM {
|
|
|
88
116
|
return input.get('cookie') || '';
|
|
89
117
|
return input.cookie || '';
|
|
90
118
|
}
|
|
119
|
+
// private readSessionCookie(input: SessionInput): string | null {
|
|
120
|
+
// const raw = this.readCookieHeader(input);
|
|
121
|
+
// if (!raw) return null;
|
|
122
|
+
// // Anchored to a real cookie-name boundary (same pattern proxy() uses),
|
|
123
|
+
// // so a cookie named "notjwt" or "xjwt" can never match.
|
|
124
|
+
// const escaped = this.sessionCookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
125
|
+
// const match = raw.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]+)`));
|
|
126
|
+
// return match ? decodeURIComponent(match[1]) : null;
|
|
127
|
+
// }
|
|
128
|
+
// PATCH 1 — readSessionCookie: a corrupt cookie value (%E0%A4%A) currently
|
|
129
|
+
// throws URIError out of verifySession → 500 on /session. A corrupt value is
|
|
130
|
+
// an invalid token, not a server error: return it raw; jwtVerify classifies
|
|
131
|
+
// it as 'invalid_signature'.
|
|
91
132
|
readSessionCookie(input) {
|
|
92
133
|
const raw = this.readCookieHeader(input);
|
|
93
134
|
if (!raw)
|
|
94
135
|
return null;
|
|
95
|
-
// Anchored to a real cookie-name boundary (same pattern proxy() uses),
|
|
96
|
-
// so a cookie named "notjwt" or "xjwt" can never match.
|
|
97
136
|
const escaped = this.sessionCookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
98
137
|
const match = raw.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]+)`));
|
|
99
|
-
|
|
138
|
+
if (!match)
|
|
139
|
+
return null;
|
|
140
|
+
try {
|
|
141
|
+
return decodeURIComponent(match[1]);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return match[1];
|
|
145
|
+
}
|
|
100
146
|
}
|
|
101
147
|
async verifySession(input) {
|
|
102
148
|
const token = this.readSessionCookie(input);
|
|
@@ -305,8 +351,22 @@ class CoreIAM {
|
|
|
305
351
|
const existingCookieStr = incomingHeaders.get('cookie') || '';
|
|
306
352
|
const newCookiePairs = csrfCookies.map(c => c.split(';')[0]);
|
|
307
353
|
if (newCookiePairs.length > 0) {
|
|
308
|
-
|
|
309
|
-
|
|
354
|
+
// FIX (SDK-4): dedupe by cookie NAME, newest value wins. The old
|
|
355
|
+
// [existing, ...new].join() emitted 'csrfToken=old; csrfToken=new' once
|
|
356
|
+
// the browser jar already held a csrf cookie and the handshake rotated
|
|
357
|
+
// it — whether the backend parser reads the first or last duplicate
|
|
358
|
+
// decided 403-or-pass. Nondeterministic across parsers is a bug.
|
|
359
|
+
const jar = new Map();
|
|
360
|
+
for (const pair of [...existingCookieStr.split(';'), ...newCookiePairs]) {
|
|
361
|
+
const trimmed = pair.trim();
|
|
362
|
+
if (!trimmed)
|
|
363
|
+
continue;
|
|
364
|
+
const eq = trimmed.indexOf('=');
|
|
365
|
+
if (eq === -1)
|
|
366
|
+
continue;
|
|
367
|
+
jar.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
|
|
368
|
+
}
|
|
369
|
+
authHeaders.set('cookie', [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; '));
|
|
310
370
|
}
|
|
311
371
|
return this.proxy(path, init, authHeaders);
|
|
312
372
|
}
|
|
@@ -331,6 +391,13 @@ class CoreIAM {
|
|
|
331
391
|
body: JSON.stringify(updateData),
|
|
332
392
|
}, incomingHeaders);
|
|
333
393
|
}
|
|
394
|
+
async changePassword(currentPassword, newPassword, incomingHeaders) {
|
|
395
|
+
return this.proxyWithCsrfProtection('/auth/password', {
|
|
396
|
+
method: 'PUT',
|
|
397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
398
|
+
body: JSON.stringify({ currentPassword, newPassword }),
|
|
399
|
+
}, incomingHeaders);
|
|
400
|
+
}
|
|
334
401
|
async setupMfa(incomingHeaders) {
|
|
335
402
|
return this.proxyWithCsrfProtection('/auth/mfa/setup', { method: 'POST' }, incomingHeaders);
|
|
336
403
|
}
|
|
@@ -358,5 +425,77 @@ class CoreIAM {
|
|
|
358
425
|
body: JSON.stringify(body),
|
|
359
426
|
}, incomingHeaders);
|
|
360
427
|
}
|
|
428
|
+
// W3 beta.6: forgot-password / reset-password. Both go through the same
|
|
429
|
+
// CSRF funnel as every other CsrfGuard-decorated endpoint — the paths are
|
|
430
|
+
// pre-registered in CSRF_PROTECTED_PATHS (SDK-6), so the middleware proxy
|
|
431
|
+
// and these methods stay in parity.
|
|
432
|
+
async forgotPassword(email, incomingHeaders) {
|
|
433
|
+
return this.proxyWithCsrfProtection('/auth/request-reset', {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: { 'Content-Type': 'application/json' },
|
|
436
|
+
body: JSON.stringify({ email }),
|
|
437
|
+
}, incomingHeaders);
|
|
438
|
+
}
|
|
439
|
+
async verifyEmail(body, incomingHeaders) {
|
|
440
|
+
return this.proxyWithCsrfProtection('/auth/verify-email', {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: { 'Content-Type': 'application/json' },
|
|
443
|
+
body: JSON.stringify(body),
|
|
444
|
+
}, incomingHeaders);
|
|
445
|
+
}
|
|
446
|
+
async resendVerification(email, incomingHeaders) {
|
|
447
|
+
return this.proxyWithCsrfProtection('/auth/resend-verification', {
|
|
448
|
+
method: 'POST',
|
|
449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
450
|
+
body: JSON.stringify({ email }),
|
|
451
|
+
}, incomingHeaders);
|
|
452
|
+
}
|
|
453
|
+
async resetPassword(email, otp, newPassword, incomingHeaders) {
|
|
454
|
+
return this.proxyWithCsrfProtection('/auth/reset-password', {
|
|
455
|
+
method: 'POST',
|
|
456
|
+
headers: { 'Content-Type': 'application/json' },
|
|
457
|
+
body: JSON.stringify({ email, otp, newPassword }),
|
|
458
|
+
}, incomingHeaders);
|
|
459
|
+
}
|
|
460
|
+
// W4 (beta.7): session management surface. listSessions is a plain
|
|
461
|
+
// authenticated GET (same shape as the /auth/profile GET precedent —
|
|
462
|
+
// JWT comes from the forwarded cookie via proxy(), reads carry no CSRF
|
|
463
|
+
// requirement); the two state-changing calls go through the shared CSRF
|
|
464
|
+
// handshake exactly like changePassword.
|
|
465
|
+
async listSessions(incomingHeaders) {
|
|
466
|
+
return this.proxy('/auth/sessions', { method: 'GET' }, incomingHeaders);
|
|
467
|
+
}
|
|
468
|
+
async revokeSession(sessionId, incomingHeaders) {
|
|
469
|
+
// Same rationale as OAUTH_PROVIDERS (SDK-5): never interpolate an
|
|
470
|
+
// unvalidated path segment into a URL. The backend's ParseUUIDPipe
|
|
471
|
+
// would reject garbage with a 400 anyway — this fails fast
|
|
472
|
+
// client-side and can never become a traversal string.
|
|
473
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
474
|
+
throw new Error('revokeSession: sessionId must be a UUID');
|
|
475
|
+
}
|
|
476
|
+
return this.proxyWithCsrfProtection(`/auth/sessions/${sessionId}`, { method: 'DELETE' }, incomingHeaders);
|
|
477
|
+
}
|
|
478
|
+
async revokeAllSessions(incomingHeaders) {
|
|
479
|
+
return this.proxyWithCsrfProtection('/auth/sessions/revoke-all', { method: 'POST' }, incomingHeaders);
|
|
480
|
+
}
|
|
361
481
|
}
|
|
362
482
|
exports.CoreIAM = CoreIAM;
|
|
483
|
+
exports.CSRF_PROTECTED_PATHS = new Set([
|
|
484
|
+
'/auth/login', '/auth/register', '/auth/discover-tenants',
|
|
485
|
+
'/auth/request-otp', '/auth/verify-otp', '/auth/mfa/login', '/auth/verify-email', '/auth/resend-verification',
|
|
486
|
+
'/auth/profile', '/auth/mfa/setup', '/auth/mfa/verify', '/auth/mfa/disable',
|
|
487
|
+
'/auth/password',
|
|
488
|
+
'/auth/request-reset', '/auth/reset-password',
|
|
489
|
+
// W4 (beta.7): revoke-all is exact-matched. The per-session revoke is a
|
|
490
|
+
// DYNAMIC path (/auth/sessions/<uuid>) a string Set cannot match — the SDK
|
|
491
|
+
// methods self-handshake via proxyWithCsrfProtection so server-action
|
|
492
|
+
// callers are fully covered; middleware-proxied browser-direct DELETEs to
|
|
493
|
+
// /api/auth/sessions/<uuid> are not auto-handshaked (known limitation; the
|
|
494
|
+
// fix is a prefix matcher inside middleware.ts, deliberately deferred to
|
|
495
|
+
// keep this patch off that file).
|
|
496
|
+
'/auth/sessions/revoke-all',
|
|
497
|
+
]);
|
|
498
|
+
// FIX (SDK-5): single source of truth for supported OAuth providers. Every
|
|
499
|
+
// place that interpolates a provider segment into a backend URL must check
|
|
500
|
+
// this set first — an unvalidated path segment is a traversal surface.
|
|
501
|
+
exports.OAUTH_PROVIDERS = new Set(['google', 'github']);
|
package/dist/express/index.js
CHANGED
|
@@ -6,11 +6,6 @@ exports.requireAuth = requireAuth;
|
|
|
6
6
|
// src/express/index.ts
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const core_1 = require("../core");
|
|
9
|
-
const CSRF_PROTECTED = new Set([
|
|
10
|
-
'/auth/login', '/auth/register', '/auth/profile',
|
|
11
|
-
'/auth/mfa/setup', '/auth/mfa/verify', '/auth/mfa/disable',
|
|
12
|
-
'/auth/request-otp', '/auth/verify-otp',
|
|
13
|
-
]);
|
|
14
9
|
/** Express 4 does not catch async rejections — route them to next(err). */
|
|
15
10
|
const wrap = (fn) => (req, res, next) => { fn(req, res, next).catch(next); };
|
|
16
11
|
function sessionCookie(name, value, maxAgeSec, isProd) {
|
|
@@ -70,7 +65,12 @@ function coreIamExpress(options) {
|
|
|
70
65
|
const code = req.query.code;
|
|
71
66
|
const state = req.query.state;
|
|
72
67
|
const redirectUri = `${appUrl}${authPath}/callback/${provider}`;
|
|
73
|
-
const target = `/auth/oauth/callback/${provider}?code=${code}&state=${state}&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
68
|
+
// const target = `/auth/oauth/callback/${provider}?code=${code}&state=${state}&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
69
|
+
// OAuth callback — encode all three params:
|
|
70
|
+
const target = `/auth/oauth/callback/${provider}`
|
|
71
|
+
+ `?code=${encodeURIComponent(String(code ?? ''))}`
|
|
72
|
+
+ `&state=${encodeURIComponent(String(state ?? ''))}`
|
|
73
|
+
+ `&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
74
74
|
const response = await iam.proxy(target, { method: 'GET' }, toHeaders(req));
|
|
75
75
|
const data = await response.json().catch(() => ({}));
|
|
76
76
|
// Account linking: redirect to CoreIAM's hosted confirmation page
|
|
@@ -138,7 +138,7 @@ function coreIamExpress(options) {
|
|
|
138
138
|
init.body = req.body ? JSON.stringify(req.body) : undefined;
|
|
139
139
|
init.headers = { 'Content-Type': 'application/json' };
|
|
140
140
|
}
|
|
141
|
-
const response =
|
|
141
|
+
const response = core_1.CSRF_PROTECTED_PATHS.has(target)
|
|
142
142
|
? await iam.proxyWithCsrfProtection(target, init, toHeaders(req))
|
|
143
143
|
: await iam.proxy(target, init, toHeaders(req));
|
|
144
144
|
const data = await response.json().catch(() => ({}));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from 'hono';
|
|
2
|
+
import { CoreIAMIdentity } from '../core';
|
|
3
|
+
export interface CoreIamHonoOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** Your app's public URL (OAuth callbacks redirect back here). */
|
|
7
|
+
appUrl: string;
|
|
8
|
+
/** Mount path of the adapter. MUST match how you mount it. Default '/api/auth'. */
|
|
9
|
+
authPath?: string;
|
|
10
|
+
signInUrl?: string;
|
|
11
|
+
successUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Pass to your app: `new Hono<CoreIAMEnv>()` so `c.get('identity')` is typed. */
|
|
14
|
+
export type CoreIAMEnv = {
|
|
15
|
+
Variables: {
|
|
16
|
+
identity: CoreIAMIdentity;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Drop-in auth router for Hono (Node, Bun, Deno, Workers).
|
|
21
|
+
* const app = new Hono();
|
|
22
|
+
* app.route('/api/auth', coreIamHono({ apiKey, baseUrl, appUrl }));
|
|
23
|
+
* Same behavior as the Express adapter: CSRF handshake, OAuth + account
|
|
24
|
+
* linking, cookie forwarding, silent refresh on /session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function coreIamHono(options: CoreIamHonoOptions): Hono;
|
|
27
|
+
export interface RequireAuthConfig {
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
signInUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Route guard: local verify → c.get('identity'); 401s API requests, redirects pages. */
|
|
33
|
+
export declare function requireAuth(config?: RequireAuthConfig): MiddlewareHandler<CoreIAMEnv>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coreIamHono = coreIamHono;
|
|
4
|
+
exports.requireAuth = requireAuth;
|
|
5
|
+
// src/hono/index.ts
|
|
6
|
+
const hono_1 = require("hono");
|
|
7
|
+
const core_1 = require("../core");
|
|
8
|
+
/**
|
|
9
|
+
* Drop-in auth router for Hono (Node, Bun, Deno, Workers).
|
|
10
|
+
* const app = new Hono();
|
|
11
|
+
* app.route('/api/auth', coreIamHono({ apiKey, baseUrl, appUrl }));
|
|
12
|
+
* Same behavior as the Express adapter: CSRF handshake, OAuth + account
|
|
13
|
+
* linking, cookie forwarding, silent refresh on /session.
|
|
14
|
+
*/
|
|
15
|
+
function coreIamHono(options) {
|
|
16
|
+
const appUrl = options.appUrl.replace(/\/+$/, '');
|
|
17
|
+
const authPath = (options.authPath ?? '/api/auth').replace(/\/+$/, '');
|
|
18
|
+
const successUrl = options.successUrl ?? '/';
|
|
19
|
+
const signInUrl = options.signInUrl ?? '/login';
|
|
20
|
+
const gatewayUrl = options.baseUrl.replace(/\/+$/, '');
|
|
21
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
22
|
+
const iam = new core_1.CoreIAM(options);
|
|
23
|
+
const app = new hono_1.Hono();
|
|
24
|
+
// c.req.path is always the FULL path (Hono does not rewrite it per sub-app),
|
|
25
|
+
// so the mount prefix must be stripped by hand to get '/login' → '/auth/login'.
|
|
26
|
+
const relPath = (c) => {
|
|
27
|
+
const p = c.req.path;
|
|
28
|
+
if (p === authPath)
|
|
29
|
+
return '/';
|
|
30
|
+
if (p.startsWith(`${authPath}/`))
|
|
31
|
+
return p.slice(authPath.length);
|
|
32
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
33
|
+
console.warn(`[coreIamHono] ${p} does not start with authPath '${authPath}' — ` +
|
|
34
|
+
`if you mounted the adapter elsewhere, pass the same value as authPath.`);
|
|
35
|
+
}
|
|
36
|
+
return p;
|
|
37
|
+
};
|
|
38
|
+
// Same attributes as the Express adapter's sessionCookie().
|
|
39
|
+
const sessionCookie = (name, value, maxAgeSec) => `${name}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSec}${isProd ? '; Secure' : ''}`;
|
|
40
|
+
// Mirrors the Express adapter (json() + urlencoded()): both content types
|
|
41
|
+
// accepted on every auth route, re-serialized as JSON for the gateway.
|
|
42
|
+
const readBody = async (c) => {
|
|
43
|
+
if ((c.req.header('content-type') || '').includes('application/x-www-form-urlencoded')) {
|
|
44
|
+
return Object.fromEntries(new URLSearchParams(await c.req.text()).entries());
|
|
45
|
+
}
|
|
46
|
+
return c.req.json().catch(() => undefined);
|
|
47
|
+
};
|
|
48
|
+
// ── CORS preflight short-circuit — same as the Next.js middleware, so ──
|
|
49
|
+
// cross-origin frontends work with zero setup. Same-origin (the documented
|
|
50
|
+
// pattern) never sends OPTIONS.
|
|
51
|
+
app.options('*', (c) => {
|
|
52
|
+
const origin = c.req.header('origin');
|
|
53
|
+
if (origin) {
|
|
54
|
+
c.header('Access-Control-Allow-Origin', origin);
|
|
55
|
+
c.header('Access-Control-Allow-Credentials', 'true');
|
|
56
|
+
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
57
|
+
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token');
|
|
58
|
+
c.header('Vary', 'Origin', { append: true });
|
|
59
|
+
}
|
|
60
|
+
return c.body(null, 204);
|
|
61
|
+
});
|
|
62
|
+
// ── GET {authPath}/session — local verify + silent refresh (React provider) ──
|
|
63
|
+
app.get('/session', async (c) => {
|
|
64
|
+
const result = await iam.verifySession(c.req.raw.headers);
|
|
65
|
+
if (result.identity)
|
|
66
|
+
return c.json(result.identity);
|
|
67
|
+
if (result.reason === 'expired') {
|
|
68
|
+
const { identity, setCookies } = await iam.refreshSession(c.req.raw.headers);
|
|
69
|
+
if (identity) {
|
|
70
|
+
setCookies?.forEach((sc) => c.header('Set-Cookie', sc, { append: true }));
|
|
71
|
+
return c.json(identity);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
75
|
+
});
|
|
76
|
+
// ── GET {authPath}/oauth/:provider — initiate ──
|
|
77
|
+
app.get('/oauth/:provider', async (c, next) => {
|
|
78
|
+
const provider = c.req.param('provider');
|
|
79
|
+
if (provider === 'link')
|
|
80
|
+
return next(); // link/confirm is POSTed below
|
|
81
|
+
const redirectUri = `${appUrl}${authPath}/callback/${provider}`;
|
|
82
|
+
const target = `/auth/oauth/initiate/${provider}?redirectUri=${encodeURIComponent(redirectUri)}&scopes=email,profile`;
|
|
83
|
+
const response = await iam.proxy(target, { method: 'GET' }, c.req.raw.headers);
|
|
84
|
+
const data = await response.json().catch(() => ({}));
|
|
85
|
+
if (data.url)
|
|
86
|
+
return c.redirect(data.url, 302);
|
|
87
|
+
return c.redirect(signInUrl, 302);
|
|
88
|
+
});
|
|
89
|
+
// ── GET {authPath}/callback/:provider ──
|
|
90
|
+
app.get('/callback/:provider', async (c) => {
|
|
91
|
+
const provider = c.req.param('provider');
|
|
92
|
+
const code = c.req.query('code');
|
|
93
|
+
const state = c.req.query('state');
|
|
94
|
+
const redirectUri = `${appUrl}${authPath}/callback/${provider}`;
|
|
95
|
+
const target = `/auth/oauth/callback/${provider}`
|
|
96
|
+
+ `?code=${encodeURIComponent(code ?? '')}`
|
|
97
|
+
+ `&state=${encodeURIComponent(state ?? '')}`
|
|
98
|
+
+ `&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
99
|
+
const response = await iam.proxy(target, { method: 'GET' }, c.req.raw.headers);
|
|
100
|
+
const data = (await response.json().catch(() => ({})));
|
|
101
|
+
// Account linking → CoreIAM's hosted confirmation page
|
|
102
|
+
if (data.action === 'link_candidate') {
|
|
103
|
+
const redirect = new URL(`${gatewayUrl}/auth/hosted/confirm-link`);
|
|
104
|
+
redirect.searchParams.set('token', data.tempToken);
|
|
105
|
+
redirect.searchParams.set('provider', data.provider);
|
|
106
|
+
redirect.searchParams.set('email', data.email || '');
|
|
107
|
+
redirect.searchParams.set('appOrigin', appUrl);
|
|
108
|
+
return c.redirect(redirect.toString(), 303);
|
|
109
|
+
}
|
|
110
|
+
// Normal login: forward gateway cookies, then redirect.
|
|
111
|
+
response.headers.getSetCookie().forEach((sc) => c.header('Set-Cookie', sc, { append: true }));
|
|
112
|
+
return c.redirect(successUrl, 302);
|
|
113
|
+
});
|
|
114
|
+
// ── POST {authPath}/oauth/link/confirm (hosted page posts urlencoded) ──
|
|
115
|
+
app.post('/oauth/link/confirm', async (c) => {
|
|
116
|
+
const contentType = c.req.header('content-type') || '';
|
|
117
|
+
let formData;
|
|
118
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
119
|
+
const params = new URLSearchParams(await c.req.text());
|
|
120
|
+
formData = { tempToken: params.get('tempToken') ?? undefined, password: params.get('password') ?? undefined };
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
formData = (await c.req.json().catch(() => ({})));
|
|
124
|
+
}
|
|
125
|
+
const response = await iam.proxy('/auth/oauth/link/confirm', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
body: JSON.stringify(formData),
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
}, c.req.raw.headers);
|
|
130
|
+
const data = (await response.json().catch(() => ({})));
|
|
131
|
+
if (response.ok && data.success) {
|
|
132
|
+
const finalUrl = new URL(data.redirectUri || '/', appUrl).toString();
|
|
133
|
+
if (data.accessToken)
|
|
134
|
+
c.header('Set-Cookie', sessionCookie('jwt', data.accessToken, 24 * 60 * 60), { append: true });
|
|
135
|
+
if (data.clientRefreshToken)
|
|
136
|
+
c.header('Set-Cookie', sessionCookie('refreshToken', data.clientRefreshToken, 7 * 24 * 60 * 60), { append: true });
|
|
137
|
+
// 200 + JS redirect — NOT a 303: breaks the cross-site redirect chain so
|
|
138
|
+
// the browser sends the freshly-set SameSite=Lax cookies on the next hop.
|
|
139
|
+
// (Identical to the Express adapter and Next middleware — do not "simplify".)
|
|
140
|
+
return c.html(`<!DOCTYPE html>
|
|
141
|
+
<html>
|
|
142
|
+
<head><title>Linking Successful</title></head>
|
|
143
|
+
<body>
|
|
144
|
+
<p>Account linked successfully! Redirecting...</p>
|
|
145
|
+
<script>window.location.href = ${JSON.stringify(finalUrl)};</script>
|
|
146
|
+
</body>
|
|
147
|
+
</html>`);
|
|
148
|
+
}
|
|
149
|
+
const errorRedirect = new URL(`${gatewayUrl}/auth/hosted/confirm-link`);
|
|
150
|
+
errorRedirect.searchParams.set('token', formData.tempToken || '');
|
|
151
|
+
errorRedirect.searchParams.set('provider', data.provider || 'google');
|
|
152
|
+
errorRedirect.searchParams.set('email', data.email || '');
|
|
153
|
+
errorRedirect.searchParams.set('appOrigin', appUrl);
|
|
154
|
+
errorRedirect.searchParams.set('error', data.message || 'Invalid password. Link failed.');
|
|
155
|
+
return c.redirect(errorRedirect.toString(), 303);
|
|
156
|
+
});
|
|
157
|
+
// ── Everything else: generic JSON passthrough (login, register, logout,
|
|
158
|
+
// csrf-token, MFA, OTP, profile). CSRF-protected endpoints run the
|
|
159
|
+
// handshake first — the same set, from core, as Express/Next. ──
|
|
160
|
+
app.all('*', async (c) => {
|
|
161
|
+
const target = `/auth${relPath(c)}`;
|
|
162
|
+
const init = { method: c.req.method };
|
|
163
|
+
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
|
|
164
|
+
const body = await readBody(c);
|
|
165
|
+
if (body !== undefined) {
|
|
166
|
+
init.body = JSON.stringify(body);
|
|
167
|
+
init.headers = { 'Content-Type': 'application/json' };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const response = core_1.CSRF_PROTECTED_PATHS.has(target)
|
|
171
|
+
? await iam.proxyWithCsrfProtection(target, init, c.req.raw.headers)
|
|
172
|
+
: await iam.proxy(target, init, c.req.raw.headers);
|
|
173
|
+
c.status(response.status);
|
|
174
|
+
response.headers.getSetCookie().forEach((sc) => c.header('Set-Cookie', sc, { append: true }));
|
|
175
|
+
return c.json(await response.json().catch(() => ({})));
|
|
176
|
+
});
|
|
177
|
+
return app;
|
|
178
|
+
}
|
|
179
|
+
/** Route guard: local verify → c.get('identity'); 401s API requests, redirects pages. */
|
|
180
|
+
function requireAuth(config = {}) {
|
|
181
|
+
const iam = new core_1.CoreIAM(config);
|
|
182
|
+
const { signInUrl = '/login' } = config;
|
|
183
|
+
return async (c, next) => {
|
|
184
|
+
const identity = await iam.getSession(c.req.raw.headers);
|
|
185
|
+
if (!identity) {
|
|
186
|
+
const accept = c.req.header('accept') || '';
|
|
187
|
+
const wantsJson = c.req.path.startsWith('/api') || accept.includes('json') || accept.trim() === '*/*';
|
|
188
|
+
if (wantsJson)
|
|
189
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
190
|
+
return c.redirect(signInUrl, 302);
|
|
191
|
+
}
|
|
192
|
+
c.set('identity', identity);
|
|
193
|
+
await next();
|
|
194
|
+
};
|
|
195
|
+
}
|
package/dist/nextjs/actions.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { CoreIAMIdentity } from '../core';
|
|
1
|
+
import { CoreIAMIdentity, RequestOtpBody, VerifyOtpBody, VerifyEmailBody } from '../core';
|
|
2
2
|
export interface AuthResult {
|
|
3
3
|
ok: boolean;
|
|
4
4
|
status: number;
|
|
5
|
+
retryAfter?: number;
|
|
5
6
|
data: any;
|
|
6
7
|
}
|
|
7
8
|
export declare function getSession(): Promise<CoreIAMIdentity | null>;
|
|
@@ -9,8 +10,16 @@ export declare function loginActionSdk(identifier: string, password: string): Pr
|
|
|
9
10
|
export declare function registerActionSdk(userData: any): Promise<AuthResult>;
|
|
10
11
|
export declare function logoutActionSdk(): Promise<AuthResult>;
|
|
11
12
|
export declare function updateProfileActionSdk(updateData: Record<string, any>): Promise<AuthResult>;
|
|
13
|
+
export declare function changePasswordActionSdk(currentPassword: string, newPassword: string): Promise<AuthResult>;
|
|
12
14
|
export declare function setupMfaActionSdk(): Promise<AuthResult>;
|
|
13
15
|
export declare function verifyMfaActionSdk(token: string): Promise<AuthResult>;
|
|
14
16
|
export declare function disableMfaActionSdk(): Promise<AuthResult>;
|
|
15
|
-
export declare function requestOtpActionSdk(body:
|
|
16
|
-
export declare function verifyOtpActionSdk(body:
|
|
17
|
+
export declare function requestOtpActionSdk(body: RequestOtpBody): Promise<AuthResult>;
|
|
18
|
+
export declare function verifyOtpActionSdk(body: VerifyOtpBody): Promise<AuthResult>;
|
|
19
|
+
export declare function verifyEmailActionSdk(body: VerifyEmailBody): Promise<AuthResult>;
|
|
20
|
+
export declare function resendVerificationActionSdk(email: string): Promise<AuthResult>;
|
|
21
|
+
export declare function forgotPasswordActionSdk(email: string): Promise<AuthResult>;
|
|
22
|
+
export declare function resetPasswordActionSdk(email: string, otp: string, newPassword: string): Promise<AuthResult>;
|
|
23
|
+
export declare function listSessionsActionSdk(): Promise<AuthResult>;
|
|
24
|
+
export declare function revokeSessionActionSdk(sessionId: string): Promise<AuthResult>;
|
|
25
|
+
export declare function revokeAllSessionsActionSdk(): Promise<AuthResult>;
|