ajo-kit-auth 0.1.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 +15 -0
- package/README.md +269 -0
- package/dist/ability.js +47 -0
- package/dist/index.js +629 -0
- package/dist/migrations/0001_initial.js +28 -0
- package/package.json +65 -0
- package/src/ability.client.ts +77 -0
- package/src/account.ts +42 -0
- package/src/confirm.ts +54 -0
- package/src/cookie.ts +34 -0
- package/src/csrf.ts +80 -0
- package/src/guard.ts +96 -0
- package/src/index.ts +32 -0
- package/src/limit.ts +38 -0
- package/src/password.ts +12 -0
- package/src/reset.ts +40 -0
- package/src/secret.ts +18 -0
- package/src/session.ts +150 -0
- package/src/store.ts +13 -0
- package/src/token.ts +77 -0
- package/src/types.ts +78 -0
- package/src/verify.ts +44 -0
- package/src/wares.ts +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026, Cristian Falcone
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
14
|
+
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# ajo-kit-auth
|
|
2
|
+
|
|
3
|
+
Authentication and authorization for `ajo-kit` apps.
|
|
4
|
+
|
|
5
|
+
Includes:
|
|
6
|
+
|
|
7
|
+
- session auth (cookie)
|
|
8
|
+
- bearer tokens with abilities
|
|
9
|
+
- CSRF middleware
|
|
10
|
+
- route guards
|
|
11
|
+
- in-memory rate limiting
|
|
12
|
+
- password reset tokens
|
|
13
|
+
- email verification signatures
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add ajo-kit-auth
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`ajo-kit-auth` requires `ajo-kit` as a peer dependency.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
### 1. Configure DB accessor
|
|
26
|
+
|
|
27
|
+
Call `configure()` once during app boot so auth modules can access your Kysely instance.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { configure } from '@kit/auth'
|
|
31
|
+
import { db } from '/src/data'
|
|
32
|
+
|
|
33
|
+
configure(() => db())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Run migrations
|
|
37
|
+
|
|
38
|
+
`ajo-kit-auth` exposes `kit.migrations`, so with the package installed:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
kit migrate up
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This creates auth tables (`users`, `sessions`, `roles`, `members`, `tokens`, `resets`).
|
|
45
|
+
|
|
46
|
+
### 3. Register auth middlewares
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/wares.ts
|
|
50
|
+
import { wares } from '@kit/auth'
|
|
51
|
+
|
|
52
|
+
export default [wares.session(), wares.csrf]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`session()` resolves `req.user` from cookies. Bearer tokens authenticate
|
|
56
|
+
`/api/*` routes, where an explicit Bearer token takes precedence over a session
|
|
57
|
+
cookie.
|
|
58
|
+
|
|
59
|
+
`csrf` validates unsafe cookie-auth requests, including `/api/*`. It skips safe
|
|
60
|
+
methods, bearer-token requests, and unauthenticated API requests.
|
|
61
|
+
|
|
62
|
+
### 4. Set secret for verification links
|
|
63
|
+
|
|
64
|
+
```env
|
|
65
|
+
APP_SECRET=<32+ random characters from your secret manager>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Development can run without this value. Production fails closed when
|
|
69
|
+
`APP_SECRET` is missing, too short, or left as a sample placeholder.
|
|
70
|
+
|
|
71
|
+
For non-local production, also configure `APP_URL` in the app environment so
|
|
72
|
+
same-origin checks and generated links use the trusted public origin.
|
|
73
|
+
|
|
74
|
+
## Main Exports
|
|
75
|
+
|
|
76
|
+
The package root exports the APIs below.
|
|
77
|
+
|
|
78
|
+
### `password`
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { password } from '@kit/auth'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Argon2id hash/verify helpers.
|
|
85
|
+
|
|
86
|
+
### `session`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { session } from '@kit/auth'
|
|
90
|
+
|
|
91
|
+
const id = await session.create(user, remember, ip, agent)
|
|
92
|
+
const active = await session.validate(id)
|
|
93
|
+
await session.touch(id)
|
|
94
|
+
await session.remove(id)
|
|
95
|
+
await session.prune()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`create()` returns the plaintext cookie value. The database stores only a
|
|
99
|
+
SHA-256 hash of that value in `sessions.id`.
|
|
100
|
+
|
|
101
|
+
Session lifetime is 30 days by default or 365 days with `remember = true`.
|
|
102
|
+
`validate()` enforces a 30-minute idle timeout, removes expired sessions, and
|
|
103
|
+
updates `last` at most once every 5 minutes.
|
|
104
|
+
|
|
105
|
+
Pass `activity = false` for background checks such as SSE freshness. `prune()`
|
|
106
|
+
removes expired rows.
|
|
107
|
+
|
|
108
|
+
### `cookie`
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { cookie } from '@kit/auth'
|
|
112
|
+
|
|
113
|
+
const id = cookie.read(req)
|
|
114
|
+
cookie.write(res, id, remember)
|
|
115
|
+
cookie.clear(res)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Cookie name is `session`, with `HttpOnly; SameSite=Lax; Path=/` and `Secure`
|
|
119
|
+
in production.
|
|
120
|
+
|
|
121
|
+
### `csrf`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { csrf } from '@kit/auth'
|
|
125
|
+
|
|
126
|
+
const token = csrf.set(req, res)
|
|
127
|
+
const ok = csrf.verify(req)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Verification accepts:
|
|
131
|
+
|
|
132
|
+
- signed double-submit bound to the current session
|
|
133
|
+
(`XSRF-TOKEN` cookie + `X-XSRF-TOKEN` header)
|
|
134
|
+
- same-origin check (`Origin`/`Referer` host matches request host)
|
|
135
|
+
|
|
136
|
+
### `wares`
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { wares } from '@kit/auth'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`session(lookup?)` accepts an optional custom user resolver. Bearer token auth is scoped to `/api/*`; route actions use cookie sessions and CSRF.
|
|
143
|
+
|
|
144
|
+
### Guards
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import {
|
|
148
|
+
ability,
|
|
149
|
+
auth,
|
|
150
|
+
authorize,
|
|
151
|
+
confirmed,
|
|
152
|
+
guest,
|
|
153
|
+
guard,
|
|
154
|
+
protect,
|
|
155
|
+
redirect,
|
|
156
|
+
verified,
|
|
157
|
+
when,
|
|
158
|
+
} from '@kit/auth'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `auth()` requires an authenticated user.
|
|
162
|
+
- `authorize(req, ...abilities)` checks account and bearer-token abilities.
|
|
163
|
+
- `ability(...abilities)` requires account abilities; bearer requests must
|
|
164
|
+
also carry them on the token.
|
|
165
|
+
- `protect('/login')` redirects guests.
|
|
166
|
+
- `guest('/dashboard')` redirects authenticated users.
|
|
167
|
+
- `confirmed(window?)` requires recent password confirmation.
|
|
168
|
+
- `verified()` requires a `users.verified` timestamp.
|
|
169
|
+
- `when(condition, middleware, otherwise?)` selects middleware by request.
|
|
170
|
+
- `redirect(target)` returns an AJAX-aware redirect middleware.
|
|
171
|
+
|
|
172
|
+
The same guard functions are available through the `guard` namespace.
|
|
173
|
+
|
|
174
|
+
### `token`
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { can, token } from '@kit/auth'
|
|
178
|
+
|
|
179
|
+
const plain = await token.create(user, 'My token', ['posts:*'])
|
|
180
|
+
const valid = await token.validate(plain)
|
|
181
|
+
const canWrite = can(valid?.abilities ?? [], 'posts:write')
|
|
182
|
+
await token.revoke(plain)
|
|
183
|
+
await token.purge(user)
|
|
184
|
+
const all = await token.list(user)
|
|
185
|
+
await token.prune()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Abilities support `*`, exact matches, and resource wildcards like `posts:*`.
|
|
189
|
+
`token.create()` requires explicit abilities; app routes should bound requested
|
|
190
|
+
abilities by the authenticated account and bearer token before creating one.
|
|
191
|
+
|
|
192
|
+
Browser code imports ability helpers from the client-safe subpath:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { all, can, compact, intersect, merge } from '@kit/auth/ability'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `account`
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { account } from '@kit/auth'
|
|
202
|
+
|
|
203
|
+
const grants = await account.grants(user)
|
|
204
|
+
const abilities = await account.abilities(user)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`account.grants(user)` loads assigned role grants. `account.abilities(user)`
|
|
208
|
+
merges them and removes duplicate or redundant wildcard grants. Authorize with
|
|
209
|
+
abilities through `ability()` or `authorize()`.
|
|
210
|
+
|
|
211
|
+
### `limit`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { limit } from '@kit/auth'
|
|
215
|
+
|
|
216
|
+
if (!limit.check(ip)) throw new Error('Too many attempts')
|
|
217
|
+
limit.hit(ip, 60_000)
|
|
218
|
+
limit.remaining(ip)
|
|
219
|
+
limit.clear(ip)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The limiter stores counters in process memory. Multi-process deployments
|
|
223
|
+
require a shared limiter.
|
|
224
|
+
|
|
225
|
+
### `confirm`
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { confirm } from '@kit/auth'
|
|
229
|
+
|
|
230
|
+
confirm.stamp(req)
|
|
231
|
+
confirm.check(req, 180_000)
|
|
232
|
+
confirm.clear(req)
|
|
233
|
+
confirm.clearSession(user, sessionId)
|
|
234
|
+
confirm.clearToken(user, tokenId)
|
|
235
|
+
confirm.clearUser(user)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Tracks recent password confirmation in memory, scoped to the current session or
|
|
239
|
+
bearer token credential.
|
|
240
|
+
|
|
241
|
+
### `reset`
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { reset } from '@kit/auth'
|
|
245
|
+
|
|
246
|
+
const plain = await reset.create(user)
|
|
247
|
+
const user = await reset.validate(plain)
|
|
248
|
+
await reset.prune()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Reset tokens are SHA-256 hashed in DB and expire in 1 hour.
|
|
252
|
+
|
|
253
|
+
### `verify`
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { verify } from '@kit/auth'
|
|
257
|
+
|
|
258
|
+
const link = verify.url(user, 'https://example.com')
|
|
259
|
+
const verifiedUser = verify.validate(signature)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
HMAC-SHA256 signed token, default expiry 24 hours. Production requires a strong
|
|
263
|
+
`APP_SECRET`.
|
|
264
|
+
|
|
265
|
+
## Types
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import type { Ability, Auth, New, Role, Session, Token, User } from '@kit/auth'
|
|
269
|
+
```
|
package/dist/ability.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region packages/ajo-kit-auth/src/ability.client.ts
|
|
2
|
+
var full = "*";
|
|
3
|
+
var wildcarded = (ability) => ability === full ? null : ability.endsWith(":*") ? ability.slice(0, -2) : null;
|
|
4
|
+
/** Returns true when grants include the required ability. */
|
|
5
|
+
function can(grants, required) {
|
|
6
|
+
if (!grants) return false;
|
|
7
|
+
if (grants.includes(full)) return true;
|
|
8
|
+
if (grants.includes(required)) return true;
|
|
9
|
+
const [resource] = required.split(":");
|
|
10
|
+
return grants.includes(`${resource}:*`);
|
|
11
|
+
}
|
|
12
|
+
/** Returns true when grants include every required ability. */
|
|
13
|
+
var all = (grants, required) => required.every((ability) => can(grants, ability));
|
|
14
|
+
/** Removes duplicate and redundant grants while preserving stable order. */
|
|
15
|
+
function compact(grants) {
|
|
16
|
+
const unique = [...new Set(grants)];
|
|
17
|
+
if (unique.includes(full)) return [full];
|
|
18
|
+
const resources = new Set(unique.map(wildcarded).filter((resource) => resource !== null));
|
|
19
|
+
return unique.filter((grant) => {
|
|
20
|
+
if (wildcarded(grant)) return true;
|
|
21
|
+
return !resources.has(grant.split(":")[0]);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Merges ability grant sets with duplicate and wildcard compaction. */
|
|
25
|
+
var merge = (...sets) => compact(sets.flat());
|
|
26
|
+
/** Returns the effective grants allowed by both grant sets. */
|
|
27
|
+
function intersect(left, right) {
|
|
28
|
+
const grants = [];
|
|
29
|
+
for (const a of compact(left)) for (const b of compact(right)) {
|
|
30
|
+
const grant = overlap(a, b);
|
|
31
|
+
if (grant) grants.push(grant);
|
|
32
|
+
}
|
|
33
|
+
return compact(grants);
|
|
34
|
+
}
|
|
35
|
+
function overlap(left, right) {
|
|
36
|
+
if (left === full) return right;
|
|
37
|
+
if (right === full) return left;
|
|
38
|
+
if (left === right) return left;
|
|
39
|
+
const l = wildcarded(left);
|
|
40
|
+
const r = wildcarded(right);
|
|
41
|
+
if (l && r) return l === r ? left : null;
|
|
42
|
+
if (l && can([left], right)) return right;
|
|
43
|
+
if (r && can([right], left)) return left;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { all, can, compact, intersect, merge };
|