@oneunit/auth 2.0.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.
Files changed (75) hide show
  1. package/ARCHITECTURE.md +465 -0
  2. package/CHANGELOG.md +214 -0
  3. package/LICENSE +21 -0
  4. package/README.md +647 -0
  5. package/dist/adapters.d.ts +51 -0
  6. package/dist/adapters.d.ts.map +1 -0
  7. package/dist/adapters.js +301 -0
  8. package/dist/adapters.js.map +1 -0
  9. package/dist/auth.d.ts +59 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +560 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/errors.d.ts +39 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +65 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +10 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/jwt.d.ts +11 -0
  22. package/dist/jwt.d.ts.map +1 -0
  23. package/dist/jwt.js +125 -0
  24. package/dist/jwt.js.map +1 -0
  25. package/dist/oauth.d.ts +31 -0
  26. package/dist/oauth.d.ts.map +1 -0
  27. package/dist/oauth.js +178 -0
  28. package/dist/oauth.js.map +1 -0
  29. package/dist/password.d.ts +5 -0
  30. package/dist/password.d.ts.map +1 -0
  31. package/dist/password.js +90 -0
  32. package/dist/password.js.map +1 -0
  33. package/dist/providers.d.ts +37 -0
  34. package/dist/providers.d.ts.map +1 -0
  35. package/dist/providers.js +471 -0
  36. package/dist/providers.js.map +1 -0
  37. package/dist/rbac.d.ts +40 -0
  38. package/dist/rbac.d.ts.map +1 -0
  39. package/dist/rbac.js +240 -0
  40. package/dist/rbac.js.map +1 -0
  41. package/dist/roles.d.ts +2 -0
  42. package/dist/roles.d.ts.map +1 -0
  43. package/dist/roles.js +2 -0
  44. package/dist/roles.js.map +1 -0
  45. package/dist/token.d.ts +2 -0
  46. package/dist/token.d.ts.map +1 -0
  47. package/dist/token.js +2 -0
  48. package/dist/token.js.map +1 -0
  49. package/dist/types.d.ts +383 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/utils.d.ts +35 -0
  54. package/dist/utils.d.ts.map +1 -0
  55. package/dist/utils.js +192 -0
  56. package/dist/utils.js.map +1 -0
  57. package/examples/express.ts +111 -0
  58. package/examples/fastify.ts +59 -0
  59. package/examples/oauth-social.ts +83 -0
  60. package/examples/standalone.ts +67 -0
  61. package/examples/uwebsockets.ts +143 -0
  62. package/package.json +86 -0
  63. package/src/adapters.ts +333 -0
  64. package/src/auth.ts +684 -0
  65. package/src/errors.ts +76 -0
  66. package/src/index.ts +124 -0
  67. package/src/jwt.ts +159 -0
  68. package/src/oauth.ts +226 -0
  69. package/src/password.ts +111 -0
  70. package/src/providers.ts +551 -0
  71. package/src/rbac.ts +285 -0
  72. package/src/roles.ts +1 -0
  73. package/src/token.ts +1 -0
  74. package/src/types.ts +432 -0
  75. package/src/utils.ts +231 -0
