@azlib/identity 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +387 -0
- package/dist/errors-BGMwaW5s.d.mts +481 -0
- package/dist/errors-BGMwaW5s.d.mts.map +1 -0
- package/dist/errors-Bcjx9o6g.cjs +111 -0
- package/dist/errors-C2xZAatu.d.cts +481 -0
- package/dist/errors-C2xZAatu.d.cts.map +1 -0
- package/dist/errors-CEmnZxIn.mjs +66 -0
- package/dist/errors-CEmnZxIn.mjs.map +1 -0
- package/dist/express.cjs +405 -0
- package/dist/express.d.cts +87 -0
- package/dist/express.d.cts.map +1 -0
- package/dist/express.d.mts +87 -0
- package/dist/express.d.mts.map +1 -0
- package/dist/express.mjs +401 -0
- package/dist/express.mjs.map +1 -0
- package/dist/identity-4eP45YIP.cjs +461 -0
- package/dist/identity-Bz9RDOvT.mjs +410 -0
- package/dist/identity-Bz9RDOvT.mjs.map +1 -0
- package/dist/identity-router-DBL20UWT.d.mts +115 -0
- package/dist/identity-router-DBL20UWT.d.mts.map +1 -0
- package/dist/identity-router-Dib30Waj.d.cts +115 -0
- package/dist/identity-router-Dib30Waj.d.cts.map +1 -0
- package/dist/identity-service-B9zrvE9z.d.mts +128 -0
- package/dist/identity-service-B9zrvE9z.d.mts.map +1 -0
- package/dist/identity-service-CLzKx8Z7.d.cts +128 -0
- package/dist/identity-service-CLzKx8Z7.d.cts.map +1 -0
- package/dist/identity-store-BRRahxcS.d.cts +272 -0
- package/dist/identity-store-BRRahxcS.d.cts.map +1 -0
- package/dist/identity-store-BRRahxcS.d.mts +272 -0
- package/dist/identity-store-BRRahxcS.d.mts.map +1 -0
- package/dist/index-CpufYgyn.d.cts +30 -0
- package/dist/index-CpufYgyn.d.cts.map +1 -0
- package/dist/index-CpufYgyn.d.mts +30 -0
- package/dist/index-CpufYgyn.d.mts.map +1 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +3 -0
- package/dist/logger-Be1wDzBC.cjs +48 -0
- package/dist/logger-CcCHJVVe.mjs +33 -0
- package/dist/logger-CcCHJVVe.mjs.map +1 -0
- package/dist/nestjs.cjs +516 -0
- package/dist/nestjs.d.cts +102 -0
- package/dist/nestjs.d.cts.map +1 -0
- package/dist/nestjs.d.mts +102 -0
- package/dist/nestjs.d.mts.map +1 -0
- package/dist/nestjs.mjs +500 -0
- package/dist/nestjs.mjs.map +1 -0
- package/dist/node.cjs +946 -0
- package/dist/node.d.cts +260 -0
- package/dist/node.d.cts.map +1 -0
- package/dist/node.d.mts +260 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +890 -0
- package/dist/node.mjs.map +1 -0
- package/dist/test-utils.cjs +169 -0
- package/dist/test-utils.d.cts +12 -0
- package/dist/test-utils.d.cts.map +1 -0
- package/dist/test-utils.d.mts +12 -0
- package/dist/test-utils.d.mts.map +1 -0
- package/dist/test-utils.mjs +170 -0
- package/dist/test-utils.mjs.map +1 -0
- package/package.json +92 -0
- package/schema/model.ts +100 -0
- package/schema/mysql.sql +102 -0
- package/schema/postgres.sql +92 -0
- package/schema/prisma.schema +122 -0
- package/schema/sqlite.sql +92 -0
package/dist/express.cjs
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("./errors-Bcjx9o6g.cjs");
|
|
3
|
+
const require_logger = require("./logger-Be1wDzBC.cjs");
|
|
4
|
+
let express = require("express");
|
|
5
|
+
//#region core/express/middleware.ts
|
|
6
|
+
const extractBearerToken = (req) => {
|
|
7
|
+
const header = req.headers.authorization;
|
|
8
|
+
if (!header || !header.startsWith("Bearer ")) return null;
|
|
9
|
+
const token = header.slice(7).trim();
|
|
10
|
+
return token.length > 0 ? token : null;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Express middleware that authenticates the request using a Bearer access token and
|
|
14
|
+
* attaches the hydrated principal to `req.identity`. Forwards identity errors to the
|
|
15
|
+
* error-handling middleware.
|
|
16
|
+
*/
|
|
17
|
+
function requireAuth(service) {
|
|
18
|
+
return async (req, _res, next) => {
|
|
19
|
+
try {
|
|
20
|
+
const token = extractBearerToken(req);
|
|
21
|
+
if (!token) throw new require_errors.UnauthenticatedError();
|
|
22
|
+
req.identity = await service.authenticate(token);
|
|
23
|
+
next();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
next(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Express middleware enforcing an authorization requirement (deny-by-default). Must run
|
|
31
|
+
* after {@link requireAuth}. Optionally loads a resource for ownership policies.
|
|
32
|
+
*/
|
|
33
|
+
function requireAuthorization(service, requirement, loadResource) {
|
|
34
|
+
return async (req, _res, next) => {
|
|
35
|
+
try {
|
|
36
|
+
const principal = req.identity;
|
|
37
|
+
if (!principal) throw new require_errors.UnauthenticatedError();
|
|
38
|
+
const resource = loadResource ? await loadResource(req) : void 0;
|
|
39
|
+
if (!(await service.authorize(requirement, {
|
|
40
|
+
principal,
|
|
41
|
+
action: requirement.permission ?? "custom",
|
|
42
|
+
resource
|
|
43
|
+
})).allowed) throw new require_errors.ForbiddenError();
|
|
44
|
+
next();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
next(error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Convenience: require a single permission. */
|
|
51
|
+
function requirePermission(service, permission) {
|
|
52
|
+
return requireAuthorization(service, { permission });
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region core/express/oauth-routes.ts
|
|
56
|
+
/**
|
|
57
|
+
* Returns an Express route handler that redirects the user to the OAuth provider's
|
|
58
|
+
* authorization page. Generates and persists a CSRF state value via `options.setState`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* app.get("/auth/google", oauthAuthorize(identity.oauth!, "google", {
|
|
63
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
64
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
65
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
66
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
67
|
+
* }));
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function oauthAuthorize(oauthService, providerName, options) {
|
|
71
|
+
return async (req, res, next) => {
|
|
72
|
+
try {
|
|
73
|
+
const { url, state } = oauthService.buildAuthorizationUrl(providerName, options.redirectUri);
|
|
74
|
+
await options.setState(req, res, state);
|
|
75
|
+
res.redirect(url);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
next(error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns an Express route handler that handles the OAuth provider callback. Verifies
|
|
83
|
+
* state, exchanges the code, and calls `options.onSuccess` with the auth result.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* app.get("/auth/google/callback", oauthCallback(identity.oauth!, "google", {
|
|
88
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
89
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
90
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
91
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
92
|
+
* }));
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function oauthCallback(oauthService, providerName, options) {
|
|
96
|
+
return async (req, res, next) => {
|
|
97
|
+
try {
|
|
98
|
+
const code = typeof req.query["code"] === "string" ? req.query["code"] : null;
|
|
99
|
+
const state = typeof req.query["state"] === "string" ? req.query["state"] : null;
|
|
100
|
+
const error = typeof req.query["error"] === "string" ? req.query["error"] : null;
|
|
101
|
+
if (error) throw new require_errors.IdentityError("identity/oauth-error", `Provider error: ${typeof req.query["error_description"] === "string" ? req.query["error_description"] : error}`, 400);
|
|
102
|
+
if (!code || !state) throw new require_errors.IdentityError("identity/oauth-invalid-callback", "Missing code or state in OAuth callback.", 400);
|
|
103
|
+
const expectedState = await options.getState(req);
|
|
104
|
+
if (!expectedState) throw new require_errors.IdentityError("identity/oauth-state-missing", "No OAuth state found in session. The request may have expired.", 400);
|
|
105
|
+
const result = await oauthService.handleCallback(providerName, {
|
|
106
|
+
code,
|
|
107
|
+
state,
|
|
108
|
+
expectedState,
|
|
109
|
+
redirectUri: options.redirectUri
|
|
110
|
+
});
|
|
111
|
+
await options.onSuccess(req, res, result);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
next(error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region core/express/identity-router.ts
|
|
119
|
+
const OAUTH_STATE_COOKIE = "azlib_oauth_state";
|
|
120
|
+
function resolveCookieName(opts) {
|
|
121
|
+
return opts.name ?? "azlib_rt";
|
|
122
|
+
}
|
|
123
|
+
/** Parses cookies from the raw `Cookie` header without a dependency on `cookie-parser`. */
|
|
124
|
+
function readCookie(req, name) {
|
|
125
|
+
const raw = req.headers.cookie ?? "";
|
|
126
|
+
for (const part of raw.split(";")) {
|
|
127
|
+
const eq = part.indexOf("=");
|
|
128
|
+
if (eq === -1) continue;
|
|
129
|
+
if (part.slice(0, eq).trim() === name) try {
|
|
130
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
131
|
+
} catch {
|
|
132
|
+
return part.slice(eq + 1).trim();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Writes the refresh token into a `Set-Cookie` header and returns a body-safe tokens
|
|
138
|
+
* object that omits the refresh token.
|
|
139
|
+
*/
|
|
140
|
+
function setCookieAndStripRefreshToken(res, tokens, opts) {
|
|
141
|
+
const maxAge = tokens.refreshTokenExpiresAt.getTime() - Date.now();
|
|
142
|
+
res.cookie(resolveCookieName(opts), tokens.refreshToken, {
|
|
143
|
+
httpOnly: opts.httpOnly ?? true,
|
|
144
|
+
secure: opts.secure ?? process.env.NODE_ENV === "production",
|
|
145
|
+
sameSite: opts.sameSite ?? "lax",
|
|
146
|
+
path: opts.path ?? "/",
|
|
147
|
+
domain: opts.domain,
|
|
148
|
+
maxAge
|
|
149
|
+
});
|
|
150
|
+
const { refreshToken: _rt, refreshTokenExpiresAt: _exp, ...bodyTokens } = tokens;
|
|
151
|
+
return bodyTokens;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Sends the authentication result as JSON. In cookie mode the refresh token is stored
|
|
155
|
+
* in a `Set-Cookie` header and excluded from the body.
|
|
156
|
+
*
|
|
157
|
+
* When the result is an MFA challenge (`{ kind: "mfa_required" }`), it is forwarded as-is
|
|
158
|
+
* with HTTP 200 so the client knows to complete the TOTP step.
|
|
159
|
+
*/
|
|
160
|
+
function sendLoginResult(res, result, cookieOpts) {
|
|
161
|
+
if ("mfaToken" in result) {
|
|
162
|
+
res.json(result);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
sendAuthResult(res, result, cookieOpts);
|
|
166
|
+
}
|
|
167
|
+
function sendAuthResult(res, result, cookieOpts) {
|
|
168
|
+
if (cookieOpts) {
|
|
169
|
+
const tokens = setCookieAndStripRefreshToken(res, result.tokens, cookieOpts);
|
|
170
|
+
res.json({
|
|
171
|
+
user: result.user,
|
|
172
|
+
tokens
|
|
173
|
+
});
|
|
174
|
+
} else res.json(result);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Reads the refresh token from the request. In cookie mode it is read from the cookie;
|
|
178
|
+
* in body mode it is expected in `req.body.refreshToken`.
|
|
179
|
+
*/
|
|
180
|
+
function readRefreshToken(req, cookieOpts) {
|
|
181
|
+
if (cookieOpts) return readCookie(req, resolveCookieName(cookieOpts));
|
|
182
|
+
const token = req.body?.["refreshToken"];
|
|
183
|
+
return typeof token === "string" ? token : void 0;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Creates a pre-wired Express `Router` with all standard identity endpoints.
|
|
187
|
+
*
|
|
188
|
+
* Mount it once on your application:
|
|
189
|
+
* ```ts
|
|
190
|
+
* import express from "express";
|
|
191
|
+
* import { createIdentityService } from "@azlib/identity/node";
|
|
192
|
+
* import { createIdentityRouter, identityErrorHandler } from "@azlib/identity/express";
|
|
193
|
+
*
|
|
194
|
+
* const service = createIdentityService(config, store);
|
|
195
|
+
* const app = express();
|
|
196
|
+
*
|
|
197
|
+
* app.use(express.json());
|
|
198
|
+
* // Routes are served at /account/login, /account/register, etc. (default prefix)
|
|
199
|
+
* app.use(createIdentityRouter(service));
|
|
200
|
+
* // Or use a custom prefix:
|
|
201
|
+
* app.use(createIdentityRouter(service, { prefix: "auth" }));
|
|
202
|
+
* app.use(identityErrorHandler()); // optional convenience error handler
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* Pre-wired routes:
|
|
206
|
+
*
|
|
207
|
+
* | Method | Path | Description |
|
|
208
|
+
* |--------|------|-------------|
|
|
209
|
+
* | POST | `/register` | Create a new account |
|
|
210
|
+
* | POST | `/login` | Email + password login |
|
|
211
|
+
* | POST | `/refresh` | Rotate the refresh token |
|
|
212
|
+
* | POST | `/logout` | Revoke the current session |
|
|
213
|
+
* | GET | `/me` | Return the authenticated principal |
|
|
214
|
+
* | GET | `/:provider` | Start an OAuth 2.0 authorisation flow *(optional)* |
|
|
215
|
+
* | GET | `/:provider/callback` | Handle an OAuth 2.0 callback *(optional)* |
|
|
216
|
+
*/
|
|
217
|
+
function createIdentityRouter(service, options = {}) {
|
|
218
|
+
const prefix = options.prefix ?? "account";
|
|
219
|
+
const log = require_logger.resolveLogger(options.logger);
|
|
220
|
+
const router = (0, express.Router)();
|
|
221
|
+
const cookieOpts = typeof options.refreshToken === "object" && "cookie" in options.refreshToken ? options.refreshToken.cookie : void 0;
|
|
222
|
+
router.use((_req, res, next) => {
|
|
223
|
+
const start = Date.now();
|
|
224
|
+
res.on("finish", () => {
|
|
225
|
+
const ms = Date.now() - start;
|
|
226
|
+
log.info(`${_req.method} ${_req.originalUrl} ${res.statusCode}`, { durationMs: ms });
|
|
227
|
+
});
|
|
228
|
+
next();
|
|
229
|
+
});
|
|
230
|
+
router.post("/register", async (req, res, next) => {
|
|
231
|
+
try {
|
|
232
|
+
sendAuthResult(res, await service.register(req.body), cookieOpts);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
next(err);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
router.post("/login", async (req, res, next) => {
|
|
238
|
+
try {
|
|
239
|
+
sendLoginResult(res, await service.login(req.body), cookieOpts);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
next(err);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
router.post("/refresh", async (req, res, next) => {
|
|
245
|
+
try {
|
|
246
|
+
const token = readRefreshToken(req, cookieOpts);
|
|
247
|
+
if (!token) throw new require_errors.UnauthenticatedError("Refresh token is missing.");
|
|
248
|
+
sendAuthResult(res, await service.refresh(token), cookieOpts);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
next(err);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
router.post("/logout", async (req, res, next) => {
|
|
254
|
+
try {
|
|
255
|
+
const token = readRefreshToken(req, cookieOpts);
|
|
256
|
+
if (token) await service.logout(token);
|
|
257
|
+
if (cookieOpts) res.clearCookie(resolveCookieName(cookieOpts), { path: cookieOpts.path ?? "/" });
|
|
258
|
+
res.status(204).end();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
next(err);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
router.get("/me", requireAuth(service), (_req, res) => {
|
|
264
|
+
res.json(_req.identity);
|
|
265
|
+
});
|
|
266
|
+
router.post("/request-email-verification", requireAuth(service), async (req, res, next) => {
|
|
267
|
+
try {
|
|
268
|
+
await service.requestEmailVerification(req.identity.userId);
|
|
269
|
+
res.status(204).end();
|
|
270
|
+
} catch (err) {
|
|
271
|
+
next(err);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
router.post("/verify-email", async (req, res, next) => {
|
|
275
|
+
try {
|
|
276
|
+
const { token } = req.body;
|
|
277
|
+
if (!token) throw new require_errors.UnauthenticatedError("token is required.");
|
|
278
|
+
await service.verifyEmail(token);
|
|
279
|
+
res.status(204).end();
|
|
280
|
+
} catch (err) {
|
|
281
|
+
next(err);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
router.post("/request-password-reset", async (req, res, next) => {
|
|
285
|
+
try {
|
|
286
|
+
const { email } = req.body;
|
|
287
|
+
if (!email) throw new require_errors.UnauthenticatedError("email is required.");
|
|
288
|
+
await service.requestPasswordReset(email);
|
|
289
|
+
res.status(204).end();
|
|
290
|
+
} catch (err) {
|
|
291
|
+
next(err);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
router.post("/reset-password", async (req, res, next) => {
|
|
295
|
+
try {
|
|
296
|
+
const { token, newPassword } = req.body;
|
|
297
|
+
if (!token || !newPassword) throw new require_errors.UnauthenticatedError("token and newPassword are required.");
|
|
298
|
+
await service.resetPassword(token, newPassword);
|
|
299
|
+
res.status(204).end();
|
|
300
|
+
} catch (err) {
|
|
301
|
+
next(err);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
router.post("/mfa/verify", async (req, res, next) => {
|
|
305
|
+
try {
|
|
306
|
+
const { mfaToken, code } = req.body;
|
|
307
|
+
if (!mfaToken || !code) throw new require_errors.UnauthenticatedError("mfaToken and code are required.");
|
|
308
|
+
sendAuthResult(res, await service.verifyMfaChallenge(mfaToken, code), cookieOpts);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
next(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
router.post("/2fa/setup", requireAuth(service), async (req, res, next) => {
|
|
314
|
+
try {
|
|
315
|
+
const result = await service.setup2FA(req.identity.userId);
|
|
316
|
+
res.json(result);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
next(err);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
router.post("/2fa/enable", requireAuth(service), async (req, res, next) => {
|
|
322
|
+
try {
|
|
323
|
+
const { code } = req.body;
|
|
324
|
+
if (!code) throw new require_errors.UnauthenticatedError("code is required.");
|
|
325
|
+
await service.enable2FA(req.identity.userId, code);
|
|
326
|
+
res.status(204).end();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
next(err);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
router.post("/2fa/disable", requireAuth(service), async (req, res, next) => {
|
|
332
|
+
try {
|
|
333
|
+
const { code } = req.body;
|
|
334
|
+
if (!code) throw new require_errors.UnauthenticatedError("code is required.");
|
|
335
|
+
await service.disable2FA(req.identity.userId, code);
|
|
336
|
+
res.status(204).end();
|
|
337
|
+
} catch (err) {
|
|
338
|
+
next(err);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
router.post("/admin/lock/:userId", async (req, res, next) => {
|
|
342
|
+
try {
|
|
343
|
+
await service.lockAccount(req.params["userId"]);
|
|
344
|
+
res.status(204).end();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
next(err);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
router.post("/admin/unlock/:userId", async (req, res, next) => {
|
|
350
|
+
try {
|
|
351
|
+
await service.unlockAccount(req.params["userId"]);
|
|
352
|
+
res.status(204).end();
|
|
353
|
+
} catch (err) {
|
|
354
|
+
next(err);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
router.post("/admin/disable/:userId", async (req, res, next) => {
|
|
358
|
+
try {
|
|
359
|
+
await service.disableAccount(req.params["userId"]);
|
|
360
|
+
res.status(204).end();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
next(err);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
if (service.oauth) {
|
|
366
|
+
const oauthService = service.oauth;
|
|
367
|
+
const baseUrl = options.oauthBaseUrl ?? "";
|
|
368
|
+
const stateCookieOptions = {
|
|
369
|
+
httpOnly: true,
|
|
370
|
+
sameSite: "lax",
|
|
371
|
+
secure: process.env.NODE_ENV === "production",
|
|
372
|
+
maxAge: 600 * 1e3
|
|
373
|
+
};
|
|
374
|
+
for (const providerName of oauthService.providers) {
|
|
375
|
+
const redirectUri = `${baseUrl}/${providerName}/callback`;
|
|
376
|
+
router.get(`/${providerName}`, oauthAuthorize(oauthService, providerName, {
|
|
377
|
+
redirectUri,
|
|
378
|
+
setState: (_req, oauthRes, state) => {
|
|
379
|
+
oauthRes.cookie(OAUTH_STATE_COOKIE, state, stateCookieOptions);
|
|
380
|
+
}
|
|
381
|
+
}));
|
|
382
|
+
router.get(`/${providerName}/callback`, oauthCallback(oauthService, providerName, {
|
|
383
|
+
redirectUri,
|
|
384
|
+
getState: (oauthReq) => readCookie(oauthReq, OAUTH_STATE_COOKIE) ?? null,
|
|
385
|
+
setState: (_oauthReq, oauthRes, _state) => {
|
|
386
|
+
oauthRes.clearCookie(OAUTH_STATE_COOKIE);
|
|
387
|
+
},
|
|
388
|
+
onSuccess: (_oauthReq, oauthRes, result) => {
|
|
389
|
+
sendAuthResult(oauthRes, result, cookieOpts);
|
|
390
|
+
}
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const outer = (0, express.Router)();
|
|
395
|
+
if (prefix) outer.use(`/${prefix}`, router);
|
|
396
|
+
else outer.use(router);
|
|
397
|
+
return outer;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
exports.createIdentityRouter = createIdentityRouter;
|
|
401
|
+
exports.oauthAuthorize = oauthAuthorize;
|
|
402
|
+
exports.oauthCallback = oauthCallback;
|
|
403
|
+
exports.requireAuth = requireAuth;
|
|
404
|
+
exports.requireAuthorization = requireAuthorization;
|
|
405
|
+
exports.requirePermission = requirePermission;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { c as AuthResult, d as AuthenticatedIdentity } from "./identity-store-BRRahxcS.cjs";
|
|
2
|
+
import { V as AuthorizationRequirement, f as OAuthService } from "./errors-C2xZAatu.cjs";
|
|
3
|
+
import { t as IdentityService } from "./identity-service-CLzKx8Z7.cjs";
|
|
4
|
+
import { n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-Dib30Waj.cjs";
|
|
5
|
+
import { Request, RequestHandler, Response } from "express";
|
|
6
|
+
|
|
7
|
+
//#region core/express/middleware.d.ts
|
|
8
|
+
declare global {
|
|
9
|
+
namespace Express {
|
|
10
|
+
interface Request {
|
|
11
|
+
/** The authenticated principal, set by {@link requireAuth}. */
|
|
12
|
+
identity?: AuthenticatedIdentity;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Loads the resource a policy will be evaluated against. */
|
|
17
|
+
type ResourceLoader<TResource> = (req: Request) => TResource | Promise<TResource>;
|
|
18
|
+
/**
|
|
19
|
+
* Express middleware that authenticates the request using a Bearer access token and
|
|
20
|
+
* attaches the hydrated principal to `req.identity`. Forwards identity errors to the
|
|
21
|
+
* error-handling middleware.
|
|
22
|
+
*/
|
|
23
|
+
declare function requireAuth(service: IdentityService): RequestHandler;
|
|
24
|
+
/**
|
|
25
|
+
* Express middleware enforcing an authorization requirement (deny-by-default). Must run
|
|
26
|
+
* after {@link requireAuth}. Optionally loads a resource for ownership policies.
|
|
27
|
+
*/
|
|
28
|
+
declare function requireAuthorization<TResource = unknown>(service: IdentityService, requirement: AuthorizationRequirement<TResource>, loadResource?: ResourceLoader<TResource>): RequestHandler;
|
|
29
|
+
/** Convenience: require a single permission. */
|
|
30
|
+
declare function requirePermission(service: IdentityService, permission: string): RequestHandler;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region core/express/oauth-routes.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Options for {@link oauthAuthorize} and {@link oauthCallback}.
|
|
35
|
+
*/
|
|
36
|
+
interface OAuthRouteOptions {
|
|
37
|
+
/** The full redirect URI registered with the provider (must match exactly). */
|
|
38
|
+
redirectUri: string;
|
|
39
|
+
/**
|
|
40
|
+
* Retrieves the stored CSRF state value from the current request context (e.g. from a
|
|
41
|
+
* signed cookie or server session). Return `null` if no state has been stored yet.
|
|
42
|
+
*/
|
|
43
|
+
getState(req: Request): string | null | Promise<string | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Persists the generated CSRF state value before redirecting the user to the provider.
|
|
46
|
+
* Use a signed cookie or server session.
|
|
47
|
+
*/
|
|
48
|
+
setState(req: Request, res: Response, state: string): void | Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Called on a successful OAuth callback with the auth result. Typically sets a session
|
|
51
|
+
* cookie and redirects to the app.
|
|
52
|
+
*/
|
|
53
|
+
onSuccess(req: Request, res: Response, result: AuthResult): void | Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns an Express route handler that redirects the user to the OAuth provider's
|
|
57
|
+
* authorization page. Generates and persists a CSRF state value via `options.setState`.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* app.get("/auth/google", oauthAuthorize(identity.oauth!, "google", {
|
|
62
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
63
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
64
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
65
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
66
|
+
* }));
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function oauthAuthorize(oauthService: OAuthService, providerName: string, options: Pick<OAuthRouteOptions, "redirectUri" | "setState">): RequestHandler;
|
|
70
|
+
/**
|
|
71
|
+
* Returns an Express route handler that handles the OAuth provider callback. Verifies
|
|
72
|
+
* state, exchanges the code, and calls `options.onSuccess` with the auth result.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* app.get("/auth/google/callback", oauthCallback(identity.oauth!, "google", {
|
|
77
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
78
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
79
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
80
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
81
|
+
* }));
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
declare function oauthCallback(oauthService: OAuthService, providerName: string, options: OAuthRouteOptions): RequestHandler;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CookieRefreshOptions, IdentityRouterOptions, OAuthRouteOptions, ResourceLoader, createIdentityRouter, oauthAuthorize, oauthCallback, requireAuth, requireAuthorization, requirePermission };
|
|
87
|
+
//# sourceMappingURL=express.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.cts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;;QAOQ,MAAA;EAAA,UAEI,OAAA;IAAA,UACE,OAAA;MALwC;MAOhD,QAAA,GAAW,qBAAqB;IAAA;EAAA;AAAA;;KAM1B,cAAA,eAA6B,GAAA,EAAK,OAAA,KAAY,SAAA,GAAY,OAAA,CAAQ,SAAA;;;;;AANxC;iBAoBtB,WAAA,CAAY,OAAA,EAAS,eAAA,GAAkB,cAAc;;;;;iBAmBrD,oBAAA,qBAAA,CACd,OAAA,EAAS,eAAA,EACT,WAAA,EAAa,wBAAA,CAAyB,SAAA,GACtC,YAAA,GAAe,cAAA,CAAe,SAAA,IAC7B,cAAA;;iBAwBa,iBAAA,CACd,OAAA,EAAS,eAAA,EACT,UAAA,WACC,cAAc;;;;;;UC1EA,iBAAA;;EAEf,WAAA;EDLoD;;;;ECUpD,QAAA,CAAS,GAAA,EAAK,OAAA,mBAA0B,OAAA;EDL5B;;;;ECUZ,QAAA,CAAS,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,KAAA,kBAAuB,OAAA;EDRzB;AAMtC;;;ECOE,SAAA,CAAU,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,MAAA,EAAF,UAAA,UAAiD,OAAA;AAAA;;;;;;;;;;;;ADPD;AAcvF;;iBCUgB,cAAA,CACd,YAAA,EAAc,YAAA,EACd,YAAA,UACA,OAAA,EAAS,IAAA,CAAK,iBAAA,gCACb,cAAA;;;;;;ADdkE;AAmBrE;;;;;;;;iBCqBgB,aAAA,CACd,YAAA,EAAc,YAAA,EACd,YAAA,UACA,OAAA,EAAS,iBAAA,GACR,cAAA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { c as AuthResult, d as AuthenticatedIdentity } from "./identity-store-BRRahxcS.mjs";
|
|
2
|
+
import { V as AuthorizationRequirement, f as OAuthService } from "./errors-BGMwaW5s.mjs";
|
|
3
|
+
import { t as IdentityService } from "./identity-service-B9zrvE9z.mjs";
|
|
4
|
+
import { n as IdentityRouterOptions, r as createIdentityRouter, t as CookieRefreshOptions } from "./identity-router-DBL20UWT.mjs";
|
|
5
|
+
import { Request, RequestHandler, Response } from "express";
|
|
6
|
+
|
|
7
|
+
//#region core/express/middleware.d.ts
|
|
8
|
+
declare global {
|
|
9
|
+
namespace Express {
|
|
10
|
+
interface Request {
|
|
11
|
+
/** The authenticated principal, set by {@link requireAuth}. */
|
|
12
|
+
identity?: AuthenticatedIdentity;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Loads the resource a policy will be evaluated against. */
|
|
17
|
+
type ResourceLoader<TResource> = (req: Request) => TResource | Promise<TResource>;
|
|
18
|
+
/**
|
|
19
|
+
* Express middleware that authenticates the request using a Bearer access token and
|
|
20
|
+
* attaches the hydrated principal to `req.identity`. Forwards identity errors to the
|
|
21
|
+
* error-handling middleware.
|
|
22
|
+
*/
|
|
23
|
+
declare function requireAuth(service: IdentityService): RequestHandler;
|
|
24
|
+
/**
|
|
25
|
+
* Express middleware enforcing an authorization requirement (deny-by-default). Must run
|
|
26
|
+
* after {@link requireAuth}. Optionally loads a resource for ownership policies.
|
|
27
|
+
*/
|
|
28
|
+
declare function requireAuthorization<TResource = unknown>(service: IdentityService, requirement: AuthorizationRequirement<TResource>, loadResource?: ResourceLoader<TResource>): RequestHandler;
|
|
29
|
+
/** Convenience: require a single permission. */
|
|
30
|
+
declare function requirePermission(service: IdentityService, permission: string): RequestHandler;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region core/express/oauth-routes.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Options for {@link oauthAuthorize} and {@link oauthCallback}.
|
|
35
|
+
*/
|
|
36
|
+
interface OAuthRouteOptions {
|
|
37
|
+
/** The full redirect URI registered with the provider (must match exactly). */
|
|
38
|
+
redirectUri: string;
|
|
39
|
+
/**
|
|
40
|
+
* Retrieves the stored CSRF state value from the current request context (e.g. from a
|
|
41
|
+
* signed cookie or server session). Return `null` if no state has been stored yet.
|
|
42
|
+
*/
|
|
43
|
+
getState(req: Request): string | null | Promise<string | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Persists the generated CSRF state value before redirecting the user to the provider.
|
|
46
|
+
* Use a signed cookie or server session.
|
|
47
|
+
*/
|
|
48
|
+
setState(req: Request, res: Response, state: string): void | Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Called on a successful OAuth callback with the auth result. Typically sets a session
|
|
51
|
+
* cookie and redirects to the app.
|
|
52
|
+
*/
|
|
53
|
+
onSuccess(req: Request, res: Response, result: AuthResult): void | Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns an Express route handler that redirects the user to the OAuth provider's
|
|
57
|
+
* authorization page. Generates and persists a CSRF state value via `options.setState`.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* app.get("/auth/google", oauthAuthorize(identity.oauth!, "google", {
|
|
62
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
63
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
64
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
65
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
66
|
+
* }));
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function oauthAuthorize(oauthService: OAuthService, providerName: string, options: Pick<OAuthRouteOptions, "redirectUri" | "setState">): RequestHandler;
|
|
70
|
+
/**
|
|
71
|
+
* Returns an Express route handler that handles the OAuth provider callback. Verifies
|
|
72
|
+
* state, exchanges the code, and calls `options.onSuccess` with the auth result.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* app.get("/auth/google/callback", oauthCallback(identity.oauth!, "google", {
|
|
77
|
+
* redirectUri: `${BASE_URL}/auth/google/callback`,
|
|
78
|
+
* getState: (req) => req.session?.oauthState ?? null,
|
|
79
|
+
* setState: (req, _res, state) => { req.session!.oauthState = state; },
|
|
80
|
+
* onSuccess: (_req, res, result) => res.json(result),
|
|
81
|
+
* }));
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
declare function oauthCallback(oauthService: OAuthService, providerName: string, options: OAuthRouteOptions): RequestHandler;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CookieRefreshOptions, IdentityRouterOptions, OAuthRouteOptions, ResourceLoader, createIdentityRouter, oauthAuthorize, oauthCallback, requireAuth, requireAuthorization, requirePermission };
|
|
87
|
+
//# sourceMappingURL=express.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../core/express/middleware.ts","../core/express/oauth-routes.ts"],"mappings":";;;;;;;QAOQ,MAAA;EAAA,UAEI,OAAA;IAAA,UACE,OAAA;MALwC;MAOhD,QAAA,GAAW,qBAAqB;IAAA;EAAA;AAAA;;KAM1B,cAAA,eAA6B,GAAA,EAAK,OAAA,KAAY,SAAA,GAAY,OAAA,CAAQ,SAAA;;;;;AANxC;iBAoBtB,WAAA,CAAY,OAAA,EAAS,eAAA,GAAkB,cAAc;;;;;iBAmBrD,oBAAA,qBAAA,CACd,OAAA,EAAS,eAAA,EACT,WAAA,EAAa,wBAAA,CAAyB,SAAA,GACtC,YAAA,GAAe,cAAA,CAAe,SAAA,IAC7B,cAAA;;iBAwBa,iBAAA,CACd,OAAA,EAAS,eAAA,EACT,UAAA,WACC,cAAc;;;;;;UC1EA,iBAAA;;EAEf,WAAA;EDLoD;;;;ECUpD,QAAA,CAAS,GAAA,EAAK,OAAA,mBAA0B,OAAA;EDL5B;;;;ECUZ,QAAA,CAAS,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,KAAA,kBAAuB,OAAA;EDRzB;AAMtC;;;ECOE,SAAA,CAAU,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,MAAA,EAAF,UAAA,UAAiD,OAAA;AAAA;;;;;;;;;;;;ADPD;AAcvF;;iBCUgB,cAAA,CACd,YAAA,EAAc,YAAA,EACd,YAAA,UACA,OAAA,EAAS,IAAA,CAAK,iBAAA,gCACb,cAAA;;;;;;ADdkE;AAmBrE;;;;;;;;iBCqBgB,aAAA,CACd,YAAA,EAAc,YAAA,EACd,YAAA,UACA,OAAA,EAAS,iBAAA,GACR,cAAA"}
|