@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.
- package/ARCHITECTURE.md +465 -0
- package/CHANGELOG.md +214 -0
- package/LICENSE +21 -0
- package/README.md +647 -0
- package/dist/adapters.d.ts +51 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +301 -0
- package/dist/adapters.js.map +1 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +560 -0
- package/dist/auth.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +65 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +11 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +125 -0
- package/dist/jwt.js.map +1 -0
- package/dist/oauth.d.ts +31 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +178 -0
- package/dist/oauth.js.map +1 -0
- package/dist/password.d.ts +5 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +90 -0
- package/dist/password.js.map +1 -0
- package/dist/providers.d.ts +37 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +471 -0
- package/dist/providers.js.map +1 -0
- package/dist/rbac.d.ts +40 -0
- package/dist/rbac.d.ts.map +1 -0
- package/dist/rbac.js +240 -0
- package/dist/rbac.js.map +1 -0
- package/dist/roles.d.ts +2 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +2 -0
- package/dist/roles.js.map +1 -0
- package/dist/token.d.ts +2 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +2 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +383 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +192 -0
- package/dist/utils.js.map +1 -0
- package/examples/express.ts +111 -0
- package/examples/fastify.ts +59 -0
- package/examples/oauth-social.ts +83 -0
- package/examples/standalone.ts +67 -0
- package/examples/uwebsockets.ts +143 -0
- package/package.json +86 -0
- package/src/adapters.ts +333 -0
- package/src/auth.ts +684 -0
- package/src/errors.ts +76 -0
- package/src/index.ts +124 -0
- package/src/jwt.ts +159 -0
- package/src/oauth.ts +226 -0
- package/src/password.ts +111 -0
- package/src/providers.ts +551 -0
- package/src/rbac.ts +285 -0
- package/src/roles.ts +1 -0
- package/src/token.ts +1 -0
- package/src/types.ts +432 -0
- package/src/utils.ts +231 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mayank
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# @oneunit/auth
|
|
2
|
+
|
|
3
|
+
Framework-agnostic TypeScript authentication for Node.js. Use it from a plain script or plug it into Express, Fastify, Koa, or uWebSockets.js.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/mayank040902/framework/actions/workflows/auth.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@oneunit/auth)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Monorepo: https://github.com/mayank040902/framework
|
|
10
|
+
|
|
11
|
+
- JWT access and refresh tokens via `jsonwebtoken` (HS256 by default)
|
|
12
|
+
- Generic RBAC: you define roles and permissions
|
|
13
|
+
- Password hashing with scrypt
|
|
14
|
+
- Social login: Google, GitHub, Instagram, Facebook, X/Twitter, Discord, Apple, LinkedIn, Microsoft, Reddit, Twitch, Slack, Spotify, TikTok
|
|
15
|
+
- Works standalone or with HTTP frameworks
|
|
16
|
+
|
|
17
|
+
Requires Node.js 20+. Single runtime dependency: `jsonwebtoken`.
|
|
18
|
+
|
|
19
|
+
Docs: [README](./README.md) · [ARCHITECTURE](./ARCHITECTURE.md) · [CHANGELOG](./CHANGELOG.md) · [Security](../../docs/security.md)
|
|
20
|
+
|
|
21
|
+
Upgrading from 1.x? See the [2.0.0 migration notes](./CHANGELOG.md#migration).
|
|
22
|
+
|
|
23
|
+
## Table of contents
|
|
24
|
+
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [JWT helpers](#jwt-helpers)
|
|
28
|
+
- [RBAC](#rbac)
|
|
29
|
+
- [Passwords](#passwords)
|
|
30
|
+
- [Social login](#social-login)
|
|
31
|
+
- [Framework adapters](#framework-adapters)
|
|
32
|
+
- [Refresh tokens](#refresh-tokens)
|
|
33
|
+
- [Utilities](#utilities)
|
|
34
|
+
- [Errors](#errors)
|
|
35
|
+
- [API reference](#api-reference)
|
|
36
|
+
- [Examples](#examples)
|
|
37
|
+
- [Scripts](#scripts)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @oneunit/auth
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createAuth } from "@oneunit/auth";
|
|
50
|
+
|
|
51
|
+
const auth = createAuth({
|
|
52
|
+
secret: process.env.AUTH_SECRET,
|
|
53
|
+
issuer: "my-app",
|
|
54
|
+
accessTokenTtl: "15m",
|
|
55
|
+
refreshTokenTtl: "7d",
|
|
56
|
+
rbac: {
|
|
57
|
+
defaultRole: "member",
|
|
58
|
+
roles: {
|
|
59
|
+
member: { permissions: ["profile.read"] },
|
|
60
|
+
editor: { inherits: "member", permissions: ["post.write"] },
|
|
61
|
+
admin: { inherits: "editor", permissions: ["user.manage"] },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const { accessToken, refreshToken, payload } = await auth.login({
|
|
67
|
+
id: 42,
|
|
68
|
+
email: "ada@example.com",
|
|
69
|
+
roles: ["editor"],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const claims = await auth.verify(accessToken);
|
|
73
|
+
auth.can(claims, "post.write");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
There are no built-in product roles. Pass whatever role names your app uses.
|
|
77
|
+
|
|
78
|
+
## JWT helpers
|
|
79
|
+
|
|
80
|
+
Use these without creating an `Auth` instance:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { encode, decode, decodeUnsafe } from "@oneunit/auth";
|
|
84
|
+
|
|
85
|
+
const token = encode(
|
|
86
|
+
{ userId: 123 },
|
|
87
|
+
process.env.AUTH_SECRET,
|
|
88
|
+
{ expiresIn: "1h", subject: "123" },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const claims = decode(token, process.env.AUTH_SECRET);
|
|
92
|
+
|
|
93
|
+
// Decode without verification (inspect expired/untrusted tokens):
|
|
94
|
+
const unsafeClaims = decodeUnsafe(token);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`encodeAccessToken` and `encodeRefreshToken` add a `typ` claim (`access` or `refresh`).
|
|
98
|
+
|
|
99
|
+
## RBAC
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { createRBAC } from "@oneunit/auth";
|
|
103
|
+
|
|
104
|
+
const rbac = createRBAC({
|
|
105
|
+
roles: {
|
|
106
|
+
support: { permissions: ["ticket.read", "ticket.reply"] },
|
|
107
|
+
lead: { inherits: "support", permissions: ["ticket.assign"] },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
rbac.grant("lead", "ticket.close");
|
|
112
|
+
rbac.can({ roles: ["lead"] }, "ticket.reply");
|
|
113
|
+
rbac.authorize({ roles: ["support"] }, "ticket.read");
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`defineRoles` is a typed helper for declaring role definitions outside the constructor:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { defineRoles, createRBAC } from "@oneunit/auth";
|
|
120
|
+
|
|
121
|
+
const roles = defineRoles({
|
|
122
|
+
viewer: { permissions: ["doc.read"] },
|
|
123
|
+
editor: { inherits: "viewer", permissions: ["doc.write"] },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const rbac = createRBAC({ roles });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use `matchPermission` directly to test a single granted permission against a required one:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { matchPermission } from "@oneunit/auth";
|
|
133
|
+
|
|
134
|
+
matchPermission("invoice.*", "invoice.read"); // true
|
|
135
|
+
matchPermission("invoice.*", "invoice.read.all"); // false
|
|
136
|
+
matchPermission("invoice.**", "invoice.read.all"); // true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Permission wildcards:
|
|
140
|
+
|
|
141
|
+
| Granted | Matches | Does not match |
|
|
142
|
+
| :--- | :--- | :--- |
|
|
143
|
+
| `*` | anything | — |
|
|
144
|
+
| `invoice.*` | `invoice.read` | `invoice.read.all`, `invoices.read` |
|
|
145
|
+
| `invoice.**` | `invoice.read`, `invoice.read.all` | `payment.read` |
|
|
146
|
+
|
|
147
|
+
`*` stays within one dot-separated segment. `**` spans any number of segments.
|
|
148
|
+
|
|
149
|
+
Direct permissions on a user still work:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
rbac.can({ roles: ["member"], permissions: ["beta.access"] }, "beta.access");
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
These are evaluated against a subject you construct in code. A `permissions`
|
|
156
|
+
array on a stored user record is not copied into access tokens — see
|
|
157
|
+
[Access token claims](#access-token-claims).
|
|
158
|
+
|
|
159
|
+
> **`defaultRole` is a grant to anonymous callers.** A subject with no roles —
|
|
160
|
+
> including `null` and `undefined` — is assigned `defaultRole`, so
|
|
161
|
+
> `can(null, "post.write")` is `true` if `defaultRole` carries that permission.
|
|
162
|
+
> Keep `defaultRole` unprivileged, and check for a subject before asking:
|
|
163
|
+
>
|
|
164
|
+
> ```js
|
|
165
|
+
> if (ctx.state.user && auth.can(ctx.state.user, "post.write")) { ... }
|
|
166
|
+
> ```
|
|
167
|
+
>
|
|
168
|
+
> The bundled adapters check for a missing user before calling `can`.
|
|
169
|
+
|
|
170
|
+
## Passwords
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { hashPassword, verifyPassword, needsRehash } from "@oneunit/auth";
|
|
174
|
+
|
|
175
|
+
const passwordHash = await hashPassword("correct horse battery staple");
|
|
176
|
+
await verifyPassword("correct horse battery staple", passwordHash);
|
|
177
|
+
|
|
178
|
+
// Check if a stored hash needs upgrading (e.g. cost parameter changed):
|
|
179
|
+
if (needsRehash(passwordHash, { cost: 32768 })) {
|
|
180
|
+
const newHash = await hashPassword(password, { cost: 32768 });
|
|
181
|
+
// persist newHash
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
With a user store, `auth.register()` and `auth.loginWithPassword()` hash and verify for you.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const auth = createAuth({
|
|
189
|
+
secret: process.env.AUTH_SECRET,
|
|
190
|
+
userStore: {
|
|
191
|
+
async findByCredentials(identifier) {},
|
|
192
|
+
async create(input) {},
|
|
193
|
+
async updatePassword(id, passwordHash) {},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await auth.register({ email: "ada@example.com", password: "s3cret-pass" });
|
|
198
|
+
await auth.loginWithPassword("ada@example.com", "s3cret-pass");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`register()` ignores a `roles` field in its first argument so a public sign-up
|
|
202
|
+
form cannot self-assign a role. Pass roles as the second, server-side argument.
|
|
203
|
+
|
|
204
|
+
## Social login
|
|
205
|
+
|
|
206
|
+
Built-in providers: `google`, `github`, `instagram`, `facebook`, `twitter` (X), `discord`, `apple`, `linkedin`, `microsoft`, `reddit`, `twitch`, `slack`, `spotify`, `tiktok`.
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
const auth = createAuth({
|
|
210
|
+
secret: process.env.AUTH_SECRET,
|
|
211
|
+
providers: {
|
|
212
|
+
google: {
|
|
213
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
214
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
215
|
+
redirectUri: "http://localhost:3000/auth/google/callback",
|
|
216
|
+
},
|
|
217
|
+
github: {
|
|
218
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
219
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
220
|
+
redirectUri: "http://localhost:3000/auth/github/callback",
|
|
221
|
+
},
|
|
222
|
+
instagram: {
|
|
223
|
+
clientId: process.env.INSTAGRAM_CLIENT_ID,
|
|
224
|
+
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
|
|
225
|
+
redirectUri: "http://localhost:3000/auth/instagram/callback",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const { url, state } = await auth.getAuthorizationUrl("google");
|
|
231
|
+
|
|
232
|
+
const session = await auth.loginWithOAuth("google", {
|
|
233
|
+
code: req.query.code,
|
|
234
|
+
state: req.query.state,
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The callback params also accept a string: a full URL, a path with a query, a
|
|
239
|
+
leading `?code=...`, or a bare `"code=...&state=..."`.
|
|
240
|
+
|
|
241
|
+
### PKCE
|
|
242
|
+
|
|
243
|
+
Public clients should set `pkce: true`. The package generates and stores the
|
|
244
|
+
verifier for you. To manage it yourself, use the exported helpers:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { pkceVerifier, pkceChallenge } from "@oneunit/auth";
|
|
248
|
+
|
|
249
|
+
const verifier = pkceVerifier();
|
|
250
|
+
const challenge = pkceChallenge(verifier); // S256, base64url
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Provider user store
|
|
254
|
+
|
|
255
|
+
Optional user-store methods for linking accounts:
|
|
256
|
+
|
|
257
|
+
- `findByProvider(provider, providerId)`
|
|
258
|
+
- `findByEmail(email)`
|
|
259
|
+
- `createFromProvider(provider, profile, tokens)`
|
|
260
|
+
- `linkProvider(userId, provider, profile)`
|
|
261
|
+
|
|
262
|
+
If no store is configured, login still works and uses a synthetic id such as
|
|
263
|
+
`google:123`. That id is built from the provider profile's `id`, so
|
|
264
|
+
`profileMap` must map a stable identifier — a profile without one throws
|
|
265
|
+
`ProviderError` rather than collapsing every user of that provider onto the
|
|
266
|
+
same subject.
|
|
267
|
+
|
|
268
|
+
### Custom providers
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
import { createProvider, createOAuth } from "@oneunit/auth";
|
|
272
|
+
|
|
273
|
+
const acme = createProvider({
|
|
274
|
+
id: "acme",
|
|
275
|
+
authorizationUrl: "https://acme.example/oauth/authorize",
|
|
276
|
+
tokenUrl: "https://acme.example/oauth/token",
|
|
277
|
+
userInfoUrl: "https://acme.example/me",
|
|
278
|
+
scopes: ["profile"],
|
|
279
|
+
profileMap: { id: "id", email: "email", name: "name" },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const oauth = createOAuth();
|
|
283
|
+
oauth.use("acme", { provider: acme, clientId: "...", clientSecret: "..." });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
List all built-in provider definitions:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
import { builtinProviders, getProvider } from "@oneunit/auth";
|
|
290
|
+
|
|
291
|
+
console.log(Object.keys(builtinProviders)); // ["google", "github", ...]
|
|
292
|
+
const gh = getProvider("github");
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Framework adapters
|
|
296
|
+
|
|
297
|
+
The adapters never import a web framework. They read the request and response
|
|
298
|
+
objects you pass them structurally, so `@oneunit/auth` has no peer dependencies
|
|
299
|
+
at all and works whether or not Express, Fastify, Koa, or uWebSockets.js is
|
|
300
|
+
installed. Install your framework as usual alongside this package.
|
|
301
|
+
|
|
302
|
+
This also means the adapters are not tied to a framework's major version: they
|
|
303
|
+
depend on a small shape (`headers`, `cookies`, `query`, a `send`-style reply)
|
|
304
|
+
rather than on a class.
|
|
305
|
+
|
|
306
|
+
### Express
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
import { createAuth, expressAdapter } from "@oneunit/auth";
|
|
310
|
+
|
|
311
|
+
const auth = createAuth({ secret: process.env.AUTH_SECRET });
|
|
312
|
+
const { authenticate, requirePermission, requireRole } = expressAdapter(auth);
|
|
313
|
+
|
|
314
|
+
app.get("/me", authenticate(), (req, res) => res.json(req.user));
|
|
315
|
+
app.get("/admin", authenticate(), requireRole("admin"), handler);
|
|
316
|
+
app.get("/reports", authenticate(), requirePermission("report.read"), handler);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Fastify
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
import { createAuth, fastifyAdapter } from "@oneunit/auth";
|
|
323
|
+
|
|
324
|
+
await fastify.register(fastifyAdapter(auth));
|
|
325
|
+
fastify.get("/me", { preHandler: [fastify.authenticate()] }, async (req) => req.user);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Koa
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
import { createAuth, koaAdapter } from "@oneunit/auth";
|
|
332
|
+
|
|
333
|
+
const { authenticate, requirePermission } = koaAdapter(auth);
|
|
334
|
+
app.use(authenticate());
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### uWebSockets.js
|
|
338
|
+
|
|
339
|
+
uWS request/response objects are invalid after the first `await`. The adapter snapshots headers and query first.
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
import uWS from "uWebSockets.js";
|
|
343
|
+
import { createAuth, uwsAdapter } from "@oneunit/auth";
|
|
344
|
+
|
|
345
|
+
const auth = createAuth({ secret: process.env.AUTH_SECRET });
|
|
346
|
+
const { authenticate, requirePermission, json } = uwsAdapter(auth);
|
|
347
|
+
|
|
348
|
+
uWS.App()
|
|
349
|
+
.get("/me", authenticate()((res, _req, request) => {
|
|
350
|
+
json(res, 200, { user: request.user });
|
|
351
|
+
}))
|
|
352
|
+
.get("/stats", authenticate()(requirePermission("stats.read")((res, _req, request) => {
|
|
353
|
+
json(res, 200, { ok: true, userId: request.user.userId });
|
|
354
|
+
})))
|
|
355
|
+
.listen(3000, (token) => {
|
|
356
|
+
if (!token) throw new Error("listen failed");
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Use `snapshotUwsRequest` directly if you need the raw snapshot outside the adapter:
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
import { snapshotUwsRequest } from "@oneunit/auth";
|
|
364
|
+
|
|
365
|
+
const snapshot = snapshotUwsRequest(res, req);
|
|
366
|
+
// snapshot.headers, snapshot.query, snapshot.url are safe to use after await
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Generic / createAdapters
|
|
370
|
+
|
|
371
|
+
`createAdapters` generates all four adapter sets at once:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { createAuth, createAdapters } from "@oneunit/auth";
|
|
375
|
+
|
|
376
|
+
const auth = createAuth({ secret: process.env.AUTH_SECRET });
|
|
377
|
+
const { express, fastify, koa, uws } = createAdapters(auth);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Standalone HTTP
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
const claims = await auth.verifyRequest(req);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Tokens are read from `Authorization: Bearer`, an `access_token` cookie, or `?access_token=`.
|
|
387
|
+
|
|
388
|
+
You can also extract the bearer token yourself:
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
import { extractBearerToken } from "@oneunit/auth";
|
|
392
|
+
|
|
393
|
+
const token = extractBearerToken(req);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Refresh tokens
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
const session = await auth.login(user);
|
|
400
|
+
const next = await auth.refresh(session.refreshToken);
|
|
401
|
+
await auth.logout(next.refreshToken);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Refresh tokens rotate on every use: `auth.refresh()` invalidates the token it
|
|
405
|
+
consumes. A refresh token is never accepted by `verify()` or `verifyRequest()`,
|
|
406
|
+
so it cannot be replayed as an access credential.
|
|
407
|
+
|
|
408
|
+
### In-memory refresh store
|
|
409
|
+
|
|
410
|
+
For development and testing, use the built-in memory store:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
import { createAuth, createMemoryRefreshStore } from "@oneunit/auth";
|
|
414
|
+
|
|
415
|
+
const auth = createAuth({
|
|
416
|
+
secret: process.env.AUTH_SECRET,
|
|
417
|
+
refreshStore: createMemoryRefreshStore(),
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Custom refresh store
|
|
422
|
+
|
|
423
|
+
Persist tokens with a custom `refreshStore`:
|
|
424
|
+
|
|
425
|
+
| Method | Required | Purpose |
|
|
426
|
+
| --- | --- | --- |
|
|
427
|
+
| `save(record)` | yes | Store a newly issued refresh token |
|
|
428
|
+
| `get(id)` | yes | Look up a record |
|
|
429
|
+
| `consume(id)` | recommended | Atomically look up **and** remove a record |
|
|
430
|
+
| `revoke(id)` | required unless `consume` exists | Invalidate a record on logout |
|
|
431
|
+
|
|
432
|
+
Implement `consume` whenever you can. `get` followed by `revoke` is two
|
|
433
|
+
round-trips, so two concurrent requests presenting the same token can both
|
|
434
|
+
pass the validity check and each receive a new session. `consume` must be a
|
|
435
|
+
single atomic operation (`GETDEL` in Redis, a `DELETE ... RETURNING` in SQL).
|
|
436
|
+
|
|
437
|
+
`revoke` and `consume` are not optional in practice: if neither exists,
|
|
438
|
+
`auth.refresh()` and `auth.logout()` throw `ConfigurationError` rather than
|
|
439
|
+
report a logout that never happened. A store implementing only `consume()` is
|
|
440
|
+
enough for both rotation and logout.
|
|
441
|
+
|
|
442
|
+
### Access token claims
|
|
443
|
+
|
|
444
|
+
Permissions in an access token are derived from RBAC roles. A `permissions`
|
|
445
|
+
array on the user record is ignored unless you opt in:
|
|
446
|
+
|
|
447
|
+
```js
|
|
448
|
+
const auth = createAuth({ secret, trustUserPermissions: true });
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Roles assigned during registration come from the server-side options argument,
|
|
452
|
+
never the request body:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
await auth.register({ email, password }, { roles: ["member"] });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Reserved claims
|
|
459
|
+
|
|
460
|
+
`sub`, `userId`, `roles`, `permissions`, `typ`, `iss`, `aud`, `exp`, `iat`,
|
|
461
|
+
`nbf`, and `jti` are derived from the authenticated user and RBAC, and cannot be
|
|
462
|
+
replaced by a claim extractor or by `additionalClaims`. Doing so throws rather
|
|
463
|
+
than silently minting a token for someone else:
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
auth.registerExtractor("sub", fn); // throws
|
|
467
|
+
auth.login(user, { additionalClaims: { roles: ["admin"] } }); // throws
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Any other name is yours — `name`, `email`, `tier`, `tenantId`, and so on.
|
|
471
|
+
|
|
472
|
+
## Utilities
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
import {
|
|
476
|
+
isValidExpiresIn,
|
|
477
|
+
parseExpiresIn,
|
|
478
|
+
randomToken,
|
|
479
|
+
randomState,
|
|
480
|
+
} from "@oneunit/auth";
|
|
481
|
+
|
|
482
|
+
isValidExpiresIn("15m"); // true
|
|
483
|
+
isValidExpiresIn("1y"); // false — ambiguous unit
|
|
484
|
+
|
|
485
|
+
parseExpiresIn("7d"); // 604800 (seconds)
|
|
486
|
+
|
|
487
|
+
const token = randomToken(); // cryptographically random hex string
|
|
488
|
+
const state = randomState(); // for OAuth state parameter
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### OAuth state store
|
|
492
|
+
|
|
493
|
+
For development, `createMemoryStateStore()` keeps OAuth state in memory:
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
import { createMemoryStateStore } from "@oneunit/auth";
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Errors
|
|
500
|
+
|
|
501
|
+
All errors extend `AuthError` and include `code` and `status`:
|
|
502
|
+
|
|
503
|
+
| Class | Code | Status |
|
|
504
|
+
| --- | --- | --- |
|
|
505
|
+
| `InvalidTokenError` | `INVALID_TOKEN` | 401 |
|
|
506
|
+
| `TokenExpiredError` | `TOKEN_EXPIRED` | 401 |
|
|
507
|
+
| `UnauthorizedError` | `UNAUTHORIZED` | 401 |
|
|
508
|
+
| `ForbiddenError` | `FORBIDDEN` | 403 |
|
|
509
|
+
| `ConfigurationError` | `CONFIGURATION_ERROR` | 500 |
|
|
510
|
+
| `OAuthError` | `OAUTH_ERROR` | 401 |
|
|
511
|
+
| `ProviderError` | `PROVIDER_ERROR` | 502 |
|
|
512
|
+
| `ValidationError` | `VALIDATION_ERROR` | 400 |
|
|
513
|
+
|
|
514
|
+
## API reference
|
|
515
|
+
|
|
516
|
+
Every named export from `@oneunit/auth`:
|
|
517
|
+
|
|
518
|
+
### Core
|
|
519
|
+
|
|
520
|
+
| Export | Kind | Description |
|
|
521
|
+
| :--- | :--- | :--- |
|
|
522
|
+
| `createAuth` | function | Create an `Auth` instance with JWT, RBAC, OAuth, and password support |
|
|
523
|
+
| `Auth` | class | The auth instance class |
|
|
524
|
+
| `auth` | function | Alias for `createAuth` |
|
|
525
|
+
|
|
526
|
+
### JWT
|
|
527
|
+
|
|
528
|
+
| Export | Kind | Description |
|
|
529
|
+
| :--- | :--- | :--- |
|
|
530
|
+
| `encode` | function | Sign a JWT payload |
|
|
531
|
+
| `decode` | function | Verify and decode a JWT |
|
|
532
|
+
| `decodeUnsafe` | function | Decode a JWT without verification |
|
|
533
|
+
| `encodeAccessToken` | function | Sign a JWT with `typ: "access"` |
|
|
534
|
+
| `encodeRefreshToken` | function | Sign a JWT with `typ: "refresh"` |
|
|
535
|
+
|
|
536
|
+
### RBAC
|
|
537
|
+
|
|
538
|
+
| Export | Kind | Description |
|
|
539
|
+
| :--- | :--- | :--- |
|
|
540
|
+
| `createRBAC` | function | Create an RBAC instance |
|
|
541
|
+
| `RBAC` | class | The RBAC class |
|
|
542
|
+
| `defineRoles` | function | Typed helper for role definitions |
|
|
543
|
+
| `matchPermission` | function | Test a granted permission against a required one |
|
|
544
|
+
|
|
545
|
+
### Passwords
|
|
546
|
+
|
|
547
|
+
| Export | Kind | Description |
|
|
548
|
+
| :--- | :--- | :--- |
|
|
549
|
+
| `hashPassword` | function | Hash a password with scrypt |
|
|
550
|
+
| `verifyPassword` | function | Verify a password against a hash |
|
|
551
|
+
| `needsRehash` | function | Check if a hash needs upgrading |
|
|
552
|
+
|
|
553
|
+
### OAuth
|
|
554
|
+
|
|
555
|
+
| Export | Kind | Description |
|
|
556
|
+
| :--- | :--- | :--- |
|
|
557
|
+
| `createOAuth` | function | Create a standalone OAuth manager |
|
|
558
|
+
| `OAuth` | class | The OAuth class |
|
|
559
|
+
| `createProvider` | function | Define a custom OAuth provider |
|
|
560
|
+
| `getProvider` | function | Look up a built-in provider by name |
|
|
561
|
+
| `builtinProviders` | object | Map of all built-in provider definitions |
|
|
562
|
+
| `pkceVerifier` | function | Generate a PKCE code verifier |
|
|
563
|
+
| `pkceChallenge` | function | Compute S256 PKCE challenge |
|
|
564
|
+
| `createMemoryStateStore` | function | In-memory OAuth state store |
|
|
565
|
+
|
|
566
|
+
### Built-in providers
|
|
567
|
+
|
|
568
|
+
`google`, `github`, `instagram`, `facebook`, `twitter`, `discord`, `apple`, `linkedin`, `microsoft`, `reddit`, `twitch`, `slack`, `spotify`, `tiktok` — each exported as a provider definition object.
|
|
569
|
+
|
|
570
|
+
### Framework adapters
|
|
571
|
+
|
|
572
|
+
| Export | Kind | Description |
|
|
573
|
+
| :--- | :--- | :--- |
|
|
574
|
+
| `expressAdapter` | function | Express middleware factory |
|
|
575
|
+
| `fastifyAdapter` | function | Fastify plugin factory |
|
|
576
|
+
| `koaAdapter` | function | Koa middleware factory |
|
|
577
|
+
| `uwsAdapter` | function | uWebSockets.js adapter factory |
|
|
578
|
+
| `snapshotUwsRequest` | function | Snapshot a uWS request for use after `await` |
|
|
579
|
+
| `createAdapters` | function | Create all four adapters at once |
|
|
580
|
+
|
|
581
|
+
### Stores
|
|
582
|
+
|
|
583
|
+
| Export | Kind | Description |
|
|
584
|
+
| :--- | :--- | :--- |
|
|
585
|
+
| `createMemoryRefreshStore` | function | In-memory refresh token store (dev/test) |
|
|
586
|
+
|
|
587
|
+
### Utilities
|
|
588
|
+
|
|
589
|
+
| Export | Kind | Description |
|
|
590
|
+
| :--- | :--- | :--- |
|
|
591
|
+
| `parseExpiresIn` | function | Parse a TTL string to seconds |
|
|
592
|
+
| `isValidExpiresIn` | function | Validate a TTL string or number |
|
|
593
|
+
| `randomToken` | function | Cryptographically random hex token |
|
|
594
|
+
| `randomState` | function | Random string for OAuth state |
|
|
595
|
+
| `extractBearerToken` | function | Extract a bearer token from a request |
|
|
596
|
+
|
|
597
|
+
### Errors
|
|
598
|
+
|
|
599
|
+
`AuthError`, `InvalidTokenError`, `TokenExpiredError`, `UnauthorizedError`, `ForbiddenError`, `ConfigurationError`, `OAuthError`, `ProviderError`, `ValidationError`.
|
|
600
|
+
|
|
601
|
+
### Types
|
|
602
|
+
|
|
603
|
+
`AuthErrorOptions`, `JwtSignOptions`, `JwtVerifyOptions`, `JwtPayload`, `Secret`, `RoleDefinition`, `RBACOptions`, `AuthSubject`, `PasswordOptions`, `OAuthProfile`, `OAuthTokens`, `OAuthProviderConfig`, `OAuthProvider`, `ProviderDefinition`, `StateStore`, `OAuthOptions`, `OAuthAuthorizeOptions`, `UserRecord`, `UserStore`, `RefreshRecord`, `RefreshStore`, `LoginOptions`, `LoginResult`, `AuthOptions`, `RequestLike`, `ExtractTokenOptions`, `UwsHttpResponse`, `UwsHttpRequest`, `UwsRequestSnapshot`, `UwsHandler`, `ExpressRequestLike`, `ExpressResponseLike`, `ExpressNext`, `KoaContextLike`, `FastifyLike`, `FastifyRequestLike`, `FastifyReplyLike`.
|
|
604
|
+
|
|
605
|
+
## Examples
|
|
606
|
+
|
|
607
|
+
| File | Shows |
|
|
608
|
+
| :--- | :--- |
|
|
609
|
+
| `examples/standalone.ts` | Login, rotation, replay rejection, wildcards, TTL validation |
|
|
610
|
+
| `examples/express.ts` | Middleware, `optional` auth, refresh and logout routes, OAuth |
|
|
611
|
+
| `examples/fastify.ts` | Plugin registration, plugin-level `optional`, preHandlers |
|
|
612
|
+
| `examples/uwebsockets.ts` | Abort-safe handlers, request snapshots, provider lookup |
|
|
613
|
+
| `examples/oauth-social.ts` | Provider config, PKCE, state handling |
|
|
614
|
+
|
|
615
|
+
They import from `@oneunit/auth`, so they run against a real install:
|
|
616
|
+
|
|
617
|
+
```bash
|
|
618
|
+
npm run example:standalone
|
|
619
|
+
npm run example:oauth
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
From an installed copy:
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
npx tsx node_modules/@oneunit/auth/examples/standalone.ts
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
The OAuth example needs `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`; it exits
|
|
629
|
+
with a note otherwise, because provider config is validated at `authorize()`
|
|
630
|
+
time rather than at `createAuth()` time.
|
|
631
|
+
|
|
632
|
+
## Scripts
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
npm test
|
|
636
|
+
npm run typecheck
|
|
637
|
+
npm run build
|
|
638
|
+
npm run pack:check
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
CI runs typecheck, tests, build, `pack:check`, and `pnpm audit` on Node 20, 22,
|
|
642
|
+
and 24, then installs the packed tarball into a clean project and exercises the
|
|
643
|
+
public API and the shipped examples against it.
|
|
644
|
+
|
|
645
|
+
## License
|
|
646
|
+
|
|
647
|
+
MIT. Copyright (c) 2026 mayank.
|