@chidchanun/bcp 0.2.4 → 0.2.6
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 +181 -77
- package/docs/README.md +84 -127
- package/docs/api-manifest.json +6 -3
- package/docs/api-reference.md +60 -4
- package/docs/auth-route-guards.md +104 -61
- package/docs/auth-session-store.md +204 -0
- package/docs/authentication.md +128 -44
- package/docs/authorization-security.md +298 -0
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +19 -4
- package/docs/releases/0.2.5.md +152 -0
- package/docs/releases/0.2.6.md +194 -0
- package/docs/security.md +55 -1
- package/package.json +1 -1
- package/packages/client/src/auth.ts +36 -0
- package/packages/client/src/server.ts +16 -0
- package/packages/server/src/auth-guard.ts +204 -95
- package/packages/server/src/auth-session-store.ts +318 -0
- package/packages/server/src/auth.ts +443 -50
- package/packages/server/src/authorization.ts +368 -0
- package/packages/server/src/request-security.ts +596 -0
package/docs/api-manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
6
|
"coverage": "public-entrypoints",
|
|
7
7
|
"entrypoints": [
|
|
@@ -85,10 +85,12 @@
|
|
|
85
85
|
"source": "packages/client/src/auth.ts",
|
|
86
86
|
"environment": "server",
|
|
87
87
|
"route": "/docs/api-reference#bcp-auth",
|
|
88
|
-
"summary": "Authentication,
|
|
88
|
+
"summary": "Authentication Platform v2 plus permission checks, authorization policies and auth/guest/role/permission route guards.",
|
|
89
89
|
"guides": [
|
|
90
90
|
"/docs/authentication",
|
|
91
|
+
"/docs/auth-session-store",
|
|
91
92
|
"/docs/auth-route-guards",
|
|
93
|
+
"/docs/authorization-security",
|
|
92
94
|
"/docs/session-auth"
|
|
93
95
|
]
|
|
94
96
|
},
|
|
@@ -97,9 +99,10 @@
|
|
|
97
99
|
"source": "packages/client/src/server.ts",
|
|
98
100
|
"environment": "server",
|
|
99
101
|
"route": "/docs/api-reference#bcp-server",
|
|
100
|
-
"summary": "Request context, cookies, logging, production hardening, upload, storage, response and session APIs.",
|
|
102
|
+
"summary": "Request context, cookies, CSRF/same-origin protection, logging, production hardening, upload, storage, response and session APIs.",
|
|
101
103
|
"guides": [
|
|
102
104
|
"/docs/server-request-apis",
|
|
105
|
+
"/docs/authorization-security",
|
|
103
106
|
"/docs/file-upload",
|
|
104
107
|
"/docs/storage",
|
|
105
108
|
"/docs/storage-ecosystem",
|
package/docs/api-reference.md
CHANGED
|
@@ -164,53 +164,109 @@ Related guides: [Database](database.md), [Database Migrations](database-migratio
|
|
|
164
164
|
|
|
165
165
|
## `bcp/auth`
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Server-only authentication and authorization APIs.
|
|
168
168
|
|
|
169
169
|
```ts
|
|
170
170
|
import {
|
|
171
|
+
AuthorizationError,
|
|
172
|
+
assertPermission,
|
|
171
173
|
auth,
|
|
174
|
+
authorize,
|
|
175
|
+
can,
|
|
176
|
+
cannot,
|
|
172
177
|
createAuth,
|
|
173
178
|
createAuthGuard,
|
|
179
|
+
createGuestGuard,
|
|
180
|
+
createMemoryAuthSessionStore,
|
|
181
|
+
createPermissionGuard,
|
|
174
182
|
createRoleGuard,
|
|
183
|
+
defineAuthorizationPolicy,
|
|
175
184
|
getGuardAuth,
|
|
176
185
|
getSession,
|
|
186
|
+
getUserPermissions,
|
|
187
|
+
hasPermission,
|
|
177
188
|
login,
|
|
178
189
|
logout,
|
|
190
|
+
logoutAll,
|
|
179
191
|
requireAuth,
|
|
192
|
+
requireGuest,
|
|
193
|
+
requirePermission,
|
|
180
194
|
requireRole,
|
|
195
|
+
revokeSession,
|
|
196
|
+
revokeUserSessions,
|
|
181
197
|
rotateSession,
|
|
198
|
+
type AuthApi,
|
|
199
|
+
type AuthLoginOptions,
|
|
200
|
+
type AuthOptions,
|
|
201
|
+
type AuthSession,
|
|
202
|
+
type AuthSessionStore,
|
|
203
|
+
type AuthSessionStoreRecord,
|
|
204
|
+
type AuthUser,
|
|
205
|
+
type AuthorizationContext,
|
|
206
|
+
type AuthorizationMatch,
|
|
207
|
+
type AuthorizationPolicy,
|
|
208
|
+
type MemoryAuthSessionStore,
|
|
209
|
+
type PermissionCheckOptions,
|
|
210
|
+
type PermissionRequirement,
|
|
211
|
+
type RequirePermissionOptions,
|
|
182
212
|
} from "bcp/auth";
|
|
183
213
|
```
|
|
184
214
|
|
|
185
|
-
|
|
215
|
+
The default authentication mode remains stateless signed JWT-cookie authentication.
|
|
216
|
+
|
|
217
|
+
Configure `AuthOptions.store` to enable server-side session revocation and `idleTimeout`. `createMemoryAuthSessionStore()` is provided for development/testing; production multi-instance applications should implement `AuthSessionStore` with shared durable storage.
|
|
218
|
+
|
|
219
|
+
Authorization & Security v2 adds permission checks, permission route guards and resource-aware policy functions:
|
|
220
|
+
|
|
221
|
+
- `hasPermission()` / `assertPermission()` for flat permission or scope fields,
|
|
222
|
+
- `requirePermission()` / `createPermissionGuard()` for route protection,
|
|
223
|
+
- `defineAuthorizationPolicy()` for application policy definitions,
|
|
224
|
+
- `can()` / `cannot()` for policy checks,
|
|
225
|
+
- `authorize()` for throwing `AuthorizationError` when a policy denies access.
|
|
226
|
+
|
|
227
|
+
Related guides: [Authentication](authentication.md), [Auth Session Stores](auth-session-store.md), [Auth Route Guards](auth-route-guards.md), [Authorization & Security v2](authorization-security.md), [JWT Sessions](session-auth.md).
|
|
186
228
|
|
|
187
229
|
## `bcp/server`
|
|
188
230
|
|
|
189
231
|
Server request/runtime APIs.
|
|
190
232
|
|
|
191
|
-
This entrypoint includes request context, cookies, logging, graceful shutdown hooks, multipart upload helpers, storage adapters, file delivery, response helpers and low-level session primitives.
|
|
233
|
+
This entrypoint includes request context, cookies, CSRF and same-origin protection, logging, graceful shutdown hooks, multipart upload helpers, storage adapters, file delivery, response helpers and low-level session primitives.
|
|
192
234
|
|
|
193
235
|
```ts
|
|
194
236
|
import {
|
|
195
237
|
clientIp,
|
|
196
238
|
cookies,
|
|
239
|
+
createCsrfToken,
|
|
197
240
|
createLocalStorage,
|
|
198
241
|
createLogger,
|
|
199
242
|
createS3Storage,
|
|
200
243
|
createStorageResponse,
|
|
244
|
+
destroyCsrfToken,
|
|
201
245
|
getProductionHardeningConfig,
|
|
202
246
|
headers,
|
|
247
|
+
isSafeHttpMethod,
|
|
248
|
+
isSameOriginRequest,
|
|
203
249
|
json,
|
|
204
250
|
redirect,
|
|
205
251
|
registerShutdownHook,
|
|
206
252
|
requestId,
|
|
207
253
|
requestMethod,
|
|
208
254
|
requestUrl,
|
|
255
|
+
requireCsrfRequest,
|
|
256
|
+
requireSameOriginRequest,
|
|
209
257
|
storeMultipartFile,
|
|
258
|
+
verifyCsrfRequest,
|
|
259
|
+
verifyCsrfToken,
|
|
260
|
+
RequestSecurityError,
|
|
261
|
+
type CsrfTokenOptions,
|
|
262
|
+
type SameOriginOptions,
|
|
263
|
+
type VerifyCsrfRequestOptions,
|
|
210
264
|
} from "bcp/server";
|
|
211
265
|
```
|
|
212
266
|
|
|
213
|
-
|
|
267
|
+
`requireSameOriginRequest()` protects unsafe cookie-authenticated mutations by validating `Origin`/`Referer`. `createCsrfToken()` and `requireCsrfRequest()` provide signed double-submit style CSRF protection. `BCP_CSRF_SECRET` is preferred when configured and falls back to `BCP_SESSION_SECRET`.
|
|
268
|
+
|
|
269
|
+
Related guides: [Server Request APIs](server-request-apis.md), [Authorization & Security v2](authorization-security.md), [File Upload](file-upload.md), [Storage](storage.md), [Storage Ecosystem](storage-ecosystem.md), [Production Hardening](production-hardening.md).
|
|
214
270
|
|
|
215
271
|
## `bcp/server-only`
|
|
216
272
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auth Route Guards
|
|
2
2
|
|
|
3
|
-
BCP Framework
|
|
3
|
+
BCP Framework integrates Authentication Platform v2 and Authorization & Security v2 with route guards through the server-only `bcp/auth` entrypoint.
|
|
4
4
|
|
|
5
5
|
## Protect a route tree
|
|
6
6
|
|
|
@@ -18,22 +18,52 @@ export const guard =
|
|
|
18
18
|
});
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The guard redirects unauthenticated requests to `/login` with status `303` by default.
|
|
21
|
+
The guard redirects unauthenticated requests to `/login` with status `303` by default.
|
|
22
22
|
|
|
23
|
-
To return `401 Unauthorized` instead
|
|
23
|
+
To return `401 Unauthorized` instead:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
export const guard =
|
|
27
|
+
createAuthGuard({
|
|
28
|
+
redirectTo:
|
|
29
|
+
null,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Guest-only routes — 0.2.5+
|
|
34
|
+
|
|
35
|
+
Login, registration, and password-recovery entry pages often should not remain accessible after the user has already authenticated.
|
|
36
|
+
|
|
37
|
+
Use `createGuestGuard()`:
|
|
24
38
|
|
|
25
39
|
```ts
|
|
26
40
|
import {
|
|
27
|
-
|
|
41
|
+
createGuestGuard,
|
|
28
42
|
} from "bcp/auth";
|
|
29
43
|
|
|
30
44
|
export const guard =
|
|
31
|
-
|
|
45
|
+
createGuestGuard({
|
|
32
46
|
redirectTo:
|
|
33
|
-
|
|
47
|
+
"/dashboard",
|
|
34
48
|
});
|
|
35
49
|
```
|
|
36
50
|
|
|
51
|
+
Behavior:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
anonymous user
|
|
55
|
+
↓
|
|
56
|
+
page continues
|
|
57
|
+
|
|
58
|
+
signed-in user
|
|
59
|
+
↓
|
|
60
|
+
303 /dashboard
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `redirectTo: null` to return `409 Already authenticated` instead of redirecting.
|
|
64
|
+
|
|
65
|
+
The low-level form is `requireGuest()`.
|
|
66
|
+
|
|
37
67
|
## Require a role
|
|
38
68
|
|
|
39
69
|
```ts
|
|
@@ -59,58 +89,87 @@ export const guard =
|
|
|
59
89
|
|
|
60
90
|
An authenticated user without the required role receives `403 Forbidden` by default.
|
|
61
91
|
|
|
62
|
-
|
|
92
|
+
Require one of several roles:
|
|
63
93
|
|
|
64
94
|
```ts
|
|
65
95
|
export const guard =
|
|
66
|
-
createRoleGuard<User>(
|
|
96
|
+
createRoleGuard<User>([
|
|
67
97
|
"admin",
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"/forbidden",
|
|
71
|
-
}
|
|
72
|
-
);
|
|
98
|
+
"manager",
|
|
99
|
+
]);
|
|
73
100
|
```
|
|
74
101
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
The default role matching mode is `any`:
|
|
102
|
+
Require all values from a role field:
|
|
78
103
|
|
|
79
104
|
```ts
|
|
80
105
|
export const guard =
|
|
81
|
-
createRoleGuard<User>(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
createRoleGuard<User>(
|
|
107
|
+
[
|
|
108
|
+
"admin",
|
|
109
|
+
"billing",
|
|
110
|
+
],
|
|
111
|
+
{
|
|
112
|
+
match: "all",
|
|
113
|
+
}
|
|
114
|
+
);
|
|
85
115
|
```
|
|
86
116
|
|
|
87
|
-
|
|
117
|
+
## Require permissions — 0.2.6+
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`requireRole()` and `createRoleGuard()` can read another user field and require every value:
|
|
119
|
+
Authorization & Security v2 adds a dedicated permission guard instead of requiring applications to overload role configuration.
|
|
92
120
|
|
|
93
121
|
```ts
|
|
122
|
+
import {
|
|
123
|
+
createPermissionGuard,
|
|
124
|
+
} from "bcp/auth";
|
|
125
|
+
|
|
94
126
|
interface User {
|
|
95
127
|
id: number;
|
|
96
128
|
permissions: string[];
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
export const guard =
|
|
100
|
-
|
|
132
|
+
createPermissionGuard<User>(
|
|
133
|
+
"users.read"
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The default permission field is `permissions`.
|
|
138
|
+
|
|
139
|
+
Require every permission:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export const guard =
|
|
143
|
+
createPermissionGuard<User>(
|
|
101
144
|
[
|
|
102
145
|
"users.read",
|
|
103
146
|
"users.write",
|
|
104
147
|
],
|
|
105
148
|
{
|
|
106
|
-
|
|
107
|
-
"permissions",
|
|
108
|
-
match:
|
|
109
|
-
"all",
|
|
149
|
+
match: "all",
|
|
110
150
|
}
|
|
111
151
|
);
|
|
112
152
|
```
|
|
113
153
|
|
|
154
|
+
Use another user field such as `scopes`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
export const guard =
|
|
158
|
+
createPermissionGuard<User>(
|
|
159
|
+
"billing.read",
|
|
160
|
+
{
|
|
161
|
+
permissionField:
|
|
162
|
+
"scopes",
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The low-level form is `requirePermission()`.
|
|
168
|
+
|
|
169
|
+
Authenticated users without the required permission receive `403 Forbidden` by default. Use `forbiddenRedirectTo` when the application prefers a redirect.
|
|
170
|
+
|
|
171
|
+
For authorization decisions that depend on a resource or ownership rather than a flat permission, use the policy APIs documented in [Authorization & Security v2](authorization-security.md).
|
|
172
|
+
|
|
114
173
|
## Inline guard logic
|
|
115
174
|
|
|
116
175
|
Use `requireAuth()` when a guard needs additional application-specific checks:
|
|
@@ -125,8 +184,7 @@ export async function guard() {
|
|
|
125
184
|
await requireAuth<AppUser>();
|
|
126
185
|
|
|
127
186
|
if (
|
|
128
|
-
result instanceof
|
|
129
|
-
Response
|
|
187
|
+
result instanceof Response
|
|
130
188
|
) {
|
|
131
189
|
return result;
|
|
132
190
|
}
|
|
@@ -137,8 +195,7 @@ export async function guard() {
|
|
|
137
195
|
return new Response(
|
|
138
196
|
"Forbidden",
|
|
139
197
|
{
|
|
140
|
-
status:
|
|
141
|
-
403,
|
|
198
|
+
status: 403,
|
|
142
199
|
}
|
|
143
200
|
);
|
|
144
201
|
}
|
|
@@ -147,6 +204,14 @@ export async function guard() {
|
|
|
147
204
|
}
|
|
148
205
|
```
|
|
149
206
|
|
|
207
|
+
## Session-store policies apply inside guards
|
|
208
|
+
|
|
209
|
+
`requireAuth()`, `requireRole()`, `requirePermission()`, and `requireGuest()` all use the same `auth()` runtime.
|
|
210
|
+
|
|
211
|
+
When auth options include a server-side store and idle timeout, guard evaluation observes revocation and inactivity before allowing the route.
|
|
212
|
+
|
|
213
|
+
For shared application configuration, create a small app auth module and expose guard helpers that use the same store instead of constructing unrelated stores inside each route file.
|
|
214
|
+
|
|
150
215
|
## Auth data in child guards and loaders
|
|
151
216
|
|
|
152
217
|
Successful auth helpers return guard data in this shape:
|
|
@@ -165,7 +230,7 @@ Successful auth helpers return guard data in this shape:
|
|
|
165
230
|
}
|
|
166
231
|
```
|
|
167
232
|
|
|
168
|
-
|
|
233
|
+
BCP merges parent guard data into child guards and page loaders, so descendants can reuse the authenticated session.
|
|
169
234
|
|
|
170
235
|
Use `getGuardAuth()` for typed access:
|
|
171
236
|
|
|
@@ -195,36 +260,14 @@ export async function loader({
|
|
|
195
260
|
}
|
|
196
261
|
```
|
|
197
262
|
|
|
198
|
-
## Low-level role checks
|
|
199
|
-
|
|
200
|
-
`requireRole()` is also available directly:
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
import {
|
|
204
|
-
requireRole,
|
|
205
|
-
} from "bcp/auth";
|
|
206
|
-
|
|
207
|
-
export function guard() {
|
|
208
|
-
return requireRole<AppUser>(
|
|
209
|
-
"admin"
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
263
|
## Standalone production behavior
|
|
215
264
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
This fixes the production-only failure:
|
|
265
|
+
`bcp/auth` stays inside the production guard/runtime request-context graph. `requireAuth()`, `requireGuest()`, `requireRole()`, `requirePermission()`, `auth()`, `getSession()`, and `cookies()` therefore observe the active request after `bcp build`.
|
|
219
266
|
|
|
220
|
-
|
|
221
|
-
BCP Framework: server request APIs can only be used while handling a request.
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
No application guard changes are required. Continue importing authentication helpers from `bcp/auth` normally.
|
|
225
|
-
|
|
226
|
-
The production form-action bundle applies the same runtime unification because route guards may execute before an action.
|
|
267
|
+
The production form-action bundle uses the same runtime boundary because guards may execute before an action.
|
|
227
268
|
|
|
228
269
|
## Security boundary
|
|
229
270
|
|
|
230
|
-
`bcp/auth` is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard.
|
|
271
|
+
`bcp/auth` is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard.
|
|
272
|
+
|
|
273
|
+
Authentication and authorization checks should stay in guards, loaders, actions, API routes, or other server modules.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Auth Session Stores
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.2.5 — Authentication Platform v2` adds an optional server-side session-store contract to `bcp/auth`.
|
|
4
|
+
|
|
5
|
+
The existing signed JWT cookie mode remains the default. Applications that do not configure a store continue to use stateless authentication exactly as before.
|
|
6
|
+
|
|
7
|
+
A session store is useful when the application needs server-side revocation, logout across devices, idle expiration, or explicit session administration.
|
|
8
|
+
|
|
9
|
+
## Why use a store?
|
|
10
|
+
|
|
11
|
+
A signed JWT proves that the cookie was issued by the application and has not been modified. By itself, however, a JWT remains valid until it expires.
|
|
12
|
+
|
|
13
|
+
With an auth session store enabled, BCP checks both:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
signed JWT cookie
|
|
17
|
+
+
|
|
18
|
+
server-side sid record
|
|
19
|
+
↓
|
|
20
|
+
active authenticated session
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This allows an application to invalidate a session before the JWT's normal expiry.
|
|
24
|
+
|
|
25
|
+
## Memory store
|
|
26
|
+
|
|
27
|
+
BCP includes a small in-memory implementation for local development, tests, prototypes, and single-process experiments:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
createAuth,
|
|
32
|
+
createMemoryAuthSessionStore,
|
|
33
|
+
} from "bcp/auth";
|
|
34
|
+
|
|
35
|
+
const sessionStore =
|
|
36
|
+
createMemoryAuthSessionStore();
|
|
37
|
+
|
|
38
|
+
export const appAuth =
|
|
39
|
+
createAuth({
|
|
40
|
+
store:
|
|
41
|
+
sessionStore,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The memory store is process-local and is cleared when the Node.js process restarts. It is not a shared production session database.
|
|
46
|
+
|
|
47
|
+
For multiple processes, containers, or servers, implement `AuthSessionStore` using a shared service such as Redis or a database.
|
|
48
|
+
|
|
49
|
+
## Store contract
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import type {
|
|
53
|
+
AuthSessionStore,
|
|
54
|
+
AuthSessionStoreRecord,
|
|
55
|
+
} from "bcp/auth";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The contract is:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface AuthSessionStore {
|
|
62
|
+
set(
|
|
63
|
+
record: AuthSessionStoreRecord
|
|
64
|
+
): Promise<void> | void;
|
|
65
|
+
|
|
66
|
+
get(
|
|
67
|
+
sid: string
|
|
68
|
+
):
|
|
69
|
+
| Promise<AuthSessionStoreRecord | null>
|
|
70
|
+
| AuthSessionStoreRecord
|
|
71
|
+
| null;
|
|
72
|
+
|
|
73
|
+
touch(
|
|
74
|
+
sid: string,
|
|
75
|
+
lastSeenAt: number
|
|
76
|
+
): Promise<void> | void;
|
|
77
|
+
|
|
78
|
+
revoke(
|
|
79
|
+
sid: string,
|
|
80
|
+
revokedAt?: number
|
|
81
|
+
): Promise<boolean> | boolean;
|
|
82
|
+
|
|
83
|
+
revokeUser(
|
|
84
|
+
userId: string,
|
|
85
|
+
revokedAt?: number
|
|
86
|
+
): Promise<number> | number;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Store records contain only session-control metadata:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
interface AuthSessionStoreRecord {
|
|
94
|
+
sid: string;
|
|
95
|
+
userId: string;
|
|
96
|
+
createdAt: number;
|
|
97
|
+
expiresAt: number;
|
|
98
|
+
lastSeenAt: number;
|
|
99
|
+
revokedAt?: number | null;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`userId` is an opaque normalized identifier generated by BCP. Application stores should preserve it exactly as received.
|
|
104
|
+
|
|
105
|
+
## Logout and revocation
|
|
106
|
+
|
|
107
|
+
Normal logout revokes the active server-side `sid` before expiring the browser cookie when a store is configured:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
await appAuth.logout();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Revoke a known session:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await appAuth.revokeSession(
|
|
117
|
+
sessionId
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Revoke all stored sessions belonging to a user:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
await appAuth.revokeUserSessions(
|
|
125
|
+
userId
|
|
126
|
+
);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Logout the current browser and revoke all stored sessions for that user:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const revokedCount =
|
|
133
|
+
await appAuth.logoutAll();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`logoutAll()` requires a session store. Stateless-only authentication cannot invalidate JWT cookies held by other devices before those tokens expire.
|
|
137
|
+
|
|
138
|
+
## Idle timeout
|
|
139
|
+
|
|
140
|
+
An optional idle timeout can be applied when a session store is enabled:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const appAuth =
|
|
144
|
+
createAuth({
|
|
145
|
+
store:
|
|
146
|
+
sessionStore,
|
|
147
|
+
idleTimeout:
|
|
148
|
+
60 * 30,
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The value is measured in seconds.
|
|
153
|
+
|
|
154
|
+
Every successful `auth()` check updates `lastSeenAt`. If the stored session has been inactive for at least the configured interval, BCP revokes it and authentication returns `null`.
|
|
155
|
+
|
|
156
|
+
Because idle timeout depends on server-side state, configuring `idleTimeout` without a store is rejected instead of silently pretending to enforce the policy.
|
|
157
|
+
|
|
158
|
+
## Session rotation
|
|
159
|
+
|
|
160
|
+
`rotateSession()` issues a new `sid` while keeping the current user and session data:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const nextSession =
|
|
164
|
+
await appAuth.rotateSession();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
When a store is configured, the previous `sid` is revoked as part of rotation so an older cookie cannot continue to authenticate through the server-side session check.
|
|
168
|
+
|
|
169
|
+
Typical rotation points include:
|
|
170
|
+
|
|
171
|
+
- successful re-authentication,
|
|
172
|
+
- password changes,
|
|
173
|
+
- role or permission elevation,
|
|
174
|
+
- sensitive account changes.
|
|
175
|
+
|
|
176
|
+
## Production implementation guidance
|
|
177
|
+
|
|
178
|
+
A production store should provide atomic, durable operations for `set`, `get`, `touch`, `revoke`, and `revokeUser`.
|
|
179
|
+
|
|
180
|
+
Recommended properties:
|
|
181
|
+
|
|
182
|
+
- shared across all application instances,
|
|
183
|
+
- expiration based on `expiresAt`,
|
|
184
|
+
- indexed lookup by `sid`,
|
|
185
|
+
- indexed lookup by normalized `userId`,
|
|
186
|
+
- atomic revocation,
|
|
187
|
+
- bounded cleanup of expired records,
|
|
188
|
+
- no storage of passwords or raw session secrets.
|
|
189
|
+
|
|
190
|
+
The JWT remains HttpOnly and signed by `BCP_SESSION_SECRET`; the session store is an additional revocation/lifecycle layer, not a replacement for cookie security.
|
|
191
|
+
|
|
192
|
+
## Backward compatibility
|
|
193
|
+
|
|
194
|
+
This remains valid in `0.2.5`:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import {
|
|
198
|
+
auth,
|
|
199
|
+
login,
|
|
200
|
+
logout,
|
|
201
|
+
} from "bcp/auth";
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Without `store`, BCP continues operating in stateless JWT-cookie mode.
|