@@ -0,0 +1,143 @@
1
+ import { createAuth, uwsAdapter } from "@oneunit/auth";
2
+ import type { UwsHttpRequest, UwsHttpResponse, UwsRequestSnapshot, UserRecord } from "@oneunit/auth";
3
+
4
+ interface ExampleUwsResponse extends UwsHttpResponse {
5
+ onAborted(cb: () => void): void;
6
+ onData(cb: (chunk: ArrayBuffer, isLast: boolean) => void): void;
7
+ }
8
+
9
+ interface ExampleUwsRequest extends UwsHttpRequest {
10
+ getParameter?(index: number): string;
11
+ }
12
+
13
+ interface ExampleUwsApp {
14
+ post(
15
+ path: string,
16
+ handler: (res: ExampleUwsResponse, req: ExampleUwsRequest) => void,
17
+ ): ExampleUwsApp;
18
+ get(
19
+ path: string,
20
+ handler: (res: ExampleUwsResponse, req: ExampleUwsRequest) => unknown,
21
+ ): ExampleUwsApp;
22
+ listen(port: number, cb: (token: unknown) => void): unknown;
23
+ }
24
+
25
+ // uWebSockets.js is not an npm dependency of this package — the adapter is pure
26
+ // and needs no import, only a server does. Install it from its GitHub package to
27
+ // actually run this example.
28
+ const uWS = {
29
+ App(): ExampleUwsApp {
30
+ console.log("Install uWebSockets.js to run this example: npm i uNetworking/uWebSockets.js");
31
+ console.log("The uwsAdapter is pure and imports nothing; only the server does.");
32
+ process.exit(0);
33
+ },
34
+ };
35
+
36
+ const auth = createAuth({
37
+ secret: process.env.AUTH_SECRET ?? "change-me-in-production",
38
+ issuer: "uws-example",
39
+ rbac: {
40
+ defaultRole: "user",
41
+ roles: {
42
+ user: { permissions: ["me.read"] },
43
+ admin: { inherits: "user", permissions: ["stats.read"] },
44
+ },
45
+ },
46
+ providers: {
47
+ google: {
48
+ clientId: process.env.GOOGLE_CLIENT_ID,
49
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
50
+ redirectUri: "http://localhost:3000/auth/google/callback",
51
+ },
52
+ },
53
+ });
54
+
55
+ const { authenticate, requirePermission, json } = uwsAdapter(auth);
56
+
57
+ const port = Number(process.env.PORT ?? 3000);
58
+
59
+ /**
60
+ * Stands in for your database lookup. Roles come from the store, never from
61
+ * the parsed request body, so a caller cannot choose their own grants.
62
+ */
63
+ async function findUserByEmail(email: string): Promise<UserRecord> {
64
+ return { id: "user_01", email, name: "Ada", roles: ["user"] };
65
+ }
66
+
67
+ uWS.App()
68
+ .post("/auth/login", (res, _req) => {
69
+ const aborted = { value: false };
70
+ res.onAborted(() => {
71
+ aborted.value = true;
72
+ });
73
+
74
+ let buffer = Buffer.alloc(0);
75
+ res.onData((chunk: ArrayBuffer, isLast: boolean) => {
76
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
77
+ if (!isLast) {
78
+ return;
79
+ }
80
+
81
+ Promise.resolve()
82
+ .then(async () => {
83
+ const body = JSON.parse(buffer.toString("utf8") || "{}") as {
84
+ email?: string;
85
+ };
86
+ // Roles come from your store, never from the parsed body. `auth.login()`
87
+ // signs whatever record it is given and performs no authorization.
88
+ const user = await findUserByEmail(String(body.email ?? ""));
89
+ const session = await auth.login(user);
90
+ if (!aborted.value) {
91
+ json(res, 200, session);
92
+ }
93
+ })
94
+ .catch((error: { status?: number; code?: string; message: string }) => {
95
+ if (!aborted.value) {
96
+ json(res, error.status ?? 400, {
97
+ error: error.code ?? "AUTH_ERROR",
98
+ message: error.message,
99
+ });
100
+ }
101
+ });
102
+ });
103
+ })
104
+ .get("/auth/:provider", (res, req) => {
105
+ const provider = req.getParameter?.(0) ?? "";
106
+ let aborted = false;
107
+ res.onAborted(() => {
108
+ aborted = true;
109
+ });
110
+
111
+ auth.getAuthorizationUrl(provider)
112
+ .then(({ url }) => {
113
+ if (aborted) {
114
+ return;
115
+ }
116
+ res.cork?.(() => {
117
+ res.writeStatus?.("302 Found");
118
+ res.writeHeader?.("Location", url);
119
+ res.end?.();
120
+ });
121
+ })
122
+ .catch((error: { status?: number; code?: string; message: string }) => {
123
+ if (!aborted) {
124
+ json(res, error.status ?? 400, {
125
+ error: error.code ?? "AUTH_ERROR",
126
+ message: error.message,
127
+ });
128
+ }
129
+ });
130
+ })
131
+ .get("/me", authenticate()((res, _req, request?: UwsRequestSnapshot) => {
132
+ json(res, 200, { user: request?.user });
133
+ }))
134
+ .get("/stats", authenticate()(requirePermission("stats.read")((res, _req, request?: UwsRequestSnapshot) => {
135
+ json(res, 200, { ok: true, userId: request?.user?.userId });
136
+ })))
137
+ .listen(port, (token: unknown) => {
138
+ if (token) {
139
+ console.log(`uWebSockets.js listening on ${port}`);
140
+ } else {
141
+ console.error(`Failed to listen on ${port}`);
142
+ }
143
+ });
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@oneunit/auth",
3
+ "version": "2.0.0",
4
+ "description": "Framework-agnostic authentication: JWT, generic RBAC, password hashing, and social OAuth. Works standalone or with Express, Fastify, Koa, and uWebSockets.js.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "examples",
21
+ "LICENSE",
22
+ "README.md",
23
+ "ARCHITECTURE.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "dev": "tsc -w -p tsconfig.build.json",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit",
30
+ "test": "tsx --test test/*.test.ts",
31
+ "prepack": "npm run build",
32
+ "prepublishOnly": "npm run build && npm test",
33
+ "pack:check": "npm pack --dry-run",
34
+ "example": "npm run build && tsx examples/",
35
+ "example:standalone": "npm run build && tsx examples/standalone.ts",
36
+ "example:oauth": "npm run build && tsx examples/oauth-social.ts"
37
+ },
38
+ "keywords": [
39
+ "auth",
40
+ "authentication",
41
+ "jwt",
42
+ "jsonwebtoken",
43
+ "oauth",
44
+ "oauth2",
45
+ "oidc",
46
+ "rbac",
47
+ "permissions",
48
+ "roles",
49
+ "google",
50
+ "github",
51
+ "express",
52
+ "fastify",
53
+ "koa",
54
+ "uwebsockets",
55
+ "typescript",
56
+ "oneunit"
57
+ ],
58
+ "author": "mayank",
59
+ "license": "MIT",
60
+ "sideEffects": false,
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public",
66
+ "registry": "https://registry.npmjs.org/"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/mayank040902/framework.git",
71
+ "directory": "packages/auth"
72
+ },
73
+ "bugs": {
74
+ "url": "https://github.com/mayank040902/framework/issues"
75
+ },
76
+ "homepage": "https://github.com/mayank040902/framework/tree/main/packages/auth#readme",
77
+ "dependencies": {
78
+ "jsonwebtoken": "^9.0.2"
79
+ },
80
+ "devDependencies": {
81
+ "@types/jsonwebtoken": "^9.0.7",
82
+ "@types/node": "^22.10.0",
83
+ "tsx": "^4.19.2",
84
+ "typescript": "^5.7.2"
85
+ }
86
+ }
@@ -0,0 +1,333 @@
1
+ import { AuthError, ForbiddenError, UnauthorizedError } from "./errors.js";
2
+ import type { Auth } from "./auth.js";
3
+ import type {
4
+ ExpressNext,
5
+ ExpressRequestLike,
6
+ ExpressResponseLike,
7
+ ExtractTokenOptions,
8
+ FastifyLike,
9
+ FastifyReplyLike,
10
+ FastifyRequestLike,
11
+ KoaContextLike,
12
+ UwsHandler,
13
+ UwsHttpRequest,
14
+ UwsHttpResponse,
15
+ UwsRequestSnapshot,
16
+ } from "./types.js";
17
+
18
+ function sendError(reply: ExpressResponseLike, error: unknown): void {
19
+ const err = error as AuthError;
20
+ const status = err.status ?? 401;
21
+ const body = {
22
+ error: err.code ?? "AUTH_ERROR",
23
+ message: err.message,
24
+ };
25
+
26
+ if (typeof reply.status === "function" && typeof reply.send === "function") {
27
+ const next = reply.status(status);
28
+ if (next && typeof next.send === "function") {
29
+ next.send(body);
30
+ } else {
31
+ reply.send(body);
32
+ }
33
+ return;
34
+ }
35
+ if (typeof reply.code === "function") {
36
+ const coded = reply.code(status);
37
+ const send = coded.send ?? reply.send;
38
+ if (typeof send === "function") {
39
+ send.call(coded, body);
40
+ return;
41
+ }
42
+ }
43
+ if (typeof reply.status === "function") {
44
+ reply.status(status);
45
+ reply.body = body;
46
+ return;
47
+ }
48
+ throw error;
49
+ }
50
+
51
+ export function expressAdapter(auth: Auth) {
52
+ return {
53
+ initialize() {
54
+ return (req: ExpressRequestLike, _res: ExpressResponseLike, next?: ExpressNext) => {
55
+ req.auth = auth;
56
+ next?.();
57
+ };
58
+ },
59
+
60
+ authenticate(options: ExtractTokenOptions = {}) {
61
+ return async (req: ExpressRequestLike, res: ExpressResponseLike, next?: ExpressNext) => {
62
+ try {
63
+ req.user = await auth.verifyRequest(req, options);
64
+ req.token = auth.extractToken(req, options);
65
+ next?.();
66
+ } catch (error) {
67
+ if (options.optional && error instanceof UnauthorizedError) {
68
+ req.user = null;
69
+ next?.();
70
+ return;
71
+ }
72
+ if (options.passthrough) {
73
+ next?.(error);
74
+ return;
75
+ }
76
+ sendError(res, error);
77
+ }
78
+ };
79
+ },
80
+
81
+ requirePermission(...permissions: string[]) {
82
+ return (req: ExpressRequestLike, res: ExpressResponseLike, next?: ExpressNext) => {
83
+ try {
84
+ if (!req.user) {
85
+ throw new UnauthorizedError("Authentication required");
86
+ }
87
+ auth.authorize(req.user, permissions);
88
+ next?.();
89
+ } catch (error) {
90
+ sendError(res, error);
91
+ }
92
+ };
93
+ },
94
+
95
+ requireRole(...roles: string[]) {
96
+ return (req: ExpressRequestLike, res: ExpressResponseLike, next?: ExpressNext) => {
97
+ try {
98
+ if (!req.user) {
99
+ throw new UnauthorizedError("Authentication required");
100
+ }
101
+ if (!auth.hasRole(req.user, roles)) {
102
+ throw new ForbiddenError("Insufficient role");
103
+ }
104
+ next?.();
105
+ } catch (error) {
106
+ sendError(res, error);
107
+ }
108
+ };
109
+ },
110
+ };
111
+ }
112
+
113
+ export function fastifyAdapter(auth: Auth) {
114
+ return async function plugin(fastify: FastifyLike, options: ExtractTokenOptions = {}) {
115
+ fastify.decorate("auth", auth);
116
+ fastify.decorateRequest("user", null);
117
+
118
+ fastify.decorate("authenticate", (opts: ExtractTokenOptions = {}) => async (request: FastifyRequestLike, reply: FastifyReplyLike) => {
119
+ // Merge once so the error path sees the same options as the verify path;
120
+ // reading `opts.optional` alone ignored plugin-level configuration.
121
+ const settings: ExtractTokenOptions = { ...options, ...opts };
122
+ try {
123
+ request.user = await auth.verifyRequest(request, settings);
124
+ } catch (error) {
125
+ if (settings.optional && error instanceof UnauthorizedError) {
126
+ request.user = null;
127
+ return;
128
+ }
129
+ const err = error as AuthError;
130
+ return reply.code(err.status ?? 401).send({
131
+ error: err.code ?? "AUTH_ERROR",
132
+ message: err.message,
133
+ });
134
+ }
135
+ });
136
+
137
+ fastify.decorate("requirePermission", (...permissions: string[]) => async (request: FastifyRequestLike, reply: FastifyReplyLike) => {
138
+ if (!request.user) {
139
+ return reply.code(401).send({ error: "UNAUTHORIZED", message: "Authentication required" });
140
+ }
141
+ if (!auth.can(request.user, permissions)) {
142
+ return reply.code(403).send({ error: "FORBIDDEN", message: "Missing permission" });
143
+ }
144
+ });
145
+
146
+ fastify.decorate("requireRole", (...roles: string[]) => async (request: FastifyRequestLike, reply: FastifyReplyLike) => {
147
+ if (!request.user) {
148
+ return reply.code(401).send({ error: "UNAUTHORIZED", message: "Authentication required" });
149
+ }
150
+ if (!auth.hasRole(request.user, roles)) {
151
+ return reply.code(403).send({ error: "FORBIDDEN", message: "Insufficient role" });
152
+ }
153
+ });
154
+ };
155
+ }
156
+
157
+ export function koaAdapter(auth: Auth) {
158
+ return {
159
+ authenticate(options: ExtractTokenOptions = {}) {
160
+ return async (ctx: KoaContextLike, next: () => Promise<void>) => {
161
+ try {
162
+ ctx.state.user = await auth.verifyRequest(ctx.request, options);
163
+ ctx.state.token = auth.extractToken(ctx.request, options);
164
+ } catch (error) {
165
+ if (options.optional && error instanceof UnauthorizedError) {
166
+ ctx.state.user = null;
167
+ await next();
168
+ return;
169
+ }
170
+ const err = error as AuthError;
171
+ ctx.status = err.status ?? 401;
172
+ ctx.body = { error: err.code ?? "AUTH_ERROR", message: err.message };
173
+ return;
174
+ }
175
+ // `next()` stays outside the try so downstream handler errors propagate
176
+ // to Koa's own error handling instead of being reported as auth failures.
177
+ await next();
178
+ };
179
+ },
180
+
181
+ requirePermission(...permissions: string[]) {
182
+ return async (ctx: KoaContextLike, next: () => Promise<void>) => {
183
+ if (!ctx.state.user) {
184
+ ctx.status = 401;
185
+ ctx.body = { error: "UNAUTHORIZED", message: "Authentication required" };
186
+ return;
187
+ }
188
+ if (!auth.can(ctx.state.user, permissions)) {
189
+ ctx.status = 403;
190
+ ctx.body = { error: "FORBIDDEN", message: "Missing permission" };
191
+ return;
192
+ }
193
+ await next();
194
+ };
195
+ },
196
+ };
197
+ }
198
+
199
+ function jsonResponse(res: UwsHttpResponse, status: number, body: unknown): void {
200
+ if (typeof res.cork === "function") {
201
+ res.cork(() => {
202
+ res.writeStatus?.(String(status));
203
+ res.writeHeader?.("Content-Type", "application/json; charset=utf-8");
204
+ res.end?.(JSON.stringify(body));
205
+ });
206
+ return;
207
+ }
208
+
209
+ if (typeof res.writeStatus === "function") {
210
+ res.writeStatus(String(status));
211
+ res.writeHeader?.("Content-Type", "application/json; charset=utf-8");
212
+ res.end?.(JSON.stringify(body));
213
+ }
214
+ }
215
+
216
+ function attachAbort(res: UwsHttpResponse): () => boolean {
217
+ let aborted = false;
218
+ if (typeof res.onAborted === "function") {
219
+ res.onAborted(() => {
220
+ aborted = true;
221
+ });
222
+ }
223
+ return () => aborted;
224
+ }
225
+
226
+ export function snapshotUwsRequest(
227
+ res: UwsHttpResponse,
228
+ req: UwsHttpRequest,
229
+ extra: Partial<UwsRequestSnapshot> = {},
230
+ ): UwsRequestSnapshot {
231
+ const headers: Record<string, string> = {};
232
+ if (typeof req.forEach === "function") {
233
+ req.forEach((key, value) => {
234
+ headers[key] = value;
235
+ });
236
+ } else {
237
+ const authorization = req.getHeader?.("authorization");
238
+ const cookie = req.getHeader?.("cookie");
239
+ if (authorization) headers.authorization = authorization;
240
+ if (cookie) headers.cookie = cookie;
241
+ }
242
+
243
+ return {
244
+ method: req.getMethod?.() ?? extra.method,
245
+ url: req.getUrl?.() ?? extra.url,
246
+ query: req.getQuery?.() ?? extra.query,
247
+ headers,
248
+ cookies: extra.cookies,
249
+ res,
250
+ };
251
+ }
252
+
253
+ export function uwsAdapter(auth: Auth) {
254
+ return {
255
+ snapshot: snapshotUwsRequest,
256
+
257
+ authenticate(options: ExtractTokenOptions = {}) {
258
+ return (handler: UwsHandler): UwsHandler => async (res, req) => {
259
+ const aborted = attachAbort(res);
260
+ const request = snapshotUwsRequest(res, req);
261
+
262
+ try {
263
+ request.user = await auth.verifyRequest(request, options);
264
+ request.token = auth.extractToken(request, options);
265
+ if (aborted()) {
266
+ return;
267
+ }
268
+ return await handler(res, req, request);
269
+ } catch (error) {
270
+ if (options.optional && error instanceof UnauthorizedError) {
271
+ request.user = null;
272
+ if (aborted()) {
273
+ return;
274
+ }
275
+ return await handler(res, req, request);
276
+ }
277
+ if (aborted()) {
278
+ return;
279
+ }
280
+ const err = error as AuthError;
281
+ jsonResponse(res, err.status ?? 401, {
282
+ error: err.code ?? "AUTH_ERROR",
283
+ message: err.message,
284
+ });
285
+ }
286
+ };
287
+ },
288
+
289
+ requirePermission(...permissions: string[]) {
290
+ const requested = permissions.flat();
291
+ return (handler: UwsHandler): UwsHandler => async (res, req, request) => {
292
+ const snapshot = request ?? snapshotUwsRequest(res, req);
293
+ if (!snapshot.user) {
294
+ jsonResponse(res, 401, { error: "UNAUTHORIZED", message: "Authentication required" });
295
+ return;
296
+ }
297
+ if (!auth.can(snapshot.user, requested)) {
298
+ jsonResponse(res, 403, { error: "FORBIDDEN", message: "Missing permission" });
299
+ return;
300
+ }
301
+ return await handler(res, req, snapshot);
302
+ };
303
+ },
304
+
305
+ requireRole(...roles: string[]) {
306
+ const requested = roles.flat();
307
+ return (handler: UwsHandler): UwsHandler => async (res, req, request) => {
308
+ const snapshot = request ?? snapshotUwsRequest(res, req);
309
+ if (!snapshot.user) {
310
+ jsonResponse(res, 401, { error: "UNAUTHORIZED", message: "Authentication required" });
311
+ return;
312
+ }
313
+ if (!auth.hasRole(snapshot.user, requested)) {
314
+ jsonResponse(res, 403, { error: "FORBIDDEN", message: "Insufficient role" });
315
+ return;
316
+ }
317
+ return await handler(res, req, snapshot);
318
+ };
319
+ },
320
+
321
+ json: jsonResponse,
322
+ };
323
+ }
324
+
325
+ export function createAdapters(auth: Auth) {
326
+ return {
327
+ express: expressAdapter(auth),
328
+ fastify: fastifyAdapter(auth),
329
+ koa: koaAdapter(auth),
330
+ uws: uwsAdapter(auth),
331
+ uWebSockets: uwsAdapter(auth),
332
+ };
333
+ }