@gauts/auth 0.4.0 → 0.5.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/README.md +495 -497
- package/dist/adapters/hono/index.d.ts +2 -2
- package/dist/adapters/hono/index.d.ts.map +1 -1
- package/dist/adapters/hono/index.js +9 -3
- package/dist/adapters/hono/index.js.map +1 -1
- package/dist/adapters/prisma/index.d.ts +18 -8
- package/dist/adapters/prisma/index.d.ts.map +1 -1
- package/dist/adapters/prisma/index.js +127 -43
- package/dist/adapters/prisma/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,300 +1,121 @@
|
|
|
1
1
|
# `@gauts/auth`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Database-backed password authentication and opaque browser sessions for Node.js applications.
|
|
4
|
+
|
|
5
|
+
`@gauts/auth` provides the reusable authentication layer: password hashing, session lifecycle, secure cookies, database validation, optional short caching, and framework adapters. The application keeps control of registration, account lookup, authorization, routes, responses, and UI.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
| Capability | Support | Default |
|
|
10
|
+
| ---------------------------------- | :-----: | ------------------- |
|
|
11
|
+
| Argon2id password hashing | ✅ | Enabled |
|
|
12
|
+
| bcrypt password hashing | ✅ | Opt-in |
|
|
13
|
+
| Opaque server-side sessions | ✅ | Enabled |
|
|
14
|
+
| Database-backed validation | ✅ | Enabled |
|
|
15
|
+
| Sliding session renewal | ✅ | Every 24 hours |
|
|
16
|
+
| Absolute session lifetime | ✅ | 30 days |
|
|
17
|
+
| Signed browser cache | ✅ | Disabled |
|
|
18
|
+
| Full User-Agent validation | ✅ | Enabled |
|
|
19
|
+
| IP validation | ✅ | Disabled |
|
|
20
|
+
| Platform validation | ✅ | Disabled |
|
|
21
|
+
| Hono adapter | ✅ | Available |
|
|
22
|
+
| Prisma adapter | ✅ | Available |
|
|
23
|
+
| Next.js renewal adapter | ✅ | Available |
|
|
24
|
+
| Session listing and revocation | ✅ | Available |
|
|
25
|
+
| Token rotation | ❌ | Stable opaque token |
|
|
26
|
+
| JWT sessions | ❌ | Not used |
|
|
27
|
+
| Redis requirement | ❌ | Not required |
|
|
28
|
+
| Registration, OAuth, OTP, or email | ❌ | Application-owned |
|
|
29
|
+
| Route roles and permissions | ❌ | Application-owned |
|
|
30
|
+
|
|
31
|
+
“Session renewal” extends the existing session expiry when activity continues. It is not a refresh-token flow and does not rotate the opaque browser token.
|
|
4
32
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```text
|
|
8
|
-
session cookie -> opaque token
|
|
9
|
-
cache cookie -> short signed session snapshot
|
|
10
|
-
renew cookie -> untrusted renewAt timestamp
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Only the opaque session token authenticates. It remains stable during sliding renewal, but the session can never exceed its configured maximum lifetime. The optional cache is disabled when omitted and never replaces database validation for unsafe methods, renewal, logout, WebSockets, or direct core calls.
|
|
33
|
+
## Installation
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
### Requirements
|
|
16
36
|
|
|
17
37
|
- Node.js 22 or newer.
|
|
18
|
-
- A database adapter
|
|
19
|
-
- Hono 4 when using
|
|
20
|
-
- Next.js 15 or newer when using
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
38
|
+
- A database adapter.
|
|
39
|
+
- Hono 4 when using the Hono adapter.
|
|
40
|
+
- Next.js 15 or newer when using the Next.js adapter.
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
### Hono and Prisma
|
|
25
43
|
|
|
26
44
|
```bash
|
|
27
45
|
npm install @gauts/auth hono @prisma/client
|
|
28
46
|
```
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
### Next.js
|
|
31
49
|
|
|
32
50
|
```bash
|
|
33
51
|
npm install @gauts/auth next
|
|
34
52
|
```
|
|
35
53
|
|
|
36
|
-
`hono` and `next` are optional peer dependencies. The Prisma adapter receives the
|
|
37
|
-
|
|
38
|
-
## Package entry points
|
|
39
|
-
|
|
40
|
-
| Import | Purpose |
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| `@gauts/auth` | Framework-independent password and session core types. |
|
|
43
|
-
| `@gauts/auth/prisma` | Prisma implementation of the database adapter. |
|
|
44
|
-
| `@gauts/auth/hono` | Hono cookies, session methods, and middleware. |
|
|
45
|
-
| `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
|
|
46
|
-
|
|
47
|
-
## Flow
|
|
48
|
-
|
|
49
|
-
Login:
|
|
50
|
-
|
|
51
|
-
```text
|
|
52
|
-
credentials accepted
|
|
53
|
-
-> generate 256-bit opaque token
|
|
54
|
-
-> store SHA-256 token hash in DB
|
|
55
|
-
-> load current account and user relations
|
|
56
|
-
-> validate configured status and role rules
|
|
57
|
-
-> write opaque session token cookie
|
|
58
|
-
-> write renewAt as Unix seconds
|
|
59
|
-
-> optionally write signed short cache
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Protected `GET` or `HEAD`:
|
|
63
|
-
|
|
64
|
-
```text
|
|
65
|
-
opaque session token
|
|
66
|
-
-> valid signed cache bound to token and client?
|
|
67
|
-
-> yes: expose cached session, account, and user
|
|
68
|
-
-> no: perform indexed DB validation and issue a fresh cache
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Unsafe methods and direct core calls:
|
|
72
|
-
|
|
73
|
-
```text
|
|
74
|
-
opaque session token
|
|
75
|
-
-> SHA-256 token
|
|
76
|
-
-> indexed DB lookup
|
|
77
|
-
-> expiry, revocation, account, user, and client validation
|
|
78
|
-
-> clear any existing cache after successful unsafe validation
|
|
79
|
-
-> route
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Renewal:
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
Next reads renewAt from the renewal cookie
|
|
86
|
-
-> valid future Unix timestamp: no API call
|
|
87
|
-
-> missing, invalid, or due timestamp: POST /auth/renew
|
|
88
|
-
-> API performs full DB validation
|
|
89
|
-
-> DB expires_at = min(now + ttl, created_at + maxLifetime)
|
|
90
|
-
-> Set-Cookie with same token, a new renewAt, and a fresh cache
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
`auth.session.resolve()` is always DB-backed and read-only. Only the explicit renewal operation updates database expiry, and neither activity nor renewal can extend the session beyond `maxLifetime`.
|
|
94
|
-
|
|
95
|
-
## Prisma schema contract
|
|
96
|
-
|
|
97
|
-
The Prisma adapter resolves authentication through this relation chain:
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
account_sessions -> account -> user
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The default Prisma client delegate is `account_sessions`. A different session model delegate can be selected through `config.table`.
|
|
104
|
-
|
|
105
|
-
### Complete minimal schema
|
|
106
|
-
|
|
107
|
-
This is a complete MySQL/MariaDB example. If `users` and `user_accounts` already exist, merge the required fields and relations into those models instead of duplicating them.
|
|
108
|
-
|
|
109
|
-
```prisma
|
|
110
|
-
model users {
|
|
111
|
-
id String @id @default(uuid()) @db.VarChar(255)
|
|
112
|
-
role String @db.VarChar(255)
|
|
113
|
-
status String @db.VarChar(255)
|
|
114
|
-
|
|
115
|
-
accounts user_accounts[]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
model user_accounts {
|
|
119
|
-
id String @id @default(uuid()) @db.VarChar(255)
|
|
120
|
-
user_id String @db.VarChar(255)
|
|
121
|
-
email String @db.VarChar(255)
|
|
122
|
-
name String @db.VarChar(255)
|
|
123
|
-
role String @db.VarChar(255)
|
|
124
|
-
status String @db.VarChar(255)
|
|
125
|
-
timezone String? @db.VarChar(255)
|
|
126
|
-
|
|
127
|
-
user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
128
|
-
sessions account_sessions[]
|
|
129
|
-
|
|
130
|
-
@@index([user_id])
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
model account_sessions {
|
|
134
|
-
id String @id @default(uuid()) @db.VarChar(255)
|
|
135
|
-
account_id String @db.VarChar(255)
|
|
136
|
-
token_hash String @unique @db.VarChar(64)
|
|
137
|
-
ip String? @db.VarChar(45)
|
|
138
|
-
platform String? @db.VarChar(255)
|
|
139
|
-
agent String? @db.Text
|
|
140
|
-
expires_at DateTime @db.Timestamp(0)
|
|
141
|
-
revoked_at DateTime? @db.Timestamp(0)
|
|
142
|
-
created_at DateTime @default(now()) @db.Timestamp(0)
|
|
143
|
-
updated_at DateTime? @db.Timestamp(0)
|
|
144
|
-
|
|
145
|
-
account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
|
|
146
|
-
|
|
147
|
-
@@index([account_id])
|
|
148
|
-
@@index([expires_at])
|
|
149
|
-
@@index([revoked_at])
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
The adapter requires these Prisma field and relation names:
|
|
154
|
-
|
|
155
|
-
| Path | Required fields |
|
|
156
|
-
| --- | --- |
|
|
157
|
-
| Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
|
|
158
|
-
| `account` relation | `id`, `email`, `name`, `role`, `status`, `timezone` |
|
|
159
|
-
| `account.user` relation | `id`, `role`, `status` |
|
|
160
|
-
|
|
161
|
-
The relation fields must be named `account` and `user`, because those are the names selected by the adapter. Prisma also requires the inverse relations; their field names (`sessions` and `accounts` above) can be changed because the adapter never queries them.
|
|
162
|
-
|
|
163
|
-
Application models may contain additional fields, defaults, indexes, and relations. `role` and `status` may use application-specific Prisma enums instead of `String`; Prisma returns both as strings to the adapter. For other database providers, replace the native `@db.*` annotations with compatible types and keep `agent` large enough to store the complete User-Agent.
|
|
54
|
+
`hono` and `next` are optional peer dependencies. The Prisma adapter receives the application's generated Prisma client and does not import Prisma at runtime.
|
|
164
55
|
|
|
165
|
-
|
|
56
|
+
### Package entry points
|
|
166
57
|
|
|
167
|
-
|
|
58
|
+
| Import | Purpose |
|
|
59
|
+
| -------------------- | --------------------------------------------------------- |
|
|
60
|
+
| `@gauts/auth` | Password service, session core, errors, and public types. |
|
|
61
|
+
| `@gauts/auth/prisma` | Prisma database adapter. |
|
|
62
|
+
| `@gauts/auth/hono` | Hono cookies, methods, and middleware. |
|
|
63
|
+
| `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
|
|
168
64
|
|
|
169
|
-
##
|
|
65
|
+
## Quick start
|
|
170
66
|
|
|
171
|
-
The Hono adapter
|
|
67
|
+
The Hono adapter does not create routes automatically. The application defines its own login, renewal, logout, and protected endpoints.
|
|
172
68
|
|
|
173
|
-
### Create
|
|
69
|
+
### 1. Create the auth instance
|
|
174
70
|
|
|
175
71
|
```ts
|
|
176
72
|
import { createHonoAuth } from "@gauts/auth/hono";
|
|
177
73
|
import { createPrismaAdapter } from "@gauts/auth/prisma";
|
|
178
74
|
|
|
179
|
-
|
|
180
|
-
secret: process.env.AUTH_SECRET,
|
|
75
|
+
import { prisma } from "./db.js";
|
|
181
76
|
|
|
77
|
+
export const auth = createHonoAuth({
|
|
182
78
|
db: createPrismaAdapter({
|
|
183
79
|
client: prisma,
|
|
184
80
|
config: {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
81
|
+
access: {
|
|
82
|
+
account: {
|
|
83
|
+
allowedStatuses: ["ACTIVE"],
|
|
84
|
+
},
|
|
85
|
+
user: {
|
|
86
|
+
allowedStatuses: ["ACTIVE"],
|
|
87
|
+
},
|
|
190
88
|
},
|
|
191
89
|
},
|
|
192
90
|
}),
|
|
193
|
-
|
|
194
|
-
getIp: (c) => getTrustedClientIp(c),
|
|
195
|
-
|
|
196
|
-
session: {
|
|
197
|
-
validation: ["agent"],
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
cache: {
|
|
201
|
-
ttl: 60,
|
|
202
|
-
},
|
|
203
91
|
});
|
|
204
92
|
```
|
|
205
93
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`createHonoAuth` is the normal Hono entry point: it creates the framework-independent core and attaches the Hono methods in one object. Use `createAuth` plus `createHonoAdapter` only when the same core instance must be composed manually:
|
|
209
|
-
|
|
210
|
-
The resolved cookie names are exposed on the auth instance, including defaults:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
auth.cookie.sessionName; // "__ses"
|
|
214
|
-
auth.cookie.cacheName; // "__cac"
|
|
215
|
-
auth.cookie.renewName; // "__ren"
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
import { createAuth } from "@gauts/auth";
|
|
220
|
-
import { createHonoAdapter } from "@gauts/auth/hono";
|
|
94
|
+
This uses the defaults:
|
|
221
95
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
96
|
+
```text
|
|
97
|
+
password Argon2id
|
|
98
|
+
session TTL 7 days
|
|
99
|
+
renewal every 24 hours
|
|
100
|
+
max lifetime 30 days
|
|
101
|
+
validation User-Agent
|
|
102
|
+
cache disabled
|
|
103
|
+
cookies __ses, __cac, __ren
|
|
229
104
|
```
|
|
230
105
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Only `account_id` is stored in the session row. The Prisma adapter loads the current `account` and `user` relations during authentication; no dynamic account data is copied into the session.
|
|
234
|
-
|
|
235
|
-
### Type the application
|
|
106
|
+
### 2. Add the routes
|
|
236
107
|
|
|
237
108
|
```ts
|
|
238
109
|
import { Hono } from "hono";
|
|
239
110
|
import type { HonoAuthEnv } from "@gauts/auth/hono";
|
|
240
111
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
`auth.requireSession` installs:
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
const session = c.get("session");
|
|
248
|
-
const account = c.get("account");
|
|
249
|
-
const user = c.get("user");
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
The values have these shapes:
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
type AuthAccount = {
|
|
256
|
-
email: string;
|
|
257
|
-
id: string;
|
|
258
|
-
name: string;
|
|
259
|
-
role: string;
|
|
260
|
-
status: string;
|
|
261
|
-
timezone: string | null;
|
|
262
|
-
user: AuthUser;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
type AuthUser = {
|
|
266
|
-
id: string;
|
|
267
|
-
role: string;
|
|
268
|
-
status: string;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
type Session = {
|
|
272
|
-
account_id: string;
|
|
273
|
-
client: {
|
|
274
|
-
agent: string | null;
|
|
275
|
-
ip: string | null;
|
|
276
|
-
platform: string | null;
|
|
277
|
-
};
|
|
278
|
-
created_at: Date;
|
|
279
|
-
expires_at: Date;
|
|
280
|
-
id: string;
|
|
281
|
-
renew_at: Date;
|
|
282
|
-
};
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Login
|
|
286
|
-
|
|
287
|
-
The application owns input validation, credential lookup, rate limiting, and error responses. The adapter access rules validate current account and user status/role before the session is accepted.
|
|
112
|
+
import { auth } from "./auth.js";
|
|
113
|
+
import { DUMMY_PASSWORD_HASH } from "./password.js";
|
|
288
114
|
|
|
289
|
-
|
|
290
|
-
const DUMMY_PASSWORD_HASH =
|
|
291
|
-
"$argon2id$v=19$m=65536,p=4,t=3$PUotpfVXonc0VRFuV1pKZQ$oxxA8DMvGRTSbZvh2Dkokeyih9sbKeodWYROqVxP9BI";
|
|
115
|
+
const app = new Hono<HonoAuthEnv>();
|
|
292
116
|
|
|
293
117
|
app.post("/auth/login", async (c) => {
|
|
294
|
-
const body = await c.req.json<{
|
|
295
|
-
email: string;
|
|
296
|
-
password: string;
|
|
297
|
-
}>();
|
|
118
|
+
const body = await c.req.json<{ email: string; password: string }>();
|
|
298
119
|
const account = await findAccount(body.email);
|
|
299
120
|
const passwordValid = await auth.password.verify({
|
|
300
121
|
password: body.password,
|
|
@@ -312,112 +133,78 @@ app.post("/auth/login", async (c) => {
|
|
|
312
133
|
|
|
313
134
|
return c.json({ authenticated: true });
|
|
314
135
|
});
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
The dummy hash ensures that unknown accounts still perform the configured password verification. Precompute it once with the same algorithm and cost as the application; never generate it inside the request handler. Keep the response identical for unknown accounts and invalid passwords.
|
|
318
136
|
|
|
319
|
-
Only `account_id` is persisted by the session. Email, roles, statuses, password hashes, and other dynamic account data never enter the session table.
|
|
320
|
-
|
|
321
|
-
### Protect routes
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
app.get("/account", auth.requireSession, (c) => {
|
|
325
|
-
return c.json({
|
|
326
|
-
account: c.get("account"),
|
|
327
|
-
session_id: c.get("session").id,
|
|
328
|
-
user: c.get("user"),
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
`requireSession` authenticates the request. With cache configured, only `GET` and `HEAD` may use it. Every other method validates through the database and clears the short cache after successful validation. It does not apply application-specific route roles or permissions.
|
|
334
|
-
|
|
335
|
-
### Renewal endpoint
|
|
336
|
-
|
|
337
|
-
```ts
|
|
338
137
|
app.post("/auth/renew", async (c) => {
|
|
339
138
|
await auth.renewSession(c);
|
|
340
139
|
return c.body(null, 204);
|
|
341
140
|
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
`renewSession` always performs full database validation. It independently derives whether renewal is due from `created_at` and the last database renewal; the renewal marker never authorizes renewal.
|
|
345
|
-
|
|
346
|
-
If renewal is not yet due, database expiry is not changed. The API still returns the authoritative session cookie, renewal marker, and cache.
|
|
347
141
|
|
|
348
|
-
### Logout
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
142
|
app.post("/auth/logout", async (c) => {
|
|
352
143
|
await auth.revokeSession(c);
|
|
353
144
|
return c.body(null, 204);
|
|
354
145
|
});
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
Logout marks the database session as revoked and then clears all three cookies. If database revocation fails, the cookies are not cleared and the error is propagated.
|
|
358
|
-
|
|
359
|
-
## Next.js renewal adapter
|
|
360
|
-
|
|
361
|
-
The Next.js adapter does not authenticate sessions. It calls the private API renewal URL when `renewAt` is missing, invalid, or due.
|
|
362
|
-
|
|
363
|
-
```ts
|
|
364
|
-
import { createNextAuth } from "@gauts/auth/next";
|
|
365
146
|
|
|
366
|
-
|
|
367
|
-
|
|
147
|
+
app.get("/account", auth.requireSession, (c) => {
|
|
148
|
+
return c.json({
|
|
149
|
+
account: c.get("account"),
|
|
150
|
+
session: c.get("session"),
|
|
151
|
+
user: c.get("user"),
|
|
152
|
+
});
|
|
368
153
|
});
|
|
369
154
|
```
|
|
370
155
|
|
|
371
|
-
|
|
156
|
+
Precompute `DUMMY_PASSWORD_HASH` once with the same algorithm and cost as the application. This ensures unknown accounts perform equivalent password verification work. Keep the response identical for unknown accounts and incorrect passwords.
|
|
157
|
+
|
|
158
|
+
### 3. Enable the optional cache
|
|
372
159
|
|
|
373
160
|
```ts
|
|
374
|
-
|
|
161
|
+
export const auth = createHonoAuth({
|
|
162
|
+
cache: {
|
|
163
|
+
ttl: 60,
|
|
164
|
+
},
|
|
165
|
+
db,
|
|
166
|
+
secret: requiredEnv("AUTH_SECRET"),
|
|
167
|
+
});
|
|
375
168
|
```
|
|
376
169
|
|
|
377
|
-
|
|
170
|
+
`AUTH_SECRET` must contain at least 32 high-entropy bytes. It stays in the API and is never shared with Next.js.
|
|
378
171
|
|
|
379
|
-
|
|
380
|
-
import type { NextRequest } from "next/server";
|
|
381
|
-
import { NextResponse } from "next/server";
|
|
172
|
+
`requiredEnv()` represents an application helper that returns a non-empty environment string or fails during startup.
|
|
382
173
|
|
|
383
|
-
|
|
384
|
-
const response = NextResponse.next();
|
|
385
|
-
const renewal = await nextAuth.renew({
|
|
386
|
-
request,
|
|
387
|
-
response,
|
|
388
|
-
});
|
|
174
|
+
## Configuration reference
|
|
389
175
|
|
|
390
|
-
|
|
391
|
-
return NextResponse.redirect(new URL("/login", request.url));
|
|
392
|
-
}
|
|
176
|
+
### `createHonoAuth()`
|
|
393
177
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
178
|
+
| Property | Type / allowed values | Required | Default | Description |
|
|
179
|
+
| ---------- | --------------------- | :---------------------: | ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
180
|
+
| `db` | `DbAdapter` | ✅ | — | Authoritative session persistence and account loading. |
|
|
181
|
+
| `getIp` | `HonoGetIp` | Only with IP validation | Omitted | Returns the client IP from a source trusted by the application. May be synchronous or asynchronous. |
|
|
182
|
+
| `password` | `PasswordConfig` | ❌ | Argon2id defaults | Password hashing and verification configuration. |
|
|
183
|
+
| `session` | `SessionConfig` | ❌ | Session defaults | Expiry, renewal, and client validation configuration. |
|
|
184
|
+
| `cookie` | `HonoCookieConfig` | ❌ | Cookie defaults | Names, domain, path, SameSite, and Secure settings. |
|
|
185
|
+
| `cache` | `{ ttl: number }` | ❌ | Disabled | Enables the short signed browser cache. |
|
|
186
|
+
| `secret` | `string` | When `cache` is enabled | — | HMAC secret for the signed cache. Minimum 32 UTF-8 bytes. |
|
|
397
187
|
|
|
398
|
-
|
|
399
|
-
|
|
188
|
+
```ts
|
|
189
|
+
type HonoGetIp = (c: Context) => Promise<string | null | undefined> | string | null | undefined;
|
|
400
190
|
```
|
|
401
191
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
| `attempted` | `status` | Meaning |
|
|
405
|
-
| --- | ---: | --- |
|
|
406
|
-
| `false` | `null` | Session token exists and `renewAt` is a valid future timestamp. |
|
|
407
|
-
| `false` | `401` | Session token is missing or malformed. No API call occurred. |
|
|
408
|
-
| `true` | HTTP status | `/auth/renew` was called and its `Set-Cookie` headers were copied. |
|
|
409
|
-
|
|
410
|
-
The adapter forwards only the session cookie and the client/origin headers required for session, host, and CSRF validation. If `Origin` is missing and both `X-Forwarded-Proto` and `X-Forwarded-Host` were received, it reconstructs the public origin from those trusted proxy headers. It never derives public headers from the internal `NextRequest` URL. Other cookies, `Authorization`, and arbitrary request headers are never forwarded. Renewal rejects redirects and times out after five seconds. `renewUrl` must point to the application's trusted private API, and the deployment proxy must overwrite forwarded headers from untrusted clients.
|
|
192
|
+
When `getIp` is omitted, the adapter stores `ip: null` and does not read IP headers automatically. Configuring `session.validation` with `"ip"` requires `getIp` and fails during initialization when it is missing.
|
|
411
193
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
Apply the proxy only to protected application routes, or skip renewal handling for public routes before calling `nextAuth.renew`. Otherwise a missing cookie produces `status: 401`, which is correct for protected routes but not for login or public pages.
|
|
194
|
+
### Password
|
|
415
195
|
|
|
416
|
-
|
|
196
|
+
#### Argon2id
|
|
417
197
|
|
|
418
|
-
|
|
198
|
+
Argon2id is selected when `password.algorithm` is omitted or set to `"argon2id"`.
|
|
419
199
|
|
|
420
|
-
|
|
200
|
+
| Property | Type / allowed values | Default | Description |
|
|
201
|
+
| ------------- | --------------------------- | -----------: | ----------------------------------------------------------------- |
|
|
202
|
+
| `algorithm` | `"argon2id"` | `"argon2id"` | Password algorithm used for both hashing and verification. |
|
|
203
|
+
| `hashLength` | Integer `16`–`64` | `32` | Output hash length in bytes. |
|
|
204
|
+
| `maxBytes` | Integer `1`–`1,048,576` | `1024` | Maximum UTF-8 password size accepted by hashing and verification. |
|
|
205
|
+
| `memoryCost` | Integer `8,192`–`1,048,576` | `65,536` | Argon2 memory cost in KiB. |
|
|
206
|
+
| `parallelism` | Integer `1`–`16` | `4` | Number of parallel lanes. |
|
|
207
|
+
| `timeCost` | Integer `1`–`10` | `3` | Number of Argon2 iterations. |
|
|
421
208
|
|
|
422
209
|
```ts
|
|
423
210
|
password: {
|
|
@@ -425,15 +212,16 @@ password: {
|
|
|
425
212
|
}
|
|
426
213
|
```
|
|
427
214
|
|
|
428
|
-
|
|
429
|
-
| --- | ---: | ---: |
|
|
430
|
-
| `hashLength` | `32` | `16` to `64` |
|
|
431
|
-
| `maxBytes` | `1024` | `1` to `1,048,576` |
|
|
432
|
-
| `memoryCost` | `65,536` | `8,192` to `1,048,576` |
|
|
433
|
-
| `parallelism` | `4` | `1` to `16` |
|
|
434
|
-
| `timeCost` | `3` | `1` to `10` |
|
|
215
|
+
#### bcrypt
|
|
435
216
|
|
|
436
|
-
Applications with existing bcrypt hashes must select bcrypt explicitly
|
|
217
|
+
Applications with existing bcrypt hashes must select bcrypt explicitly.
|
|
218
|
+
|
|
219
|
+
| Property | Type / allowed values | Default | Description |
|
|
220
|
+
| ---------------- | -------------------------------------- | -------: | ------------------------------------------------------- |
|
|
221
|
+
| `algorithm` | `"bcrypt"` | Required | Selects bcrypt for both hashing and verification. |
|
|
222
|
+
| `maxBytes` | Integer `1`–`72` | `72` | Maximum UTF-8 size accepted for new passwords. |
|
|
223
|
+
| `rounds` | Integer `4`–`31` | `12` | bcrypt cost factor. |
|
|
224
|
+
| `verifyMaxBytes` | Integer from `maxBytes` to `1,048,576` | `72` | Maximum input accepted while verifying existing hashes. |
|
|
437
225
|
|
|
438
226
|
```ts
|
|
439
227
|
password: {
|
|
@@ -441,95 +229,101 @@ password: {
|
|
|
441
229
|
}
|
|
442
230
|
```
|
|
443
231
|
|
|
444
|
-
|
|
445
|
-
| --- | ---: | ---: |
|
|
446
|
-
| `maxBytes` | `72` | `1` to `72` |
|
|
447
|
-
| `rounds` | `12` | `4` to `31` |
|
|
448
|
-
| `verifyMaxBytes` | `72` | `maxBytes` to `1,048,576` |
|
|
449
|
-
|
|
450
|
-
The selected algorithm is used for both hashing and verification. The package never detects algorithms, migrates hashes, rehashes passwords, or falls back to another algorithm.
|
|
232
|
+
The package does not detect algorithms, migrate hashes, rehash passwords, or fall back to another algorithm.
|
|
451
233
|
|
|
452
234
|
### Session
|
|
453
235
|
|
|
236
|
+
All time values are seconds.
|
|
237
|
+
|
|
238
|
+
| Property | Type / allowed values | Default | Description |
|
|
239
|
+
| --------------- | ----------------------------------------------- | --------------------: | ------------------------------------------------------------------------- |
|
|
240
|
+
| `maxLifetime` | Integer from `ttl` to `31,536,000` | `2,592,000` (30 days) | Maximum session lifetime from the original login, regardless of activity. |
|
|
241
|
+
| `renewInterval` | Integer `1` to `ttl - 1` | `86,400` (24 hours) | Minimum interval before sliding renewal is due. |
|
|
242
|
+
| `ttl` | Integer `60`–`31,536,000` | `604,800` (7 days) | Inactivity lifetime assigned at login and renewal. |
|
|
243
|
+
| `validation` | Unique array of `"agent"`, `"ip"`, `"platform"` | `["agent"]` | Client fields that must match the stored session exactly. |
|
|
244
|
+
|
|
454
245
|
```ts
|
|
455
246
|
session: {
|
|
456
|
-
maxLifetime:
|
|
457
|
-
renewInterval:
|
|
458
|
-
ttl:
|
|
459
|
-
validation: ["agent"],
|
|
247
|
+
maxLifetime: 60 * 60 * 24 * 30,
|
|
248
|
+
renewInterval: 60 * 60 * 24,
|
|
249
|
+
ttl: 60 * 60 * 24 * 7,
|
|
250
|
+
validation: ["agent", "ip"],
|
|
460
251
|
}
|
|
461
252
|
```
|
|
462
253
|
|
|
463
|
-
|
|
254
|
+
Validation values:
|
|
464
255
|
|
|
465
|
-
|
|
|
466
|
-
|
|
|
467
|
-
| `
|
|
468
|
-
| `
|
|
469
|
-
| `
|
|
470
|
-
| `validation` | `["agent"]` | Unique `agent`, `ip`, `platform` fields | Exact client fields compared on every validation. |
|
|
256
|
+
| Enum | Source | Behavior |
|
|
257
|
+
| ------------ | ------------------------------------- | -------------------------------------------------------- |
|
|
258
|
+
| `"agent"` | Complete `User-Agent` header | Enabled by default. The complete value must match. |
|
|
259
|
+
| `"ip"` | Application-provided `getIp()` result | IPv4 and IPv6 are canonicalized before exact comparison. |
|
|
260
|
+
| `"platform"` | `Sec-CH-UA-Platform` header | Normalized and compared exactly. |
|
|
471
261
|
|
|
472
|
-
|
|
262
|
+
Every selected field is required during session creation and validation. A mismatch revokes the database session.
|
|
263
|
+
|
|
264
|
+
Expiry is derived as follows:
|
|
473
265
|
|
|
474
266
|
```text
|
|
475
267
|
maxExpiresAt = created_at + maxLifetime
|
|
476
268
|
expires_at = min(now + ttl, maxExpiresAt)
|
|
477
|
-
renew_at
|
|
269
|
+
renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
|
|
478
270
|
```
|
|
479
271
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
### Cookie
|
|
272
|
+
### Cookies
|
|
483
273
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
```
|
|
274
|
+
| Property | Type / allowed values | Default | Description |
|
|
275
|
+
| ------------- | ----------------------------- | ----------------- | ------------------------------------------------------------------------- |
|
|
276
|
+
| `sessionName` | Valid cookie name | `"__ses"` | Contains the opaque token. This is the only authenticating cookie. |
|
|
277
|
+
| `cacheName` | Valid cookie name | `"__cac"` | Contains the optional signed short cache. |
|
|
278
|
+
| `renewName` | Valid cookie name | `"__ren"` | Contains the untrusted `renew_at` Unix timestamp. |
|
|
279
|
+
| `domain` | `string` | Browser host only | Optional cookie domain. |
|
|
280
|
+
| `path` | String beginning with `/` | `"/"` | Cookie path. |
|
|
281
|
+
| `sameSite` | `"Strict" \| "Lax" \| "None"` | `"Lax"` | Browser SameSite policy. |
|
|
282
|
+
| `secure` | `boolean` | `true` | Requires HTTPS when enabled. Set `false` only for local HTTP development. |
|
|
495
283
|
|
|
496
|
-
`HttpOnly`
|
|
284
|
+
All three cookies are always `HttpOnly` and expire with their respective server-side purpose. Their names must be unique.
|
|
497
285
|
|
|
498
|
-
|
|
499
|
-
- The cache cookie contains the signed snapshot and expires after `cache.ttl`.
|
|
500
|
-
- The renewal cookie contains the authoritative `renew_at` as Unix seconds and expires with the session cookie.
|
|
501
|
-
- The renewal timestamp is an untrusted scheduling hint. It never authenticates or extends a session.
|
|
502
|
-
- Cookie names default to `__ses`, `__cac`, and `__ren`.
|
|
503
|
-
- All three names must be valid and unique.
|
|
286
|
+
Cookie prefix rules are enforced:
|
|
504
287
|
|
|
505
|
-
- `__Host-` requires `secure: true`, `path: "/"`, and no domain
|
|
288
|
+
- `__Host-` requires `secure: true`, `path: "/"`, and no `domain`.
|
|
506
289
|
- `__Secure-` requires `secure: true`.
|
|
507
290
|
- `SameSite=None` requires `secure: true`.
|
|
508
|
-
- Local HTTP development requires `secure: false`.
|
|
509
291
|
|
|
510
|
-
|
|
292
|
+
SameSite values:
|
|
511
293
|
|
|
512
|
-
|
|
294
|
+
| Enum | Behavior |
|
|
295
|
+
| ---------- | ---------------------------------------------------------------------------- |
|
|
296
|
+
| `"Strict"` | Sends cookies only in same-site contexts. |
|
|
297
|
+
| `"Lax"` | Sends cookies in same-site contexts and eligible top-level safe navigations. |
|
|
298
|
+
| `"None"` | Allows cross-site cookie use and requires `secure: true`. |
|
|
299
|
+
|
|
300
|
+
The resolved names are available through:
|
|
513
301
|
|
|
514
302
|
```ts
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
303
|
+
auth.cookie.sessionName; // "__ses"
|
|
304
|
+
auth.cookie.cacheName; // "__cac"
|
|
305
|
+
auth.cookie.renewName; // "__ren"
|
|
519
306
|
```
|
|
520
307
|
|
|
521
|
-
|
|
308
|
+
### Signed cache
|
|
309
|
+
|
|
310
|
+
| Property | Type / allowed values | Default | Description |
|
|
311
|
+
| ----------- | ----------------------------------- | -------- | --------------------------------------------------------- |
|
|
312
|
+
| `cache.ttl` | Integer `1` to `session.ttl` | Disabled | Maximum cache lifetime in seconds. |
|
|
313
|
+
| `secret` | String with at least 32 UTF-8 bytes | — | Signs the cache with HMAC-SHA-256. Required with `cache`. |
|
|
314
|
+
|
|
315
|
+
The cache:
|
|
522
316
|
|
|
523
|
-
|
|
317
|
+
- is cryptographically bound to the opaque token and client identity;
|
|
318
|
+
- is accepted only for `GET` and `HEAD`;
|
|
319
|
+
- never extends the authoritative database session;
|
|
320
|
+
- is bypassed for unsafe methods, renewal, logout, WebSockets, and core calls;
|
|
321
|
+
- falls back to normal database authentication when absent, expired, malformed, or altered.
|
|
524
322
|
|
|
525
|
-
|
|
526
|
-
- is cryptographically bound to the opaque session token;
|
|
527
|
-
- contains the resolved session, account, user, and its own expiry;
|
|
528
|
-
- compares the same configured IP, User-Agent, and platform fields on every hit;
|
|
529
|
-
- never extends the authoritative session expiry;
|
|
530
|
-
- is accepted only for `GET` and `HEAD` requests.
|
|
323
|
+
The cache is signed but not encrypted. Do not place passwords, password hashes, raw session tokens, or application secrets in account/session data.
|
|
531
324
|
|
|
532
|
-
|
|
325
|
+
<details>
|
|
326
|
+
<summary>Internal compact cache payload</summary>
|
|
533
327
|
|
|
534
328
|
```ts
|
|
535
329
|
{
|
|
@@ -553,113 +347,300 @@ The signed value uses a compact internal payload:
|
|
|
553
347
|
}
|
|
554
348
|
```
|
|
555
349
|
|
|
556
|
-
`account_id` is
|
|
350
|
+
`session.account_id` is reconstructed from `acc.id` after signature validation.
|
|
557
351
|
|
|
558
|
-
|
|
352
|
+
</details>
|
|
559
353
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
### Trusted client IP
|
|
354
|
+
### Prisma adapter
|
|
563
355
|
|
|
564
356
|
```ts
|
|
565
|
-
|
|
357
|
+
const db = createPrismaAdapter({
|
|
358
|
+
client: prisma,
|
|
359
|
+
config: {
|
|
360
|
+
session: {
|
|
361
|
+
table: "account_sessions",
|
|
362
|
+
relations: {
|
|
363
|
+
account: "account",
|
|
364
|
+
user: "user",
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
access: {
|
|
368
|
+
account: {
|
|
369
|
+
allowedRoles: ["OWNER", "ADMIN"],
|
|
370
|
+
allowedStatuses: ["ACTIVE"],
|
|
371
|
+
},
|
|
372
|
+
user: {
|
|
373
|
+
allowedRoles: ["ADMIN"],
|
|
374
|
+
allowedStatuses: ["ACTIVE"],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
});
|
|
566
379
|
```
|
|
567
380
|
|
|
568
|
-
|
|
381
|
+
| Property | Type / allowed values | Required | Default | Description |
|
|
382
|
+
| --------------------------------------- | ------------------------------- | :-----------------------------: | -------------------- | --------------------------------------------------------------------- |
|
|
383
|
+
| `client` | Generated Prisma client | ✅ | — | Prisma client containing the session delegate. |
|
|
384
|
+
| `config.session` | Session model configuration | ❌ | Conventional names | Groups the Prisma delegate and relation names. |
|
|
385
|
+
| `config.session.table` | Compatible Prisma delegate name | Only without `account_sessions` | `"account_sessions"` | Delegate used to store sessions. This is not the physical table name. |
|
|
386
|
+
| `config.session.relations.account` | Non-empty `string` | ❌ | `"account"` | Account relation field on the session model. |
|
|
387
|
+
| `config.session.relations.user` | Non-empty `string` | ❌ | `"user"` | User relation field nested inside the account relation. |
|
|
388
|
+
| `config.access.account.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | Account statuses allowed to authenticate. |
|
|
389
|
+
| `config.access.account.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | Account roles allowed to authenticate. |
|
|
390
|
+
| `config.access.user.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | User statuses allowed to authenticate. |
|
|
391
|
+
| `config.access.user.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | User roles allowed to authenticate. |
|
|
392
|
+
|
|
393
|
+
Status and role values are deliberately dynamic. The package does not define application-specific enums.
|
|
394
|
+
Every configured account and user condition must match. Omitting `allowedRoles` accepts every role, but both `allowedStatuses` lists remain required.
|
|
569
395
|
|
|
570
|
-
|
|
396
|
+
### Next.js adapter
|
|
571
397
|
|
|
572
398
|
```ts
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
};
|
|
399
|
+
import { createNextAuth } from "@gauts/auth/next";
|
|
400
|
+
|
|
401
|
+
export const nextAuth = createNextAuth({
|
|
402
|
+
renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
|
|
403
|
+
});
|
|
578
404
|
```
|
|
579
405
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
- Every field selected in `session.validation` is required during creation and validation. Missing or invalid configured fields never match.
|
|
406
|
+
| Property | Type / allowed values | Required | Default | Description |
|
|
407
|
+
| -------------------- | -------------------------------- | :------: | --------- | --------------------------------------------- |
|
|
408
|
+
| `renewUrl` | Absolute `http:` or `https:` URL | ✅ | — | Trusted private API renewal endpoint. |
|
|
409
|
+
| `cookie.sessionName` | Valid cookie name | ❌ | `"__ses"` | Session cookie read and forwarded to the API. |
|
|
410
|
+
| `cookie.renewName` | Valid cookie name | ❌ | `"__ren"` | Renewal scheduling cookie read by Next.js. |
|
|
586
411
|
|
|
587
|
-
##
|
|
412
|
+
## Session flow
|
|
588
413
|
|
|
589
|
-
|
|
414
|
+
### Login
|
|
590
415
|
|
|
591
|
-
|
|
416
|
+
```text
|
|
417
|
+
credentials accepted
|
|
418
|
+
-> generate 256-bit opaque token
|
|
419
|
+
-> store SHA-256 token hash in DB
|
|
420
|
+
-> load current account and user
|
|
421
|
+
-> apply configured access rules
|
|
422
|
+
-> write __ses
|
|
423
|
+
-> write __ren
|
|
424
|
+
-> optionally write __cac
|
|
425
|
+
```
|
|
592
426
|
|
|
593
|
-
|
|
594
|
-
import { createPrismaAdapter } from "@gauts/auth/prisma";
|
|
427
|
+
Only the raw browser token authenticates. The database stores only its SHA-256 hash.
|
|
595
428
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
429
|
+
### Protected `GET` or `HEAD`
|
|
430
|
+
|
|
431
|
+
```text
|
|
432
|
+
session token
|
|
433
|
+
-> valid signed cache?
|
|
434
|
+
-> yes: expose cached account, user, and session
|
|
435
|
+
-> no: validate through DB and create a fresh cache
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Unsafe request
|
|
439
|
+
|
|
440
|
+
```text
|
|
441
|
+
session token
|
|
442
|
+
-> SHA-256 hash
|
|
443
|
+
-> indexed DB lookup
|
|
444
|
+
-> validate expiry, revocation, account, user, and client
|
|
445
|
+
-> clear short cache
|
|
446
|
+
-> continue
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Renewal
|
|
450
|
+
|
|
451
|
+
```text
|
|
452
|
+
Next reads __ren
|
|
453
|
+
-> future timestamp: no API request
|
|
454
|
+
-> missing, invalid, or due: POST /auth/renew
|
|
455
|
+
-> API validates through DB
|
|
456
|
+
-> update expires_at when renewal is due
|
|
457
|
+
-> Set-Cookie with the same token, new renewAt, and fresh cache
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
`auth.session.resolve()` is always DB-backed and read-only. Only explicit renewal updates database expiry.
|
|
461
|
+
|
|
462
|
+
## Prisma schema
|
|
463
|
+
|
|
464
|
+
The Prisma adapter resolves:
|
|
465
|
+
|
|
466
|
+
```text
|
|
467
|
+
account_sessions -> account -> user
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The following is a complete MySQL/MariaDB example. Merge the required fields and relations into existing account and user models when applicable.
|
|
471
|
+
|
|
472
|
+
```prisma
|
|
473
|
+
model users {
|
|
474
|
+
id String @id @default(uuid()) @db.VarChar(255)
|
|
475
|
+
role String @db.VarChar(255)
|
|
476
|
+
status String @db.VarChar(255)
|
|
477
|
+
|
|
478
|
+
accounts user_accounts[]
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
model user_accounts {
|
|
482
|
+
id String @id @default(uuid()) @db.VarChar(255)
|
|
483
|
+
user_id String @db.VarChar(255)
|
|
484
|
+
email String @db.VarChar(255)
|
|
485
|
+
name String @db.VarChar(255)
|
|
486
|
+
role String @db.VarChar(255)
|
|
487
|
+
status String @db.VarChar(255)
|
|
488
|
+
timezone String? @db.VarChar(255)
|
|
489
|
+
|
|
490
|
+
user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
491
|
+
sessions account_sessions[]
|
|
492
|
+
|
|
493
|
+
@@index([user_id])
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
model account_sessions {
|
|
497
|
+
id String @id @default(uuid()) @db.VarChar(255)
|
|
498
|
+
account_id String @db.VarChar(255)
|
|
499
|
+
token_hash String @unique @db.VarChar(64)
|
|
500
|
+
ip String? @db.VarChar(45)
|
|
501
|
+
platform String? @db.VarChar(255)
|
|
502
|
+
agent String? @db.Text
|
|
503
|
+
expires_at DateTime @db.Timestamp(0)
|
|
504
|
+
revoked_at DateTime? @db.Timestamp(0)
|
|
505
|
+
created_at DateTime @default(now()) @db.Timestamp(0)
|
|
506
|
+
updated_at DateTime? @db.Timestamp(0)
|
|
507
|
+
|
|
508
|
+
account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
|
|
509
|
+
|
|
510
|
+
@@index([account_id])
|
|
511
|
+
@@index([expires_at])
|
|
512
|
+
@@index([revoked_at])
|
|
513
|
+
}
|
|
607
514
|
```
|
|
608
515
|
|
|
609
|
-
|
|
516
|
+
Required fields and relation names:
|
|
517
|
+
|
|
518
|
+
| Path | Required fields |
|
|
519
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
520
|
+
| Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
|
|
521
|
+
| `account` relation | `id`, `email`, `name`, `role`, `status`, `timezone` |
|
|
522
|
+
| `account.user` relation | `id`, `role`, `status` |
|
|
523
|
+
|
|
524
|
+
The relation names default to `account` and `user`. Configure `session.relations` when the application uses different field names. Their inverse relation names may differ. Application models may add fields, indexes, defaults, and relations. Role and status fields may use Prisma enums.
|
|
525
|
+
|
|
526
|
+
Keep `agent` large enough for the complete User-Agent. Use provider-compatible native annotations when the database is not MySQL/MariaDB.
|
|
527
|
+
|
|
528
|
+
`config.session.table` is a Prisma client delegate name. A model named `AdminSession` mapped with `@@map("account_sessions")` normally uses the `adminSession` delegate.
|
|
529
|
+
|
|
530
|
+
Create and run migrations through the application's Prisma workflow. The package never manages migrations.
|
|
531
|
+
|
|
532
|
+
## Hono adapter
|
|
533
|
+
|
|
534
|
+
### Request values
|
|
535
|
+
|
|
536
|
+
`auth.requireSession` sets fully typed values on the Hono context:
|
|
610
537
|
|
|
611
538
|
```ts
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
539
|
+
const account = c.get("account");
|
|
540
|
+
const session = c.get("session");
|
|
541
|
+
const user = c.get("user");
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
type AuthAccount = {
|
|
546
|
+
email: string;
|
|
547
|
+
id: string;
|
|
548
|
+
name: string;
|
|
549
|
+
role: string;
|
|
550
|
+
status: string;
|
|
551
|
+
timezone: string | null;
|
|
552
|
+
user: AuthUser;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
type AuthUser = {
|
|
556
|
+
id: string;
|
|
557
|
+
role: string;
|
|
558
|
+
status: string;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
type Session = {
|
|
562
|
+
account_id: string;
|
|
563
|
+
client: {
|
|
564
|
+
agent: string | null;
|
|
565
|
+
ip: string | null;
|
|
566
|
+
platform: string | null;
|
|
567
|
+
};
|
|
568
|
+
created_at: Date;
|
|
569
|
+
expires_at: Date;
|
|
570
|
+
id: string;
|
|
571
|
+
renew_at: Date;
|
|
572
|
+
};
|
|
624
573
|
```
|
|
625
574
|
|
|
626
|
-
|
|
575
|
+
Only `account_id` is persisted in the session row. Current account and user data are loaded through the database relation and never copied into the table.
|
|
576
|
+
|
|
577
|
+
### Methods
|
|
578
|
+
|
|
579
|
+
| Method | Purpose |
|
|
580
|
+
| --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
581
|
+
| `auth.createSession({ account_id, context })` | Creates the DB session and writes the browser cookies. |
|
|
582
|
+
| `auth.resolveSession(context)` | Resolves a request and returns account, user, and session. |
|
|
583
|
+
| `auth.renewSession(context)` | Performs DB validation, renews when due, and writes authoritative cookies. |
|
|
584
|
+
| `auth.revokeSession(context)` | Revokes the current DB session and clears cookies. |
|
|
585
|
+
| `auth.clearSession(context)` | Clears browser cookies without revoking the DB session. |
|
|
586
|
+
| `auth.getToken(context)` | Returns the validated opaque token from the request cookie. |
|
|
587
|
+
| `auth.requireSession` | Hono middleware that authenticates and populates the context. |
|
|
588
|
+
|
|
589
|
+
`requireSession` authenticates only. Application-specific route permissions remain the application's responsibility.
|
|
590
|
+
|
|
591
|
+
### Core and adapter composition
|
|
592
|
+
|
|
593
|
+
`createHonoAuth()` is the normal entry point. Use separate composition only when the same core instance is required outside Hono:
|
|
627
594
|
|
|
628
595
|
```ts
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
user: {
|
|
636
|
-
role: ["ADMIN"],
|
|
637
|
-
status: ["ACTIVE", "PENDING"],
|
|
638
|
-
},
|
|
639
|
-
},
|
|
596
|
+
import { createAuth } from "@gauts/auth";
|
|
597
|
+
import { createHonoAdapter } from "@gauts/auth/hono";
|
|
598
|
+
|
|
599
|
+
const core = createAuth({ db });
|
|
600
|
+
const hono = createHonoAdapter({
|
|
601
|
+
auth: core,
|
|
640
602
|
});
|
|
641
603
|
```
|
|
642
604
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
The configured arrays are access allowlists, not declarations of every enum value that exists in the application.
|
|
605
|
+
## Next.js adapter
|
|
646
606
|
|
|
647
|
-
|
|
607
|
+
The Next.js adapter schedules renewal; it does not authenticate pages or API requests.
|
|
648
608
|
|
|
649
609
|
```ts
|
|
650
|
-
import type {
|
|
610
|
+
import type { NextRequest } from "next/server";
|
|
611
|
+
import { NextResponse } from "next/server";
|
|
651
612
|
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
613
|
+
export const proxy = async (request: NextRequest) => {
|
|
614
|
+
const response = NextResponse.next();
|
|
615
|
+
const renewal = await nextAuth.renew({ request, response });
|
|
616
|
+
|
|
617
|
+
if (renewal.status === 401) {
|
|
618
|
+
return NextResponse.redirect(new URL("/auth/login", request.url));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (renewal.status !== null && renewal.status >= 500) {
|
|
622
|
+
return NextResponse.redirect(new URL("/maintenance", request.url));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return renewal.response;
|
|
626
|
+
};
|
|
660
627
|
```
|
|
661
628
|
|
|
662
|
-
|
|
629
|
+
Result values:
|
|
630
|
+
|
|
631
|
+
| `attempted` | `status` | Meaning |
|
|
632
|
+
| :---------: | ----------: | --------------------------------------------------------------- |
|
|
633
|
+
| `false` | `null` | Session token exists and renewal is not due. |
|
|
634
|
+
| `false` | `401` | Session token is missing or malformed; no API request occurred. |
|
|
635
|
+
| `true` | HTTP status | The renewal endpoint was called and returned this status. |
|
|
636
|
+
|
|
637
|
+
The adapter copies every returned `Set-Cookie` header to the browser response. It forwards only the session cookie and controlled client/origin headers required by the private API. Other cookies, authorization headers, and arbitrary headers are not forwarded.
|
|
638
|
+
|
|
639
|
+
`FORWARD_HEADERS` is exported from `@gauts/auth/next` for application fetchers that need the same controlled header list.
|
|
640
|
+
|
|
641
|
+
When `Origin` is absent and trusted `X-Forwarded-Proto` and `X-Forwarded-Host` headers exist, the adapter reconstructs the public origin from them. It never derives a public origin from the internal Next.js request URL. The deployment proxy must overwrite forwarded headers received from untrusted clients.
|
|
642
|
+
|
|
643
|
+
Apply renewal only to protected routes or skip public routes before calling `nextAuth.renew()`.
|
|
663
644
|
|
|
664
645
|
## Core session API
|
|
665
646
|
|
|
@@ -673,27 +654,46 @@ await auth.session.revokeToken(token);
|
|
|
673
654
|
await auth.session.revokeAccount(account_id);
|
|
674
655
|
```
|
|
675
656
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
657
|
+
| Method | Behavior |
|
|
658
|
+
| --------------- | ------------------------------------------------------ |
|
|
659
|
+
| `create` | Creates a session and returns the raw token once. |
|
|
660
|
+
| `resolve` | Performs read-only DB authentication. |
|
|
661
|
+
| `renew` | Validates through DB and updates expiry only when due. |
|
|
662
|
+
| `list` | Returns active sessions without token hashes. |
|
|
663
|
+
| `revoke` | Revokes one session belonging to an account. |
|
|
664
|
+
| `revokeToken` | Revokes the session matching a raw token. |
|
|
665
|
+
| `revokeAccount` | Revokes every active session for an account. |
|
|
681
666
|
|
|
682
|
-
|
|
667
|
+
The package does not limit session count or delete historical rows. Retention and cleanup belong to the application.
|
|
683
668
|
|
|
684
|
-
|
|
669
|
+
## Custom database adapter
|
|
685
670
|
|
|
686
|
-
|
|
671
|
+
The core depends on `DbAdapter`, not Prisma:
|
|
687
672
|
|
|
688
|
-
```
|
|
689
|
-
|
|
673
|
+
```ts
|
|
674
|
+
import type { DbAdapter } from "@gauts/auth";
|
|
675
|
+
|
|
676
|
+
const db = {
|
|
677
|
+
create: async (session) => {},
|
|
678
|
+
find: async ({ account_id, session_id }) => null,
|
|
679
|
+
findActive: async ({ account_id, now }) => [],
|
|
680
|
+
findToken: async (token_hash) => null,
|
|
681
|
+
revoke: async ({ revoked_at, session_ids }) => {},
|
|
682
|
+
updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
|
|
683
|
+
} satisfies DbAdapter;
|
|
690
684
|
```
|
|
691
685
|
|
|
692
|
-
|
|
686
|
+
`findToken` receives only the SHA-256 token hash. It must return the current nested account and user plus an `allowed` result. Raw tokens must never be persisted.
|
|
687
|
+
|
|
688
|
+
## Performance
|
|
693
689
|
|
|
694
|
-
|
|
690
|
+
The package contains no Redis or in-process cache.
|
|
695
691
|
|
|
696
|
-
|
|
692
|
+
Without the optional browser cache, each `requireSession` performs an indexed database lookup through `account_sessions.token_hash`.
|
|
693
|
+
|
|
694
|
+
With a valid cache, `GET` and `HEAD` skip the lookup until `cache.ttl` expires. Unsafe methods always use current database state.
|
|
695
|
+
|
|
696
|
+
The tradeoff is explicit: revocation and account/user changes made elsewhere may remain visible to safe cached requests until the short TTL expires. A 60-second TTL limits this stale-read window to one minute. Disable cache when immediate read revocation is required.
|
|
697
697
|
|
|
698
698
|
## Errors
|
|
699
699
|
|
|
@@ -710,31 +710,29 @@ type AuthErrorCode =
|
|
|
710
710
|
|
|
711
711
|
Use `isAuthError(error)` before reading `error.code`.
|
|
712
712
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
|
716
|
-
|
|
|
717
|
-
| `
|
|
718
|
-
| `
|
|
719
|
-
| `
|
|
720
|
-
| `
|
|
721
|
-
| `
|
|
722
|
-
| `SESSION_NOT_FOUND` | `404` |
|
|
723
|
-
| `DB_UNAVAILABLE` | `503` |
|
|
713
|
+
| Code | Suggested HTTP status | Meaning |
|
|
714
|
+
| ------------------------- | --------------------: | ----------------------------------------------------------------- |
|
|
715
|
+
| `AUTH_CONFIG_INVALID` | `500` | Invalid startup configuration. |
|
|
716
|
+
| `PASSWORD_INPUT_INVALID` | `400` | Password input violates configured limits. |
|
|
717
|
+
| `SESSION_CLIENT_MISMATCH` | `403` | A configured client field does not match; the session is revoked. |
|
|
718
|
+
| `SESSION_DATA_INVALID` | `400` | Invalid session or renewal data. |
|
|
719
|
+
| `SESSION_INVALID` | `401` | Missing, expired, revoked, or unknown session. |
|
|
720
|
+
| `SESSION_NOT_FOUND` | `404` | Requested session does not exist for the account. |
|
|
721
|
+
| `DB_UNAVAILABLE` | `503` | Database operation failed. Authentication fails closed. |
|
|
724
722
|
|
|
725
|
-
|
|
723
|
+
The package throws typed errors but does not choose application HTTP responses.
|
|
726
724
|
|
|
727
725
|
## Security responsibilities
|
|
728
726
|
|
|
729
|
-
The package provides
|
|
727
|
+
The package provides authentication primitives, not a complete application security policy. Applications remain responsible for:
|
|
730
728
|
|
|
731
729
|
- TLS and trusted-proxy configuration;
|
|
732
|
-
- CSRF, CORS, and origin validation;
|
|
730
|
+
- CSRF, CORS, host, and origin validation;
|
|
733
731
|
- login and renewal rate limiting;
|
|
734
732
|
- equivalent password verification work for unknown accounts;
|
|
735
|
-
-
|
|
736
|
-
- re-authentication for sensitive
|
|
737
|
-
- database migrations and cleanup;
|
|
733
|
+
- route roles and authorization;
|
|
734
|
+
- re-authentication for sensitive operations;
|
|
735
|
+
- database migrations and session cleanup;
|
|
738
736
|
- never logging passwords, raw tokens, cookie headers, or password hashes.
|
|
739
737
|
|
|
740
|
-
Exact IP
|
|
738
|
+
Exact IP, User-Agent, and platform validation are defense in depth. They do not prevent every stolen-cookie replay scenario.
|