@coreiam/auth 1.0.15 → 1.0.391
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 +83 -5
- package/dist/core/index.js +157 -9
- package/dist/express/index.d.ts +26 -0
- package/dist/express/index.js +171 -0
- package/dist/nextjs/actions.js +49 -6
- package/dist/nextjs/index.d.ts +11 -4
- package/dist/nextjs/index.js +96 -6
- package/dist/nextjs/middleware.d.ts +10 -3
- package/dist/nextjs/middleware.js +372 -42
- package/dist/nextjs/provider.d.ts +13 -22
- package/dist/nextjs/provider.js +103 -183
- package/dist/react/provider.d.ts +2 -1
- package/dist/react/provider.js +55 -8
- package/dist/ui/buttons.d.ts +45 -0
- package/dist/ui/buttons.js +76 -0
- package/package.json +21 -4
package/dist/core/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { type JWTPayload } from 'jose';
|
|
1
2
|
export interface CoreIAMConfig {
|
|
2
3
|
baseUrl?: string;
|
|
3
4
|
apiKey?: string;
|
|
4
5
|
tenantId?: string;
|
|
6
|
+
/** Expected token issuer. Default: 'coreiam'. */
|
|
7
|
+
issuer?: string;
|
|
8
|
+
/** Optional strict audience check — off by default, matches current gateway tokens. */
|
|
9
|
+
audience?: string;
|
|
10
|
+
/** Session cookie name. Default: 'jwt'. */
|
|
11
|
+
sessionCookieName?: string;
|
|
12
|
+
/** JWKS path on the gateway. Default: '/.well-known/jwks.json'. */
|
|
13
|
+
jwksPath?: string;
|
|
5
14
|
}
|
|
6
15
|
export interface CoreIAMIdentity {
|
|
7
16
|
sub: string;
|
|
@@ -10,14 +19,83 @@ export interface CoreIAMIdentity {
|
|
|
10
19
|
roles?: string[];
|
|
11
20
|
[key: string]: any;
|
|
12
21
|
}
|
|
22
|
+
/** Accepts a Headers instance, an Express req.headers object, or a raw Cookie string. */
|
|
23
|
+
export type SessionInput = Headers | Record<string, string | string[] | undefined> | string;
|
|
24
|
+
export type SessionFailureReason = 'no_cookie' | 'expired' | 'invalid_claims' | 'invalid_signature' | 'jwks_unavailable';
|
|
25
|
+
export interface SessionResult {
|
|
26
|
+
identity: CoreIAMIdentity | null;
|
|
27
|
+
reason?: SessionFailureReason;
|
|
28
|
+
}
|
|
29
|
+
export interface RefreshSessionResult {
|
|
30
|
+
identity: CoreIAMIdentity | null;
|
|
31
|
+
/** Raw Set-Cookie strings to apply on your response (rotated jwt/refreshToken). */
|
|
32
|
+
setCookies: string[] | null;
|
|
33
|
+
}
|
|
34
|
+
/** Maps gateway token claims → CoreIAMIdentity (same shape the Next.js middleware injects). */
|
|
35
|
+
export declare function toIdentity(payload: JWTPayload): CoreIAMIdentity;
|
|
36
|
+
/**
|
|
37
|
+
export class CoreIAM {
|
|
38
|
+
private apiKey: string = '';
|
|
39
|
+
private baseUrl: string = '';
|
|
40
|
+
|
|
41
|
+
constructor(config?: { apiKey?: string; baseUrl?: string }) {
|
|
42
|
+
this.apiKey = config?.apiKey || process.env.COREIAM_API_KEY || '';
|
|
43
|
+
// FIX: Removed hardcoded URL. Throw if missing.
|
|
44
|
+
this.baseUrl = config?.baseUrl || process.env.COREIAM_API_URL || '';
|
|
45
|
+
|
|
46
|
+
if (!this.apiKey) throw new Error('CoreIAM: Missing API Key (COREIAM_API_KEY)');
|
|
47
|
+
if (!this.baseUrl) throw new Error('CoreIAM: Missing Gateway URL (COREIAM_API_URL)');
|
|
48
|
+
}
|
|
49
|
+
*/
|
|
13
50
|
export declare class CoreIAM {
|
|
14
51
|
private apiKey;
|
|
15
52
|
private baseUrl;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
53
|
+
private issuer;
|
|
54
|
+
private audience?;
|
|
55
|
+
private sessionCookieName;
|
|
56
|
+
private jwksPath;
|
|
57
|
+
private jwks;
|
|
58
|
+
private refreshInFlight;
|
|
59
|
+
constructor(config?: CoreIAMConfig);
|
|
60
|
+
private getJwks;
|
|
61
|
+
/** Optional warm-up: call at boot so the first verification pays no JWKS latency. */
|
|
62
|
+
preloadJwks(): Promise<void>;
|
|
63
|
+
private readCookieHeader;
|
|
64
|
+
private readSessionCookie;
|
|
65
|
+
verifySession(input: SessionInput): Promise<SessionResult>;
|
|
66
|
+
getSession(input: SessionInput): Promise<CoreIAMIdentity | null>;
|
|
67
|
+
/**
|
|
68
|
+
* One-call silent refresh for non-Next servers: posts the refresh cookie
|
|
69
|
+
* to the gateway, verifies the rotated access token, and returns the new
|
|
70
|
+
* Set-Cookie strings so the caller can forward them on its response.
|
|
71
|
+
*/
|
|
72
|
+
refreshSession(input: SessionInput): Promise<RefreshSessionResult>;
|
|
73
|
+
refreshTokens(input: SessionInput): Promise<string[] | null>;
|
|
74
|
+
private doRefresh;
|
|
75
|
+
/**
|
|
76
|
+
async refreshTokens(headers: Headers): Promise<string[] | null> {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${this.baseUrl}/auth/refresh-token`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'X-API-Key': this.apiKey,
|
|
82
|
+
'cookie': headers.get('cookie') || '',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
const setCookies = response.headers.getSetCookie();
|
|
88
|
+
if (setCookies && setCookies.length > 0) {
|
|
89
|
+
return setCookies;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
*/
|
|
21
99
|
proxy(path: string, init: RequestInit, incomingHeaders: Headers): Promise<Response>;
|
|
22
100
|
getCsrfToken(headers: Headers): Promise<Response>;
|
|
23
101
|
login(body: any, headers: Headers): Promise<Response>;
|
package/dist/core/index.js
CHANGED
|
@@ -1,41 +1,189 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/core/index.ts
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.CoreIAM = void 0;
|
|
4
|
+
exports.toIdentity = toIdentity;
|
|
5
|
+
// src/core/index.ts
|
|
6
|
+
const jose_1 = require("jose");
|
|
7
|
+
/** Maps gateway token claims → CoreIAMIdentity (same shape the Next.js middleware injects). */
|
|
8
|
+
function toIdentity(payload) {
|
|
9
|
+
return {
|
|
10
|
+
sub: payload.sub,
|
|
11
|
+
email: payload.email,
|
|
12
|
+
tenantId: payload.tenantId,
|
|
13
|
+
roles: Array.isArray(payload.roles) ? payload.roles : [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
export class CoreIAM {
|
|
18
|
+
private apiKey: string = '';
|
|
19
|
+
private baseUrl: string = '';
|
|
20
|
+
|
|
21
|
+
constructor(config?: { apiKey?: string; baseUrl?: string }) {
|
|
22
|
+
this.apiKey = config?.apiKey || process.env.COREIAM_API_KEY || '';
|
|
23
|
+
// FIX: Removed hardcoded URL. Throw if missing.
|
|
24
|
+
this.baseUrl = config?.baseUrl || process.env.COREIAM_API_URL || '';
|
|
25
|
+
|
|
26
|
+
if (!this.apiKey) throw new Error('CoreIAM: Missing API Key (COREIAM_API_KEY)');
|
|
27
|
+
if (!this.baseUrl) throw new Error('CoreIAM: Missing Gateway URL (COREIAM_API_URL)');
|
|
28
|
+
}
|
|
29
|
+
*/
|
|
5
30
|
class CoreIAM {
|
|
6
31
|
apiKey = '';
|
|
7
32
|
baseUrl = '';
|
|
33
|
+
issuer = 'coreiam';
|
|
34
|
+
audience;
|
|
35
|
+
sessionCookieName = 'jwt';
|
|
36
|
+
jwksPath = '/.well-known/jwks.json';
|
|
37
|
+
jwks = null;
|
|
38
|
+
refreshInFlight = null;
|
|
8
39
|
constructor(config) {
|
|
9
40
|
this.apiKey = config?.apiKey || process.env.COREIAM_API_KEY || '';
|
|
10
|
-
|
|
11
|
-
this.
|
|
41
|
+
this.baseUrl = (config?.baseUrl || process.env.COREIAM_API_URL || '').replace(/\/+$/, '');
|
|
42
|
+
this.issuer = config?.issuer ?? 'coreiam';
|
|
43
|
+
this.audience = config?.audience;
|
|
44
|
+
this.sessionCookieName = config?.sessionCookieName ?? 'jwt';
|
|
45
|
+
this.jwksPath = config?.jwksPath ?? '/.well-known/jwks.json';
|
|
12
46
|
if (!this.apiKey)
|
|
13
47
|
throw new Error('CoreIAM: Missing API Key (COREIAM_API_KEY)');
|
|
14
48
|
if (!this.baseUrl)
|
|
15
49
|
throw new Error('CoreIAM: Missing Gateway URL (COREIAM_API_URL)');
|
|
16
50
|
}
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
getJwks() {
|
|
52
|
+
// Lazy singleton: jose fetches once, caches, and handles key rotation.
|
|
53
|
+
this.jwks ??= (0, jose_1.createRemoteJWKSet)(new URL(`${this.baseUrl}${this.jwksPath}`), {
|
|
54
|
+
cooldownDuration: 30_000,
|
|
55
|
+
cacheMaxAge: 10 * 60_000,
|
|
56
|
+
});
|
|
57
|
+
return this.jwks;
|
|
58
|
+
}
|
|
59
|
+
/** Optional warm-up: call at boot so the first verification pays no JWKS latency. */
|
|
60
|
+
async preloadJwks() {
|
|
61
|
+
try {
|
|
62
|
+
// Deliberately look up a key that won't exist — jose fetches and
|
|
63
|
+
// caches the whole key set on the first lookup, which is the warm-up.
|
|
64
|
+
await this.getJwks()({ alg: 'RS256', kid: '__preload__' }, {});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* expected: no matching key — the fetch is the point */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
readCookieHeader(input) {
|
|
71
|
+
if (typeof input === 'string')
|
|
72
|
+
return input;
|
|
73
|
+
if (typeof input.get === 'function')
|
|
74
|
+
return input.get('cookie') || '';
|
|
75
|
+
return input.cookie || '';
|
|
76
|
+
}
|
|
77
|
+
readSessionCookie(input) {
|
|
78
|
+
const raw = this.readCookieHeader(input);
|
|
79
|
+
if (!raw)
|
|
80
|
+
return null;
|
|
81
|
+
// Anchored to a real cookie-name boundary (same pattern proxy() uses),
|
|
82
|
+
// so a cookie named "notjwt" or "xjwt" can never match.
|
|
83
|
+
const escaped = this.sessionCookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
const match = raw.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]+)`));
|
|
85
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
86
|
+
}
|
|
87
|
+
async verifySession(input) {
|
|
88
|
+
const token = this.readSessionCookie(input);
|
|
89
|
+
if (!token)
|
|
90
|
+
return { identity: null, reason: 'no_cookie' };
|
|
91
|
+
try {
|
|
92
|
+
const { payload } = await (0, jose_1.jwtVerify)(token, this.getJwks(), {
|
|
93
|
+
algorithms: ['RS256'], // pinned — blocks alg confusion
|
|
94
|
+
issuer: this.issuer,
|
|
95
|
+
...(this.audience ? { audience: this.audience } : {}),
|
|
96
|
+
});
|
|
97
|
+
return { identity: toIdentity(payload) };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof jose_1.errors.JWTExpired)
|
|
101
|
+
return { identity: null, reason: 'expired' };
|
|
102
|
+
if (error instanceof jose_1.errors.JWTClaimValidationFailed)
|
|
103
|
+
return { identity: null, reason: 'invalid_claims' };
|
|
104
|
+
// JOSE errors = the token itself is bad. Anything else (fetch failure
|
|
105
|
+
// while getting the JWKS) = we couldn't check — that is not a sign-out.
|
|
106
|
+
if (error instanceof jose_1.errors.JOSEError)
|
|
107
|
+
return { identity: null, reason: 'invalid_signature' };
|
|
108
|
+
return { identity: null, reason: 'jwks_unavailable' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async getSession(input) {
|
|
112
|
+
return (await this.verifySession(input)).identity;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One-call silent refresh for non-Next servers: posts the refresh cookie
|
|
116
|
+
* to the gateway, verifies the rotated access token, and returns the new
|
|
117
|
+
* Set-Cookie strings so the caller can forward them on its response.
|
|
118
|
+
*/
|
|
119
|
+
async refreshSession(input) {
|
|
120
|
+
const setCookies = await this.refreshTokens(input);
|
|
121
|
+
if (!setCookies)
|
|
122
|
+
return { identity: null, setCookies: null };
|
|
123
|
+
const rotated = setCookies
|
|
124
|
+
.find(c => c.startsWith(`${this.sessionCookieName}=`))
|
|
125
|
+
?.split(';')[0]; // "jwt=<new value>" — a valid single-cookie header string
|
|
126
|
+
if (rotated) {
|
|
127
|
+
const { identity } = await this.verifySession(rotated);
|
|
128
|
+
if (identity)
|
|
129
|
+
return { identity, setCookies };
|
|
130
|
+
}
|
|
131
|
+
return { identity: null, setCookies };
|
|
132
|
+
}
|
|
133
|
+
async refreshTokens(input) {
|
|
134
|
+
// Single-flight: concurrent requests share one gateway refresh call
|
|
135
|
+
// (matters with rotating refresh tokens). Input widened from Headers
|
|
136
|
+
// to SessionInput — backwards compatible.
|
|
137
|
+
this.refreshInFlight ??= this.doRefresh(input).finally(() => {
|
|
138
|
+
this.refreshInFlight = null;
|
|
139
|
+
});
|
|
140
|
+
return this.refreshInFlight;
|
|
141
|
+
}
|
|
142
|
+
async doRefresh(input) {
|
|
19
143
|
try {
|
|
20
144
|
const response = await fetch(`${this.baseUrl}/auth/refresh-token`, {
|
|
21
145
|
method: 'POST',
|
|
22
146
|
headers: {
|
|
23
147
|
'X-API-Key': this.apiKey,
|
|
24
|
-
'cookie':
|
|
148
|
+
'cookie': this.readCookieHeader(input),
|
|
25
149
|
},
|
|
26
150
|
});
|
|
27
151
|
if (response.ok) {
|
|
28
152
|
const setCookies = response.headers.getSetCookie();
|
|
29
|
-
if (setCookies && setCookies.length > 0)
|
|
153
|
+
if (setCookies && setCookies.length > 0)
|
|
30
154
|
return setCookies;
|
|
31
|
-
}
|
|
32
155
|
}
|
|
33
156
|
return null;
|
|
34
157
|
}
|
|
35
|
-
catch
|
|
158
|
+
catch {
|
|
36
159
|
return null;
|
|
37
160
|
}
|
|
38
161
|
}
|
|
162
|
+
// Exposed publicly so the Next.js layer can use it for getSession()
|
|
163
|
+
/**
|
|
164
|
+
async refreshTokens(headers: Headers): Promise<string[] | null> {
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(`${this.baseUrl}/auth/refresh-token`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'X-API-Key': this.apiKey,
|
|
170
|
+
'cookie': headers.get('cookie') || '',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (response.ok) {
|
|
175
|
+
const setCookies = response.headers.getSetCookie();
|
|
176
|
+
if (setCookies && setCookies.length > 0) {
|
|
177
|
+
return setCookies;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
*/
|
|
39
187
|
async proxy(path, init, incomingHeaders) {
|
|
40
188
|
const url = `${this.baseUrl}${path}`;
|
|
41
189
|
const headers = new Headers(init.headers);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Router, type RequestHandler } from 'express';
|
|
2
|
+
import { type CoreIAMConfig } from '../core';
|
|
3
|
+
export interface CoreIamExpressOptions extends CoreIAMConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Public URL of the app as the user's browser reaches it (e.g. http://localhost:5173
|
|
6
|
+
* in Vite dev, or your production domain). /api/auth must route from this origin
|
|
7
|
+
* to this Express server (reverse proxy in prod, Vite dev proxy in dev).
|
|
8
|
+
*/
|
|
9
|
+
appUrl: string;
|
|
10
|
+
/** Mount path of the auth router. Default: '/api/auth'. */
|
|
11
|
+
authPath?: string;
|
|
12
|
+
/** Redirect target after successful OAuth login. Default: '/'. */
|
|
13
|
+
successUrl?: string;
|
|
14
|
+
/** Redirect target when OAuth initiation fails. Default: '/login'. */
|
|
15
|
+
signInUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function coreIamExpress(options: CoreIamExpressOptions): Router;
|
|
18
|
+
/**
|
|
19
|
+
* Route guard: verifies the session cookie locally and puts the identity on
|
|
20
|
+
* res.locals.identity. 401s API requests; redirects page requests.
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireAuth(config?: CoreIAMConfig & {
|
|
23
|
+
signInUrl?: string;
|
|
24
|
+
}): RequestHandler;
|
|
25
|
+
export { CoreIAM } from '../core';
|
|
26
|
+
export type { CoreIAMIdentity } from '../core';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoreIAM = void 0;
|
|
4
|
+
exports.coreIamExpress = coreIamExpress;
|
|
5
|
+
exports.requireAuth = requireAuth;
|
|
6
|
+
// src/express/index.ts
|
|
7
|
+
const express_1 = require("express");
|
|
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
|
+
/** Express 4 does not catch async rejections — route them to next(err). */
|
|
15
|
+
const wrap = (fn) => (req, res, next) => { fn(req, res, next).catch(next); };
|
|
16
|
+
function sessionCookie(name, value, maxAgeSec, isProd) {
|
|
17
|
+
return `${name}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSec}${isProd ? '; Secure' : ''}`;
|
|
18
|
+
}
|
|
19
|
+
function coreIamExpress(options) {
|
|
20
|
+
const appUrl = options.appUrl.replace(/\/+$/, '');
|
|
21
|
+
const authPath = (options.authPath ?? '/api/auth').replace(/\/+$/, '');
|
|
22
|
+
const successUrl = options.successUrl ?? '/';
|
|
23
|
+
const signInUrl = options.signInUrl ?? '/login';
|
|
24
|
+
const gatewayUrl = (options.baseUrl || process.env.COREIAM_API_URL || '').replace(/\/+$/, '');
|
|
25
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
26
|
+
const iam = new core_1.CoreIAM(options);
|
|
27
|
+
const router = (0, express_1.Router)();
|
|
28
|
+
// Body parsing for JSON + the urlencoded link-confirm post.
|
|
29
|
+
// Skips automatically if the host app already parsed the body.
|
|
30
|
+
router.use((0, express_1.json)());
|
|
31
|
+
router.use((0, express_1.urlencoded)({ extended: false }));
|
|
32
|
+
const toHeaders = (req) => new Headers(req.headers);
|
|
33
|
+
// ---------------------------------------------------------------
|
|
34
|
+
// GET {authPath}/session — matches the React provider's default
|
|
35
|
+
// sessionUrl ('/api/auth/session'). Local JWKS verify + silent refresh.
|
|
36
|
+
// ---------------------------------------------------------------
|
|
37
|
+
router.get('/session', wrap(async (req, res) => {
|
|
38
|
+
const result = await iam.verifySession(req.headers);
|
|
39
|
+
if (result.identity)
|
|
40
|
+
return void res.json(result.identity);
|
|
41
|
+
if (result.reason === 'expired') {
|
|
42
|
+
const { identity, setCookies } = await iam.refreshSession(req.headers);
|
|
43
|
+
if (identity) {
|
|
44
|
+
setCookies?.forEach(c => res.append('Set-Cookie', c));
|
|
45
|
+
return void res.json(identity);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
res.status(401).json({ error: 'unauthenticated' });
|
|
49
|
+
}));
|
|
50
|
+
// ---------------------------------------------------------------
|
|
51
|
+
// OAuth initiate: GET {authPath}/oauth/:provider
|
|
52
|
+
// ---------------------------------------------------------------
|
|
53
|
+
router.get('/oauth/:provider', wrap(async (req, res, next) => {
|
|
54
|
+
const provider = req.params.provider;
|
|
55
|
+
if (provider === 'link')
|
|
56
|
+
return next(); // not an initiate route
|
|
57
|
+
const redirectUri = `${appUrl}${authPath}/callback/${provider}`;
|
|
58
|
+
const target = `/auth/oauth/initiate/${provider}?redirectUri=${encodeURIComponent(redirectUri)}&scopes=email,profile`;
|
|
59
|
+
const response = await iam.proxy(target, { method: 'GET' }, toHeaders(req));
|
|
60
|
+
const data = await response.json().catch(() => ({}));
|
|
61
|
+
if (data.url)
|
|
62
|
+
return void res.redirect(302, data.url);
|
|
63
|
+
res.redirect(302, signInUrl);
|
|
64
|
+
}));
|
|
65
|
+
// ---------------------------------------------------------------
|
|
66
|
+
// OAuth callback: GET {authPath}/callback/:provider
|
|
67
|
+
// ---------------------------------------------------------------
|
|
68
|
+
router.get('/callback/:provider', wrap(async (req, res) => {
|
|
69
|
+
const provider = req.params.provider;
|
|
70
|
+
const code = req.query.code;
|
|
71
|
+
const state = req.query.state;
|
|
72
|
+
const redirectUri = `${appUrl}${authPath}/callback/${provider}`;
|
|
73
|
+
const target = `/auth/oauth/callback/${provider}?code=${code}&state=${state}&redirectUri=${encodeURIComponent(redirectUri)}`;
|
|
74
|
+
const response = await iam.proxy(target, { method: 'GET' }, toHeaders(req));
|
|
75
|
+
const data = await response.json().catch(() => ({}));
|
|
76
|
+
// Account linking: redirect to CoreIAM's hosted confirmation page
|
|
77
|
+
if (data.action === 'link_candidate') {
|
|
78
|
+
const redirect = new URL(`${gatewayUrl}/auth/hosted/confirm-link`);
|
|
79
|
+
redirect.searchParams.set('token', data.tempToken);
|
|
80
|
+
redirect.searchParams.set('provider', data.provider);
|
|
81
|
+
redirect.searchParams.set('email', data.email || '');
|
|
82
|
+
redirect.searchParams.set('appOrigin', appUrl);
|
|
83
|
+
return void res.redirect(303, redirect.toString());
|
|
84
|
+
}
|
|
85
|
+
// Normal OAuth login: forward the gateway's session cookies, then redirect.
|
|
86
|
+
// (Set-Cookie must be appended BEFORE res.redirect — Express sends immediately.)
|
|
87
|
+
response.headers.getSetCookie().forEach(c => res.append('Set-Cookie', c));
|
|
88
|
+
res.redirect(302, successUrl);
|
|
89
|
+
}));
|
|
90
|
+
// ---------------------------------------------------------------
|
|
91
|
+
// Account-link confirmation: POST {authPath}/oauth/link/confirm
|
|
92
|
+
// (the hosted page posts tempToken + password back here)
|
|
93
|
+
// ---------------------------------------------------------------
|
|
94
|
+
router.post('/oauth/link/confirm', wrap(async (req, res) => {
|
|
95
|
+
const formData = (req.body ?? {});
|
|
96
|
+
const response = await iam.proxy('/auth/oauth/link/confirm', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: JSON.stringify({ tempToken: formData.tempToken, password: formData.password }),
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
}, toHeaders(req));
|
|
101
|
+
const data = await response.json().catch(() => ({}));
|
|
102
|
+
if (response.ok && data.success) {
|
|
103
|
+
const finalUrl = new URL(data.redirectUri || '/', appUrl).toString();
|
|
104
|
+
// Same cookie attributes the Next.js handler uses.
|
|
105
|
+
if (data.accessToken)
|
|
106
|
+
res.append('Set-Cookie', sessionCookie('jwt', data.accessToken, 24 * 60 * 60, isProd));
|
|
107
|
+
if (data.clientRefreshToken)
|
|
108
|
+
res.append('Set-Cookie', sessionCookie('refreshToken', data.clientRefreshToken, 7 * 24 * 60 * 60, isProd));
|
|
109
|
+
// 200 + JS redirect breaks the cross-site redirect chain so the browser
|
|
110
|
+
// sends the freshly-set (SameSite=Lax) cookies on the next hop.
|
|
111
|
+
return void res.status(200).type('html').send(`<!DOCTYPE html>
|
|
112
|
+
<html>
|
|
113
|
+
<head><title>Linking Successful</title></head>
|
|
114
|
+
<body>
|
|
115
|
+
<p>Account linked successfully! Redirecting...</p>
|
|
116
|
+
<script>window.location.href = ${JSON.stringify(finalUrl)};</script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>`);
|
|
119
|
+
}
|
|
120
|
+
// Failed (wrong password) -> back to the hosted page with the error
|
|
121
|
+
const errorRedirect = new URL(`${gatewayUrl}/auth/hosted/confirm-link`);
|
|
122
|
+
errorRedirect.searchParams.set('token', formData.tempToken);
|
|
123
|
+
errorRedirect.searchParams.set('provider', data.provider || 'google');
|
|
124
|
+
errorRedirect.searchParams.set('email', data.email || '');
|
|
125
|
+
errorRedirect.searchParams.set('appOrigin', appUrl);
|
|
126
|
+
errorRedirect.searchParams.set('error', data.message || 'Invalid password. Link failed.');
|
|
127
|
+
res.redirect(303, errorRedirect.toString());
|
|
128
|
+
}));
|
|
129
|
+
// ---------------------------------------------------------------
|
|
130
|
+
// Everything else: generic JSON passthrough (login, register, logout,
|
|
131
|
+
// csrf-token, MFA, OTP, profile...). CSRF-protected endpoints run the
|
|
132
|
+
// handshake first, same as the Next.js server actions.
|
|
133
|
+
// ---------------------------------------------------------------
|
|
134
|
+
router.use(wrap(async (req, res) => {
|
|
135
|
+
const target = `/auth${req.path}`; // /login -> /auth/login
|
|
136
|
+
const init = { method: req.method };
|
|
137
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
138
|
+
init.body = req.body ? JSON.stringify(req.body) : undefined;
|
|
139
|
+
init.headers = { 'Content-Type': 'application/json' };
|
|
140
|
+
}
|
|
141
|
+
const response = CSRF_PROTECTED.has(target)
|
|
142
|
+
? await iam.proxyWithCsrfProtection(target, init, toHeaders(req))
|
|
143
|
+
: await iam.proxy(target, init, toHeaders(req));
|
|
144
|
+
const data = await response.json().catch(() => ({}));
|
|
145
|
+
res.status(response.status);
|
|
146
|
+
response.headers.getSetCookie().forEach(c => res.append('Set-Cookie', c));
|
|
147
|
+
res.json(data);
|
|
148
|
+
}));
|
|
149
|
+
return router;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Route guard: verifies the session cookie locally and puts the identity on
|
|
153
|
+
* res.locals.identity. 401s API requests; redirects page requests.
|
|
154
|
+
*/
|
|
155
|
+
function requireAuth(config) {
|
|
156
|
+
const iam = new core_1.CoreIAM(config);
|
|
157
|
+
const { signInUrl = '/login' } = config ?? {};
|
|
158
|
+
return wrap(async (req, res, next) => {
|
|
159
|
+
const identity = await iam.getSession(req.headers);
|
|
160
|
+
if (!identity) {
|
|
161
|
+
if (req.path.startsWith('/api') || req.accepts(['html', 'json']) === 'json') {
|
|
162
|
+
return void res.status(401).json({ error: 'unauthenticated' });
|
|
163
|
+
}
|
|
164
|
+
return void res.redirect(302, signInUrl);
|
|
165
|
+
}
|
|
166
|
+
res.locals.identity = identity;
|
|
167
|
+
next();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
var core_2 = require("../core");
|
|
171
|
+
Object.defineProperty(exports, "CoreIAM", { enumerable: true, get: function () { return core_2.CoreIAM; } });
|
package/dist/nextjs/actions.js
CHANGED
|
@@ -189,16 +189,59 @@ async function syncCookiesToNext(response) {
|
|
|
189
189
|
// cookies. If middleware already refreshed upstream, this succeeds; if
|
|
190
190
|
// middleware's refresh also failed, returning null here is correct, not a
|
|
191
191
|
// bug to work around.
|
|
192
|
+
// export async function getSession(): Promise<CoreIAMIdentity | null> {
|
|
193
|
+
// console.log(`[DEBUG_TRACE] getSession() real fetch @ ${new Date().toISOString()}`);
|
|
194
|
+
// const reqHeaders = await headers();
|
|
195
|
+
// const response = await getIam().proxy('/auth/profile', { method: 'GET' }, reqHeaders);
|
|
196
|
+
// if (!response.ok) return null;
|
|
197
|
+
// const coreIamIdentity = await response.json();
|
|
198
|
+
// if (!coreIamIdentity?.sub) return null;
|
|
199
|
+
// return coreIamIdentity as CoreIAMIdentity;
|
|
200
|
+
// }
|
|
192
201
|
async function getSession() {
|
|
193
|
-
console.log(`[DEBUG_TRACE] getSession() real fetch @ ${new Date().toISOString()}`);
|
|
194
202
|
const reqHeaders = await (0, headers_1.headers)();
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
// The middleware verified the JWT and attached the payload to the request headers
|
|
204
|
+
const identityHeader = reqHeaders.get('x-coreiam-identity');
|
|
205
|
+
if (identityHeader) {
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(identityHeader);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Fallback: If middleware didn't run (e.g. API route), we can still read the JWT
|
|
214
|
+
// But we DO NOT verify it here, because verification requires the JWKS fetch which
|
|
215
|
+
// is too slow for Server Components. We just decode it to get the identity.
|
|
216
|
+
// The middleware is the true security gate.
|
|
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)
|
|
197
229
|
return null;
|
|
198
|
-
|
|
199
|
-
|
|
230
|
+
try {
|
|
231
|
+
// Just decode the payload without verifying (middleware already verified it)
|
|
232
|
+
const payloadB64 = token.split('.')[1];
|
|
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
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
200
243
|
return null;
|
|
201
|
-
|
|
244
|
+
}
|
|
202
245
|
}
|
|
203
246
|
async function loginActionSdk(identifier, password) {
|
|
204
247
|
const reqHeaders = await (0, headers_1.headers)();
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
export { CoreIAMProvider, useAuth } from './provider';
|
|
2
|
-
export { loginActionSdk, registerActionSdk, logoutActionSdk, getSession } from './actions';
|
|
3
|
-
export { coreIamMiddleware } from './middleware';
|
|
4
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
5
|
-
import { CoreIAM } from '../core';
|
|
2
|
+
import { CoreIAM, CoreIAMIdentity } from '../core';
|
|
3
|
+
export { CoreIAMProvider, useAuth } from './provider';
|
|
4
|
+
export { coreIamMiddleware, coreIamAuth } from './middleware';
|
|
5
|
+
export { getSession, loginActionSdk, registerActionSdk, logoutActionSdk, updateProfileActionSdk, setupMfaActionSdk, verifyMfaActionSdk, disableMfaActionSdk, requestOtpActionSdk, verifyOtpActionSdk } from './actions';
|
|
6
6
|
export declare class NextHandlers {
|
|
7
7
|
private iam;
|
|
8
8
|
constructor(iam?: CoreIAM);
|
|
9
9
|
handleProxy(req: NextRequest, internalPath: string): Promise<NextResponse<any>>;
|
|
10
10
|
}
|
|
11
|
+
export declare class OAuthHandlers {
|
|
12
|
+
private iam;
|
|
13
|
+
constructor(iam?: CoreIAM);
|
|
14
|
+
handleInitiate(req: NextRequest, provider: string): Promise<NextResponse<unknown>>;
|
|
15
|
+
handleCallback(req: NextRequest, provider: string): Promise<NextResponse<unknown>>;
|
|
16
|
+
}
|
|
17
|
+
export declare function auth(request: NextRequest): Promise<CoreIAMIdentity | null>;
|