@gauts/auth 0.2.0 → 0.2.2
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 +559 -169
- package/dist/adapters/hono/index.d.ts +11 -3
- package/dist/adapters/hono/index.d.ts.map +1 -1
- package/dist/adapters/hono/index.js +28 -10
- package/dist/adapters/hono/index.js.map +1 -1
- package/dist/adapters/prisma/index.d.ts +31 -0
- package/dist/adapters/prisma/index.d.ts.map +1 -0
- package/dist/adapters/prisma/index.js +119 -0
- package/dist/adapters/prisma/index.js.map +1 -0
- package/dist/adapters/redis/index.d.ts +5 -5
- package/dist/adapters/redis/index.d.ts.map +1 -1
- package/dist/adapters/redis/index.js +19 -19
- package/dist/adapters/redis/index.js.map +1 -1
- package/dist/auth.d.ts +4 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -5
- package/dist/auth.js.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +17 -6
- package/dist/client/index.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -11
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/password/index.d.ts.map +1 -1
- package/dist/password/index.js +2 -4
- package/dist/password/index.js.map +1 -1
- package/dist/session/schema.d.ts.map +1 -1
- package/dist/session/schema.js +10 -10
- package/dist/session/schema.js.map +1 -1
- package/dist/session/service.d.ts +4 -4
- package/dist/session/service.d.ts.map +1 -1
- package/dist/session/service.js +130 -112
- package/dist/session/service.js.map +1 -1
- package/dist/session/token.js.map +1 -1
- package/dist/session/types.d.ts +45 -42
- package/dist/session/types.d.ts.map +1 -1
- package/package.json +92 -92
package/README.md
CHANGED
|
@@ -1,288 +1,678 @@
|
|
|
1
1
|
# `@gauts/auth`
|
|
2
2
|
|
|
3
|
-
Reusable password and opaque server-side
|
|
3
|
+
Reusable password authentication and opaque server-side sessions for Node.js applications.
|
|
4
4
|
|
|
5
|
-
`@gauts/auth` validates sessions through Redis and
|
|
5
|
+
`@gauts/auth` validates sessions through Redis and stores durable session history in a database. Redis is the authentication authority; the database is never used as an authentication fallback.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Requirements
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Sliding inactivity expiration without an absolute lifetime.
|
|
14
|
-
- Configurable client validation, using the complete User-Agent by default.
|
|
15
|
-
- Framework integrations through explicit package exports.
|
|
9
|
+
- Node.js 22 or newer.
|
|
10
|
+
- A connected Redis client.
|
|
11
|
+
- A Prisma client containing the required session model, or a custom `DbAdapter`.
|
|
12
|
+
- Hono 4 when using the Hono adapter.
|
|
16
13
|
|
|
17
14
|
## Installation
|
|
18
15
|
|
|
16
|
+
For Hono, Prisma, and Redis:
|
|
17
|
+
|
|
19
18
|
```bash
|
|
20
|
-
npm install @gauts/auth redis
|
|
19
|
+
npm install @gauts/auth hono redis @prisma/client
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
`hono` and `redis` are optional peer dependencies. The Prisma adapter does not import Prisma at runtime; it receives the generated client from the application.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
## Flow
|
|
25
|
+
|
|
26
|
+
Login:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
password -> configured algorithm -> database session row
|
|
30
|
+
-> Redis session
|
|
31
|
+
-> opaque HttpOnly cookie
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Authenticated HTTP request:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
cookie -> SHA-256 hash -> Redis -> client validation -> route
|
|
38
|
+
-> renewal due -> database expiry
|
|
39
|
+
-> Redis TTL
|
|
40
|
+
-> cookie expiry
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The browser receives one random 256-bit opaque token. The token remains the same throughout the session. Redis and the database store only its SHA-256 hash.
|
|
44
|
+
|
|
45
|
+
Sessions use sliding inactivity expiry. Activity before `renewInterval` performs no expiry write. The first eligible HTTP request after `renewInterval` extends the database expiry, Redis TTL, and browser cookie expiry.
|
|
41
46
|
|
|
47
|
+
## Prisma model
|
|
48
|
+
|
|
49
|
+
The default Prisma model is `auth_sessions`:
|
|
50
|
+
|
|
51
|
+
```prisma
|
|
52
|
+
model auth_sessions {
|
|
53
|
+
id String @id @default(uuid()) @db.VarChar(255)
|
|
54
|
+
account_id String @db.VarChar(255)
|
|
55
|
+
token_hash String @unique @db.VarChar(64)
|
|
56
|
+
ip String? @db.VarChar(45)
|
|
57
|
+
platform String? @db.VarChar(255)
|
|
58
|
+
agent String? @db.Text
|
|
59
|
+
expires_at DateTime @db.Timestamp(0)
|
|
60
|
+
revoked_at DateTime? @db.Timestamp(0)
|
|
61
|
+
created_at DateTime @default(now()) @db.Timestamp(0)
|
|
62
|
+
updated_at DateTime? @db.Timestamp(0)
|
|
63
|
+
|
|
64
|
+
@@index([account_id])
|
|
65
|
+
@@index([expires_at])
|
|
66
|
+
@@index([revoked_at])
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
These field names and compatible types are required. Additional indexes, relations, and optional/defaulted fields are allowed. Do not add required fields without defaults unless the application supplies them separately.
|
|
71
|
+
|
|
72
|
+
Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
|
|
73
|
+
|
|
74
|
+
## Quick start with Hono
|
|
75
|
+
|
|
76
|
+
### 1. Define the session data
|
|
77
|
+
|
|
78
|
+
The generic passed to `createHonoAuth` defines the application data cached in Redis and exposed on authenticated requests. Keep it small and never include secrets or password hashes.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
42
81
|
type AccountSession = {
|
|
43
|
-
|
|
44
|
-
|
|
82
|
+
email: string;
|
|
83
|
+
role: "admin" | "owner";
|
|
45
84
|
};
|
|
85
|
+
```
|
|
46
86
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
### 2. Create one auth instance
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createHonoAuth } from "@gauts/auth/hono";
|
|
91
|
+
import { createDbAdapter } from "@gauts/auth/prisma";
|
|
92
|
+
import { createRedisAdapter } from "@gauts/auth/redis";
|
|
93
|
+
|
|
94
|
+
export const auth = createHonoAuth<AccountSession>({
|
|
95
|
+
getIp: (c) => getTrustedClientIp(c),
|
|
96
|
+
db: createDbAdapter({
|
|
97
|
+
client: prisma,
|
|
98
|
+
}),
|
|
99
|
+
redis: createRedisAdapter({
|
|
100
|
+
client: redis,
|
|
101
|
+
config: { prefix: "my-app:auth" },
|
|
102
|
+
}),
|
|
56
103
|
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Both clients must already be initialized by the application. The package does not connect, reconnect, disconnect, or close them.
|
|
107
|
+
|
|
108
|
+
`createDbAdapter` uses `auth_sessions` by default. `config` is optional:
|
|
57
109
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
110
|
+
```ts
|
|
111
|
+
const db = createDbAdapter({
|
|
112
|
+
client: prisma,
|
|
113
|
+
config: {
|
|
114
|
+
table: "admin_sessions",
|
|
115
|
+
},
|
|
61
116
|
});
|
|
62
117
|
```
|
|
63
118
|
|
|
64
|
-
|
|
119
|
+
When `table` is supplied, TypeScript only accepts a compatible model from that generated Prisma client.
|
|
120
|
+
|
|
121
|
+
### 3. Type the Hono application
|
|
65
122
|
|
|
66
|
-
|
|
123
|
+
```ts
|
|
124
|
+
import { Hono } from "hono";
|
|
125
|
+
import type { HonoAuthEnv } from "@gauts/auth/hono";
|
|
67
126
|
|
|
68
|
-
|
|
69
|
-
|
|
127
|
+
const app = new Hono<HonoAuthEnv<AccountSession>>();
|
|
128
|
+
```
|
|
70
129
|
|
|
71
|
-
|
|
130
|
+
`auth.requireSession` installs both values:
|
|
72
131
|
|
|
73
132
|
```ts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
userAgent?: string | null;
|
|
77
|
-
platform?: string | null;
|
|
78
|
-
};
|
|
133
|
+
const session = c.get("session");
|
|
134
|
+
const account = c.get("account");
|
|
79
135
|
```
|
|
80
136
|
|
|
81
|
-
|
|
82
|
-
- `userAgent` is compared by default.
|
|
83
|
-
- `ip` and `platform` are always stored as session metadata, even when they are not compared.
|
|
84
|
-
- IPv4, IPv4-mapped IPv6, and IPv6 values are canonicalized by the package.
|
|
85
|
-
- Platform is unquoted, trimmed, and limited to 255 characters.
|
|
86
|
-
- User-Agent is stored and compared in full without truncation.
|
|
137
|
+
### 4. Login
|
|
87
138
|
|
|
88
|
-
|
|
89
|
-
trusted request IP and must not perform unrelated account or database work.
|
|
139
|
+
The application owns request validation, account lookup, rate limiting, status checks, and error responses.
|
|
90
140
|
|
|
91
|
-
|
|
141
|
+
```ts
|
|
142
|
+
app.post("/auth/login", async (c) => {
|
|
143
|
+
const { email, password } = await c.req.json<{
|
|
144
|
+
email: string;
|
|
145
|
+
password: string;
|
|
146
|
+
}>();
|
|
147
|
+
const account = await findAccount(email);
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
!account ||
|
|
151
|
+
!(await auth.password.verify({
|
|
152
|
+
password,
|
|
153
|
+
storedHash: account.password_hash,
|
|
154
|
+
}))
|
|
155
|
+
) {
|
|
156
|
+
return c.json({ error: "Invalid credentials." }, 401);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const session = await auth.createSession({
|
|
160
|
+
account_id: account.id,
|
|
161
|
+
context: c,
|
|
162
|
+
data: {
|
|
163
|
+
email: account.email,
|
|
164
|
+
role: account.role,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return c.json({ account: session.data });
|
|
169
|
+
});
|
|
170
|
+
```
|
|
92
171
|
|
|
93
|
-
|
|
172
|
+
`createSession` inserts the database row, creates the Redis session, writes the cookie, and returns the public session. The raw token remains internal to the Hono adapter.
|
|
173
|
+
|
|
174
|
+
### 5. Protect routes
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
app.get("/account", auth.requireSession, (c) => {
|
|
178
|
+
return c.json({
|
|
179
|
+
account: c.get("account"),
|
|
180
|
+
session_id: c.get("session").id,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`requireSession` authenticates the request. It does not apply application roles or permissions.
|
|
186
|
+
|
|
187
|
+
### 6. Logout
|
|
94
188
|
|
|
95
189
|
```ts
|
|
96
|
-
|
|
97
|
-
|
|
190
|
+
app.post("/auth/logout", async (c) => {
|
|
191
|
+
await auth.revokeSession(c);
|
|
192
|
+
return c.body(null, 204);
|
|
193
|
+
});
|
|
98
194
|
```
|
|
99
195
|
|
|
100
|
-
|
|
196
|
+
Logout deletes the Redis session, records revocation in the database, and clears the cookie. Backend revocation removes access; clearing the cookie is client cleanup.
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
### `createHonoAuth`
|
|
101
201
|
|
|
102
202
|
```ts
|
|
103
|
-
const auth =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
203
|
+
const auth = createHonoAuth<AccountSession>({
|
|
204
|
+
getIp,
|
|
205
|
+
db,
|
|
206
|
+
redis,
|
|
207
|
+
password,
|
|
208
|
+
session,
|
|
209
|
+
cookie,
|
|
109
210
|
});
|
|
110
211
|
```
|
|
111
212
|
|
|
112
|
-
|
|
213
|
+
| Property | Required | Purpose |
|
|
214
|
+
| ---------- | -------- | -------------------------------------------------------------------- |
|
|
215
|
+
| `getIp` | Yes | Returns the trusted client IP from the Hono context. |
|
|
216
|
+
| `db` | Yes | Stores durable session history. |
|
|
217
|
+
| `redis` | Yes | Stores and validates active sessions. |
|
|
218
|
+
| `password` | No | Selects password algorithm and cost limits. Defaults to Argon2id. |
|
|
219
|
+
| `session` | No | Configures expiry, renewal, maximum sessions, and client validation. |
|
|
220
|
+
| `cookie` | No | Configures the Hono session cookie. |
|
|
221
|
+
|
|
222
|
+
Configuration is resolved once when the auth instance is created. Invalid configuration throws `AUTH_CONFIG_INVALID` during startup.
|
|
223
|
+
|
|
224
|
+
### Password
|
|
225
|
+
|
|
226
|
+
Argon2id is the default:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
password: {
|
|
230
|
+
algorithm: "argon2id",
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Argon2id property | Default | Allowed | Purpose |
|
|
235
|
+
| ----------------- | -----------: | ---------------------: | --------------------------------- |
|
|
236
|
+
| `algorithm` | `"argon2id"` | `"argon2id"` | Selects Argon2id. May be omitted. |
|
|
237
|
+
| `hashLength` | `32` | `16` to `64` | Hash output length in bytes. |
|
|
238
|
+
| `maxBytes` | `1024` | `1` to `1,048,576` | Maximum UTF-8 password size. |
|
|
239
|
+
| `memoryCost` | `65,536` | `8,192` to `1,048,576` | Memory cost in KiB. |
|
|
240
|
+
| `parallelism` | `4` | `1` to `16` | Number of lanes. |
|
|
241
|
+
| `timeCost` | `3` | `1` to `10` | Number of iterations. |
|
|
242
|
+
|
|
243
|
+
Applications with bcrypt hashes must select bcrypt explicitly:
|
|
113
244
|
|
|
114
245
|
```ts
|
|
115
246
|
password: {
|
|
116
247
|
algorithm: "bcrypt",
|
|
117
|
-
maxBytes: 72,
|
|
118
|
-
verifyMaxBytes: 1024,
|
|
119
248
|
}
|
|
120
249
|
```
|
|
121
250
|
|
|
122
|
-
|
|
251
|
+
| bcrypt property | Default | Allowed | Purpose |
|
|
252
|
+
| ---------------- | -------: | ------------------------: | --------------------------------------------- |
|
|
253
|
+
| `algorithm` | Required | `"bcrypt"` | Selects bcrypt for hashing and verification. |
|
|
254
|
+
| `maxBytes` | `72` | `1` to `72` | Maximum UTF-8 size accepted for new hashes. |
|
|
255
|
+
| `rounds` | `12` | `4` to `31` | Cost factor. |
|
|
256
|
+
| `verifyMaxBytes` | `72` | `maxBytes` to `1,048,576` | Maximum UTF-8 size accepted for verification. |
|
|
123
257
|
|
|
124
|
-
|
|
258
|
+
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.
|
|
125
259
|
|
|
126
|
-
|
|
260
|
+
bcrypt ignores bytes after the first 72. Keep `verifyMaxBytes` at `72` unless preserving historical truncation is an explicit application requirement.
|
|
261
|
+
|
|
262
|
+
### Session
|
|
127
263
|
|
|
128
264
|
```ts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
password,
|
|
137
|
-
storedHash: account.passwordHash,
|
|
138
|
-
}))
|
|
139
|
-
) {
|
|
140
|
-
return c.json({ error: "Invalid credentials." }, 401);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const session = await hono.createSession({
|
|
144
|
-
accountId: account.id,
|
|
145
|
-
context: c,
|
|
146
|
-
data: {
|
|
147
|
-
email: account.email,
|
|
148
|
-
role: account.role,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
265
|
+
session: {
|
|
266
|
+
max: 10,
|
|
267
|
+
renewInterval: 24 * 60 * 60,
|
|
268
|
+
ttl: 7 * 24 * 60 * 60,
|
|
269
|
+
validation: ["agent"],
|
|
270
|
+
}
|
|
271
|
+
```
|
|
151
272
|
|
|
152
|
-
|
|
153
|
-
|
|
273
|
+
Time values are seconds.
|
|
274
|
+
|
|
275
|
+
| Property | Default | Allowed | Purpose |
|
|
276
|
+
| --------------- | ----------: | --------------------------------------------------: | ------------------------------------------------------------------------------------------ |
|
|
277
|
+
| `max` | `10` | `1` to `10,000` | Maximum active sessions per account. |
|
|
278
|
+
| `renewInterval` | `86,400` | `1` to `ttl - 1` | Minimum activity interval between expiry writes. |
|
|
279
|
+
| `ttl` | `604,800` | `60` to `31,536,000` | Inactivity lifetime on creation and renewal. |
|
|
280
|
+
| `validation` | `["agent"]` | Unique combination of `agent`, `ip`, and `platform` | Client fields compared during validation. An empty array disables client-field comparison. |
|
|
281
|
+
|
|
282
|
+
Expiry is sliding and has no forced absolute lifetime. The opaque token is not rotated during renewal.
|
|
283
|
+
|
|
284
|
+
### Cookie
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
cookie: {
|
|
288
|
+
domain: undefined,
|
|
289
|
+
name: "__Host-session",
|
|
290
|
+
path: "/",
|
|
291
|
+
sameSite: "Lax",
|
|
292
|
+
secure: true,
|
|
293
|
+
}
|
|
154
294
|
```
|
|
155
295
|
|
|
156
|
-
|
|
296
|
+
| Property | Default | Purpose |
|
|
297
|
+
| ---------- | ------------------ | ---------------------------------------------------- |
|
|
298
|
+
| `name` | `"__Host-session"` | Cookie name. |
|
|
299
|
+
| `domain` | Not set | Optional domain; without it the cookie is host-only. |
|
|
300
|
+
| `path` | `"/"` | Cookie path. |
|
|
301
|
+
| `sameSite` | `"Lax"` | `"Strict"`, `"Lax"`, or `"None"`. |
|
|
302
|
+
| `secure` | `true` | Sends the cookie only over HTTPS. |
|
|
157
303
|
|
|
158
|
-
|
|
304
|
+
`HttpOnly` is always enabled.
|
|
305
|
+
|
|
306
|
+
- `__Host-` requires `secure: true`, `path: "/"`, and no domain.
|
|
307
|
+
- `__Secure-` requires `secure: true`.
|
|
308
|
+
- `SameSite=None` requires `secure: true`.
|
|
309
|
+
- Local HTTP development with `secure: false` requires a custom name without a secure prefix.
|
|
310
|
+
|
|
311
|
+
### Redis adapter
|
|
159
312
|
|
|
160
313
|
```ts
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
314
|
+
import { createClient } from "redis";
|
|
315
|
+
import { createRedisAdapter } from "@gauts/auth/redis";
|
|
316
|
+
|
|
317
|
+
const redis = createClient({ url: process.env.REDIS_URL });
|
|
318
|
+
await redis.connect();
|
|
164
319
|
|
|
165
|
-
|
|
320
|
+
const adapter = createRedisAdapter({
|
|
321
|
+
client: redis,
|
|
322
|
+
config: {
|
|
323
|
+
prefix: "my-app:auth",
|
|
324
|
+
},
|
|
166
325
|
});
|
|
167
326
|
```
|
|
168
327
|
|
|
169
|
-
|
|
328
|
+
`config` is optional. The prefix defaults to `gauts:auth`, supports letters, numbers, `:`, `_`, and `-`, and has a maximum of 128 characters.
|
|
170
329
|
|
|
171
|
-
|
|
330
|
+
```text
|
|
331
|
+
<prefix>:session:<sha256-token-hash>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Redis failures throw `REDIS_UNAVAILABLE`. Authentication never falls back to the database.
|
|
335
|
+
|
|
336
|
+
### Prisma adapter
|
|
337
|
+
|
|
338
|
+
Default model:
|
|
172
339
|
|
|
173
340
|
```ts
|
|
174
|
-
|
|
175
|
-
const session = await hono.resolveSession(c);
|
|
341
|
+
import { createDbAdapter } from "@gauts/auth/prisma";
|
|
176
342
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
};
|
|
343
|
+
const db = createDbAdapter({
|
|
344
|
+
client: prisma,
|
|
345
|
+
});
|
|
180
346
|
```
|
|
181
347
|
|
|
182
|
-
|
|
348
|
+
Custom compatible model:
|
|
183
349
|
|
|
184
350
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
351
|
+
const db = createDbAdapter({
|
|
352
|
+
client: prisma,
|
|
353
|
+
config: {
|
|
354
|
+
table: "admin_sessions",
|
|
355
|
+
},
|
|
188
356
|
});
|
|
189
357
|
```
|
|
190
358
|
|
|
191
|
-
The adapter
|
|
192
|
-
|
|
359
|
+
The adapter owns the five database operations required by the session service: create, find one, find active, revoke, and update expiry. Applications do not implement those queries.
|
|
360
|
+
|
|
361
|
+
The default model is `auth_sessions`. If that model does not exist, `config.table` is required. TypeScript rejects a selected model whose generated result does not contain the complete session record shape.
|
|
362
|
+
|
|
363
|
+
Database failures are exposed by the auth core as `DB_UNAVAILABLE`.
|
|
193
364
|
|
|
194
|
-
|
|
365
|
+
### Custom database adapter
|
|
366
|
+
|
|
367
|
+
Applications not using Prisma can implement the exported `DbAdapter` contract:
|
|
195
368
|
|
|
196
369
|
```ts
|
|
197
|
-
|
|
370
|
+
import type { DbAdapter } from "@gauts/auth";
|
|
371
|
+
|
|
372
|
+
const db = {
|
|
373
|
+
create: async (session) => {},
|
|
374
|
+
find: async ({ account_id, session_id }) => null,
|
|
375
|
+
findActive: async ({ account_id, now }) => [],
|
|
376
|
+
revoke: async ({ revoked_at, session_ids }) => {},
|
|
377
|
+
updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
|
|
378
|
+
} satisfies DbAdapter;
|
|
379
|
+
```
|
|
198
380
|
|
|
199
|
-
|
|
200
|
-
accountId,
|
|
201
|
-
sessionId,
|
|
202
|
-
});
|
|
381
|
+
The adapter must persist only token hashes, never raw tokens. `find` must scope by both `account_id` and `session_id`. `findActive` must return only non-revoked rows whose `expires_at` is greater than `now`.
|
|
203
382
|
|
|
204
|
-
|
|
383
|
+
### Trusted client IP
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
getIp: (c) => getTrustedClientIp(c);
|
|
205
387
|
```
|
|
206
388
|
|
|
207
|
-
|
|
389
|
+
`getIp` runs during login and every authenticated HTTP request. It may be synchronous or asynchronous and returns `string`, `null`, or `undefined`.
|
|
390
|
+
|
|
391
|
+
Only the application knows which reverse proxies and forwarding headers are trusted. The package does not select `X-Forwarded-For`, `CF-Connecting-IP`, socket addresses, or another source. It canonicalizes the value returned by the application.
|
|
392
|
+
|
|
393
|
+
The Hono adapter reads the remaining metadata from request headers:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
type SessionClientInput = {
|
|
397
|
+
agent?: string | null;
|
|
398
|
+
ip?: string | null;
|
|
399
|
+
platform?: string | null;
|
|
400
|
+
};
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
- IPv4, IPv4-mapped IPv6, and IPv6 are canonicalized.
|
|
404
|
+
- `::1` becomes `127.0.0.1`.
|
|
405
|
+
- Invalid or empty IP values become `null`.
|
|
406
|
+
- Platform comes from `Sec-CH-UA-Platform`, is unquoted and limited to 255 characters.
|
|
407
|
+
- Agent comes from `User-Agent` and is stored in full without truncation.
|
|
408
|
+
- No GeoIP, DNS, country, or database lookup is performed.
|
|
409
|
+
|
|
410
|
+
## Password API
|
|
411
|
+
|
|
412
|
+
### `auth.password.algorithm`
|
|
413
|
+
|
|
414
|
+
The resolved algorithm: `"argon2id"` or `"bcrypt"`.
|
|
208
415
|
|
|
209
|
-
|
|
416
|
+
### `auth.password.hash(password)`
|
|
210
417
|
|
|
211
|
-
|
|
418
|
+
Validates the UTF-8 byte length and creates a hash using the configured algorithm.
|
|
212
419
|
|
|
213
420
|
```ts
|
|
214
|
-
await auth.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
421
|
+
const password_hash = await auth.password.hash(password);
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### `auth.password.verify({ password, storedHash })`
|
|
425
|
+
|
|
426
|
+
Verifies only with the configured algorithm. A hash from another algorithm or an invalid hash returns `false`.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
const valid = await auth.password.verify({
|
|
430
|
+
password,
|
|
431
|
+
storedHash: account.password_hash,
|
|
220
432
|
});
|
|
221
433
|
```
|
|
222
434
|
|
|
223
|
-
|
|
435
|
+
Invalid password size throws `PASSWORD_INPUT_INVALID`. Wrong credentials return `false`.
|
|
436
|
+
|
|
437
|
+
## Hono API
|
|
224
438
|
|
|
225
|
-
|
|
439
|
+
### `auth.createSession({ account_id, context, data })`
|
|
226
440
|
|
|
227
|
-
|
|
441
|
+
Creates the database and Redis session, writes the HttpOnly cookie, and returns `Session<TData>`.
|
|
228
442
|
|
|
229
|
-
|
|
230
|
-
- Account ID and relation owned by the application.
|
|
231
|
-
- SHA-256 token hash.
|
|
232
|
-
- Canonical IP.
|
|
233
|
-
- Platform metadata.
|
|
234
|
-
- Complete User-Agent.
|
|
235
|
-
- Creation, current expiry, update, and revocation timestamps.
|
|
443
|
+
### `auth.resolveSession(context)`
|
|
236
444
|
|
|
237
|
-
|
|
445
|
+
Reads and validates the cookie and returns `Session<TData>`.
|
|
238
446
|
|
|
239
|
-
|
|
447
|
+
- Throws `SESSION_INVALID` when the cookie or Redis session is missing or invalid.
|
|
448
|
+
- Clears an invalid cookie.
|
|
449
|
+
- Compares configured client fields.
|
|
450
|
+
- Renews database expiry, Redis TTL, and cookie expiry only when due.
|
|
451
|
+
- Revokes the backend session and throws `SESSION_CLIENT_MISMATCH` after a mismatch.
|
|
240
452
|
|
|
241
|
-
|
|
453
|
+
### `auth.requireSession`
|
|
454
|
+
|
|
455
|
+
Middleware that calls `resolveSession` and sets:
|
|
242
456
|
|
|
243
457
|
```ts
|
|
244
|
-
session
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
458
|
+
c.set("session", session);
|
|
459
|
+
c.set("account", session.data);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### `auth.revokeSession(context)`
|
|
463
|
+
|
|
464
|
+
Revokes the session represented by the request cookie and clears that cookie. Returns the revoked session IDs.
|
|
465
|
+
|
|
466
|
+
### `auth.clearSession(context)`
|
|
467
|
+
|
|
468
|
+
Deletes only the response cookie. It does not revoke Redis or update the database and must not be used as logout.
|
|
469
|
+
|
|
470
|
+
### `auth.getToken(context)`
|
|
471
|
+
|
|
472
|
+
Returns the raw cookie token or `null`. Never log, persist, or expose this value in a response.
|
|
473
|
+
|
|
474
|
+
## Core session API
|
|
475
|
+
|
|
476
|
+
The same instance exposes the framework-independent `auth.session` service.
|
|
477
|
+
|
|
478
|
+
### `auth.session.create({ account_id, client, data })`
|
|
479
|
+
|
|
480
|
+
Creates a session and returns:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
{
|
|
484
|
+
session: Session<TData>;
|
|
485
|
+
token: string;
|
|
249
486
|
}
|
|
250
487
|
```
|
|
251
488
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
489
|
+
Prefer `auth.createSession` in Hono applications because the core cannot write the browser cookie.
|
|
490
|
+
|
|
491
|
+
### `auth.session.resolve({ client, token })`
|
|
492
|
+
|
|
493
|
+
Validates and renews when due. It returns `null` for an invalid or expired session, otherwise:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
{
|
|
497
|
+
renewed: boolean;
|
|
498
|
+
session: Session<TData>;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
A framework using the core directly must update the cookie expiry when `renewed` is `true`.
|
|
503
|
+
|
|
504
|
+
### `auth.session.validate({ client, token })`
|
|
505
|
+
|
|
506
|
+
Validates without renewing Redis, database expiry, or cookie expiry. A configured client mismatch still revokes the backend session.
|
|
507
|
+
|
|
508
|
+
### `auth.session.list(account_id)`
|
|
509
|
+
|
|
510
|
+
Returns sessions that are active in both the database and Redis. The public result never contains `token_hash`.
|
|
511
|
+
|
|
512
|
+
### `auth.session.revoke({ account_id, session_id })`
|
|
513
|
+
|
|
514
|
+
Revokes one session after confirming it belongs to the supplied account. Throws `SESSION_NOT_FOUND` when it is absent or already revoked.
|
|
515
|
+
|
|
516
|
+
### `auth.session.revokeAccount(account_id)`
|
|
517
|
+
|
|
518
|
+
Revokes every active session for one account.
|
|
519
|
+
|
|
520
|
+
### `auth.session.revokeToken(token)`
|
|
521
|
+
|
|
522
|
+
Revokes the Redis session represented by a raw token and records the revocation in the database. Invalid or absent tokens return an empty array.
|
|
523
|
+
|
|
524
|
+
### `auth.session.sync({ account_id, data })`
|
|
525
|
+
|
|
526
|
+
Replaces the cached `data` in every active Redis session for the account while preserving tokens and TTLs.
|
|
527
|
+
|
|
528
|
+
Use it after changing cached account data. Use `revokeAccount` when a change must force re-authentication.
|
|
529
|
+
|
|
530
|
+
## Public session shapes
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
type Session<TData> = {
|
|
534
|
+
account_id: string;
|
|
535
|
+
client: {
|
|
536
|
+
agent: string | null;
|
|
537
|
+
ip: string | null;
|
|
538
|
+
platform: string | null;
|
|
539
|
+
};
|
|
540
|
+
created_at: Date;
|
|
541
|
+
data: TData;
|
|
542
|
+
expires_at: Date;
|
|
543
|
+
id: string;
|
|
544
|
+
touched_at: Date;
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
type ActiveSession = {
|
|
550
|
+
account_id: string;
|
|
551
|
+
agent: string | null;
|
|
552
|
+
created_at: Date;
|
|
553
|
+
expires_at: Date;
|
|
554
|
+
id: string;
|
|
555
|
+
ip: string | null;
|
|
556
|
+
platform: string | null;
|
|
557
|
+
revoked_at: Date | null;
|
|
558
|
+
updated_at: Date | null;
|
|
559
|
+
};
|
|
560
|
+
```
|
|
257
561
|
|
|
258
562
|
## Client validation
|
|
259
563
|
|
|
260
|
-
The default compares
|
|
564
|
+
The default compares the complete User-Agent:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
session: {
|
|
568
|
+
validation: ["agent"],
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
An administration application can also bind IP and platform:
|
|
261
573
|
|
|
262
574
|
```ts
|
|
263
575
|
session: {
|
|
264
|
-
validation: ["
|
|
576
|
+
validation: ["ip", "agent", "platform"],
|
|
265
577
|
}
|
|
266
578
|
```
|
|
267
579
|
|
|
268
|
-
|
|
580
|
+
An empty array validates only the opaque token:
|
|
269
581
|
|
|
270
582
|
```ts
|
|
271
583
|
session: {
|
|
272
|
-
validation: [
|
|
584
|
+
validation: [],
|
|
273
585
|
}
|
|
274
586
|
```
|
|
275
587
|
|
|
276
|
-
|
|
588
|
+
Comparison is exact after normalization. A configured mismatch deletes the Redis session and records revocation in the database, so both the suspicious client and legitimate browser must authenticate again.
|
|
589
|
+
|
|
590
|
+
Exact IP validation can log out legitimate users on VPN, mobile, or rotating networks. Client matching is defense in depth; it does not replace TLS, secure cookies, CSRF protection, XSS prevention, or explicit re-authentication for sensitive actions.
|
|
591
|
+
|
|
592
|
+
## Expiration and renewal
|
|
593
|
+
|
|
594
|
+
- Creation assigns `ttl` to Redis, the database row, and the cookie.
|
|
595
|
+
- Requests before `renewInterval` validate without expiry writes.
|
|
596
|
+
- The first eligible HTTP request after `renewInterval` sets expiry to `now + ttl` in Redis and the database.
|
|
597
|
+
- The Hono adapter sends `Set-Cookie` only when renewal occurs.
|
|
598
|
+
- The opaque token never changes during renewal.
|
|
599
|
+
- Continued eligible HTTP activity can keep a session alive indefinitely.
|
|
600
|
+
- A session without renewal activity expires after `ttl`.
|
|
601
|
+
- `validate` never renews; `resolve` renews only when due.
|
|
277
602
|
|
|
278
|
-
|
|
603
|
+
## Errors
|
|
279
604
|
|
|
280
|
-
|
|
605
|
+
All package errors have `name: "AuthError"` and a typed `code`.
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import { isAuthError } from "@gauts/auth";
|
|
609
|
+
|
|
610
|
+
app.onError((error, c) => {
|
|
611
|
+
if (!isAuthError(error)) {
|
|
612
|
+
return c.json({ error: "Internal server error." }, 500);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (error.code === "REDIS_UNAVAILABLE" || error.code === "DB_UNAVAILABLE") {
|
|
616
|
+
return c.json({ error: "Authentication service unavailable." }, 503);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return c.json({ error: error.message }, 401);
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
| Code | Meaning |
|
|
624
|
+
| ------------------------- | ---------------------------------------------------------------------- |
|
|
625
|
+
| `AUTH_CONFIG_INVALID` | Invalid startup configuration or adapter contract. |
|
|
626
|
+
| `PASSWORD_INPUT_INVALID` | Empty or oversized password input. |
|
|
627
|
+
| `SESSION_CLIENT_MISMATCH` | A configured client field changed and the backend session was revoked. |
|
|
628
|
+
| `SESSION_DATA_INVALID` | Required session input or stored Redis payload is invalid. |
|
|
629
|
+
| `SESSION_INVALID` | Hono cookie or Redis session is missing, expired, or invalid. |
|
|
630
|
+
| `SESSION_LIMIT_REACHED` | The account already has the maximum active sessions. |
|
|
631
|
+
| `SESSION_NOT_FOUND` | The selected session is absent or already revoked. |
|
|
632
|
+
| `DB_UNAVAILABLE` | The database adapter failed or returned invalid session data. |
|
|
633
|
+
| `REDIS_UNAVAILABLE` | Redis failed or returned invalid data; authentication fails closed. |
|
|
634
|
+
|
|
635
|
+
Applications decide final HTTP statuses and public messages.
|
|
636
|
+
|
|
637
|
+
## Framework-independent usage
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import { createAuth } from "@gauts/auth";
|
|
641
|
+
import { createDbAdapter } from "@gauts/auth/prisma";
|
|
642
|
+
import { createRedisAdapter } from "@gauts/auth/redis";
|
|
643
|
+
|
|
644
|
+
const auth = createAuth<AccountSession>({
|
|
645
|
+
db: createDbAdapter({ client: prisma }),
|
|
646
|
+
redis: createRedisAdapter({ client: redis }),
|
|
647
|
+
});
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
The application must then extract client input, read and secure the raw token, call `resolve`, and deliver renewed cookie expiry when `renewed` is `true`.
|
|
651
|
+
|
|
652
|
+
Hono applications should normally use `createHonoAuth`. `createHonoAdapter` remains available when deliberately composing the core and Hono adapter separately.
|
|
653
|
+
|
|
654
|
+
## Application responsibilities
|
|
655
|
+
|
|
656
|
+
The package does not provide:
|
|
657
|
+
|
|
658
|
+
- Registration, account lookup, OTP, OAuth, password reset, or email flows.
|
|
659
|
+
- Endpoints or UI components.
|
|
660
|
+
- Roles, permissions, or application authorization policies.
|
|
661
|
+
- Prisma migrations or database/Redis connection lifecycle.
|
|
662
|
+
- Trusted proxy policy.
|
|
663
|
+
- Rate limiting, CSRF, CORS, CSP, logging, or notifications.
|
|
664
|
+
|
|
665
|
+
The application must use HTTPS in production, protect login endpoints, validate input, prevent account enumeration, define trusted proxies, and apply CSRF and XSS protections.
|
|
666
|
+
|
|
667
|
+
Never log raw passwords, raw session tokens, cookies, password hashes, or Redis session payloads.
|
|
281
668
|
|
|
282
669
|
## Package exports
|
|
283
670
|
|
|
284
671
|
```text
|
|
285
|
-
@gauts/auth
|
|
286
|
-
@gauts/auth/
|
|
287
|
-
@gauts/auth/
|
|
672
|
+
@gauts/auth createAuth, isAuthError, and core types
|
|
673
|
+
@gauts/auth/prisma createDbAdapter and Prisma adapter types
|
|
674
|
+
@gauts/auth/redis createRedisAdapter and Redis adapter types
|
|
675
|
+
@gauts/auth/hono createHonoAuth, createHonoAdapter, and Hono types
|
|
288
676
|
```
|
|
677
|
+
|
|
678
|
+
The compiled Hono example is available in [`examples/hono`](./examples/hono).
|