@chidchanun/bcp 0.2.4 → 0.2.5
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 +76 -46
- package/docs/README.md +77 -126
- package/docs/api-manifest.json +3 -2
- package/docs/api-reference.md +22 -2
- package/docs/auth-route-guards.md +72 -37
- package/docs/auth-session-store.md +204 -0
- package/docs/authentication.md +128 -44
- package/docs/docs-web-manifest.json +5 -3
- package/docs/platform-manifest.json +12 -4
- package/docs/releases/0.2.5.md +152 -0
- package/package.json +1 -1
- package/packages/client/src/auth.ts +16 -0
- package/packages/server/src/auth-guard.ts +95 -82
- package/packages/server/src/auth-session-store.ts +318 -0
- package/packages/server/src/auth.ts +443 -50
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auth Route Guards
|
|
2
2
|
|
|
3
|
-
BCP Framework
|
|
3
|
+
BCP Framework integrates Authentication Platform v2 with route guards through the server-only `bcp/auth` entrypoint.
|
|
4
4
|
|
|
5
5
|
## Protect a route tree
|
|
6
6
|
|
|
@@ -18,22 +18,73 @@ 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({
|
|
46
|
+
redirectTo:
|
|
47
|
+
"/dashboard",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
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
|
+
```ts
|
|
66
|
+
export const guard =
|
|
67
|
+
createGuestGuard({
|
|
32
68
|
redirectTo:
|
|
33
69
|
null,
|
|
34
70
|
});
|
|
35
71
|
```
|
|
36
72
|
|
|
73
|
+
The low-level form is `requireGuest()`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {
|
|
77
|
+
requireGuest,
|
|
78
|
+
} from "bcp/auth";
|
|
79
|
+
|
|
80
|
+
export function guard() {
|
|
81
|
+
return requireGuest({
|
|
82
|
+
redirectTo:
|
|
83
|
+
"/dashboard",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
37
88
|
## Require a role
|
|
38
89
|
|
|
39
90
|
```ts
|
|
@@ -125,8 +176,7 @@ export async function guard() {
|
|
|
125
176
|
await requireAuth<AppUser>();
|
|
126
177
|
|
|
127
178
|
if (
|
|
128
|
-
result instanceof
|
|
129
|
-
Response
|
|
179
|
+
result instanceof Response
|
|
130
180
|
) {
|
|
131
181
|
return result;
|
|
132
182
|
}
|
|
@@ -137,8 +187,7 @@ export async function guard() {
|
|
|
137
187
|
return new Response(
|
|
138
188
|
"Forbidden",
|
|
139
189
|
{
|
|
140
|
-
status:
|
|
141
|
-
403,
|
|
190
|
+
status: 403,
|
|
142
191
|
}
|
|
143
192
|
);
|
|
144
193
|
}
|
|
@@ -147,6 +196,14 @@ export async function guard() {
|
|
|
147
196
|
}
|
|
148
197
|
```
|
|
149
198
|
|
|
199
|
+
## Session-store policies apply inside guards
|
|
200
|
+
|
|
201
|
+
`requireAuth()`, `requireRole()`, and `requireGuest()` all use the same `auth()` runtime.
|
|
202
|
+
|
|
203
|
+
When auth options include a server-side store and idle timeout, guard evaluation observes revocation and inactivity before allowing the route.
|
|
204
|
+
|
|
205
|
+
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.
|
|
206
|
+
|
|
150
207
|
## Auth data in child guards and loaders
|
|
151
208
|
|
|
152
209
|
Successful auth helpers return guard data in this shape:
|
|
@@ -165,7 +222,7 @@ Successful auth helpers return guard data in this shape:
|
|
|
165
222
|
}
|
|
166
223
|
```
|
|
167
224
|
|
|
168
|
-
|
|
225
|
+
BCP merges parent guard data into child guards and page loaders, so descendants can reuse the authenticated session.
|
|
169
226
|
|
|
170
227
|
Use `getGuardAuth()` for typed access:
|
|
171
228
|
|
|
@@ -195,36 +252,14 @@ export async function loader({
|
|
|
195
252
|
}
|
|
196
253
|
```
|
|
197
254
|
|
|
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
255
|
## Standalone production behavior
|
|
215
256
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
This fixes the production-only failure:
|
|
257
|
+
`bcp/auth` stays inside the production guard/runtime request-context graph. `requireAuth()`, `requireGuest()`, `requireRole()`, `auth()`, `getSession()`, and `cookies()` therefore observe the active request after `bcp build`.
|
|
219
258
|
|
|
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.
|
|
259
|
+
The production form-action bundle uses the same runtime boundary because guards may execute before an action.
|
|
227
260
|
|
|
228
261
|
## Security boundary
|
|
229
262
|
|
|
230
|
-
`bcp/auth` is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard.
|
|
263
|
+
`bcp/auth` is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard.
|
|
264
|
+
|
|
265
|
+
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.
|
package/docs/authentication.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Authentication
|
|
2
2
|
|
|
3
|
-
BCP Framework 0.
|
|
4
|
-
It builds on the signed JWT cookie/session primitives from `bcp/server` and provides a higher-level API for application authentication.
|
|
3
|
+
BCP Framework `0.2.5 — Authentication Platform v2` extends the server-only `bcp/auth` entrypoint with optional server-side session state, revocation, idle timeout, logout-all, and guest route guards while preserving the existing signed JWT-cookie mode.
|
|
5
4
|
|
|
6
5
|
## Environment
|
|
7
6
|
|
|
@@ -52,13 +51,9 @@ const session =
|
|
|
52
51
|
if (!session) {
|
|
53
52
|
// Not authenticated.
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
57
|
-
session?.user.id
|
|
58
|
-
);
|
|
59
54
|
```
|
|
60
55
|
|
|
61
|
-
`getSession()`
|
|
56
|
+
`getSession()` remains an alias for `auth()`.
|
|
62
57
|
|
|
63
58
|
## Typed auth factory
|
|
64
59
|
|
|
@@ -113,56 +108,150 @@ await appAuth.login(
|
|
|
113
108
|
);
|
|
114
109
|
```
|
|
115
110
|
|
|
116
|
-
|
|
111
|
+
## Stateless mode remains the default
|
|
112
|
+
|
|
113
|
+
Without a server-side store, authentication remains a signed JWT cookie:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
browser cookie
|
|
117
|
+
↓
|
|
118
|
+
HS256 signature + expiry validation
|
|
119
|
+
↓
|
|
120
|
+
authenticated session
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is backward-compatible with earlier BCP versions.
|
|
124
|
+
|
|
125
|
+
A stateless JWT cannot normally be invalidated on another device before expiry. Applications that need revocation can enable the `0.2.5` session-store contract.
|
|
126
|
+
|
|
127
|
+
## Revocable sessions
|
|
117
128
|
|
|
118
129
|
```ts
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
import {
|
|
131
|
+
createAuth,
|
|
132
|
+
createMemoryAuthSessionStore,
|
|
133
|
+
} from "bcp/auth";
|
|
134
|
+
|
|
135
|
+
const store =
|
|
136
|
+
createMemoryAuthSessionStore();
|
|
137
|
+
|
|
138
|
+
export const appAuth =
|
|
139
|
+
createAuth<AppUser>({
|
|
140
|
+
store,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
With a store configured, authentication requires both a valid signed cookie and an active server-side `sid` record.
|
|
145
|
+
|
|
146
|
+
The built-in memory store is intended for development and tests. Multi-instance production applications should implement `AuthSessionStore` using a shared database or Redis-like service.
|
|
147
|
+
|
|
148
|
+
See [Auth Session Stores](auth-session-store.md).
|
|
149
|
+
|
|
150
|
+
## Logout
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
await appAuth.logout();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
In stateless mode this expires the browser cookie.
|
|
157
|
+
|
|
158
|
+
When a store is configured, BCP also revokes the current `sid` before expiring the cookie.
|
|
159
|
+
|
|
160
|
+
## Logout from all sessions
|
|
161
|
+
|
|
162
|
+
With a session store:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const revokedCount =
|
|
166
|
+
await appAuth.logoutAll();
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This revokes every stored session for the current user and expires the current browser cookie.
|
|
170
|
+
|
|
171
|
+
`logoutAll()` intentionally requires a server-side store because stateless JWTs held by other devices cannot be centrally invalidated.
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
## Explicit revocation
|
|
174
|
+
|
|
175
|
+
A factory configured with a store can revoke a known session:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
await appAuth.revokeSession(
|
|
179
|
+
sid
|
|
124
180
|
);
|
|
125
|
-
|
|
126
|
-
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or revoke every stored session for a user:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
await appAuth.revokeUserSessions(
|
|
187
|
+
userId
|
|
127
188
|
);
|
|
128
189
|
```
|
|
129
190
|
|
|
130
|
-
|
|
191
|
+
Low-level forms are also exported:
|
|
131
192
|
|
|
132
193
|
```ts
|
|
133
|
-
|
|
194
|
+
import {
|
|
195
|
+
revokeSession,
|
|
196
|
+
revokeUserSessions,
|
|
197
|
+
} from "bcp/auth";
|
|
134
198
|
```
|
|
135
199
|
|
|
136
|
-
|
|
200
|
+
## Idle timeout
|
|
201
|
+
|
|
202
|
+
Server-side session state can enforce inactivity expiration:
|
|
137
203
|
|
|
138
204
|
```ts
|
|
139
|
-
|
|
205
|
+
const appAuth =
|
|
206
|
+
createAuth<AppUser>({
|
|
207
|
+
store,
|
|
208
|
+
idleTimeout:
|
|
209
|
+
60 * 30,
|
|
210
|
+
});
|
|
140
211
|
```
|
|
141
212
|
|
|
142
|
-
|
|
213
|
+
`idleTimeout` is measured in seconds. Each successful `auth()` check updates `lastSeenAt`.
|
|
214
|
+
|
|
215
|
+
If the session has been inactive for at least the configured duration, BCP revokes it and returns `null`.
|
|
216
|
+
|
|
217
|
+
BCP rejects `idleTimeout` when no store is configured so an application cannot accidentally believe it is enforcing server-side inactivity policy when it is not.
|
|
143
218
|
|
|
144
219
|
## Session rotation
|
|
145
220
|
|
|
146
|
-
Each
|
|
221
|
+
Each login receives a unique `sid`. `rotateSession()` keeps the current user/session data but issues a new session identifier, JWT, expiry window, and cookie.
|
|
147
222
|
|
|
148
223
|
```ts
|
|
149
224
|
const rotated =
|
|
150
225
|
await appAuth.rotateSession();
|
|
226
|
+
```
|
|
151
227
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
228
|
+
When a session store is configured, the previous `sid` is revoked as part of rotation.
|
|
229
|
+
|
|
230
|
+
Good rotation points include successful re-authentication, password changes, permission elevation, or other security-sensitive account changes.
|
|
231
|
+
|
|
232
|
+
## Guest-only pages
|
|
233
|
+
|
|
234
|
+
Authentication Platform v2 adds `requireGuest()` / `createGuestGuard()` for routes such as login and registration pages.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import {
|
|
238
|
+
createGuestGuard,
|
|
239
|
+
} from "bcp/auth";
|
|
240
|
+
|
|
241
|
+
export const guard =
|
|
242
|
+
createGuestGuard({
|
|
243
|
+
redirectTo:
|
|
244
|
+
"/dashboard",
|
|
245
|
+
});
|
|
157
246
|
```
|
|
158
247
|
|
|
159
|
-
|
|
248
|
+
Anonymous users continue to the page. Authenticated users are redirected with `303` by default.
|
|
160
249
|
|
|
161
|
-
|
|
250
|
+
See [Auth Route Guards](auth-route-guards.md).
|
|
162
251
|
|
|
163
252
|
## Session shape
|
|
164
253
|
|
|
165
|
-
An authenticated session contains the user, optional application
|
|
254
|
+
An authenticated session contains the user, optional application data, and signed JWT claims:
|
|
166
255
|
|
|
167
256
|
```ts
|
|
168
257
|
{
|
|
@@ -176,26 +265,21 @@ An authenticated session contains the user, optional application session data, a
|
|
|
176
265
|
}
|
|
177
266
|
```
|
|
178
267
|
|
|
268
|
+
Server-side session-store metadata is not embedded as application payload data.
|
|
269
|
+
|
|
179
270
|
## Security notes
|
|
180
271
|
|
|
181
|
-
- `bcp/auth` is server-only and must not be imported into
|
|
182
|
-
-
|
|
183
|
-
- Keep `BCP_SESSION_SECRET`
|
|
184
|
-
-
|
|
185
|
-
-
|
|
272
|
+
- `bcp/auth` is server-only and must not be imported into browser/client graphs.
|
|
273
|
+
- JWT cookie payloads are signed, not encrypted. Do not store passwords, password hashes, API secrets, access keys, or private credentials in the auth payload.
|
|
274
|
+
- Keep `BCP_SESSION_SECRET` outside source control and use a strong random value of at least 32 bytes.
|
|
275
|
+
- Use HTTPS in production so Secure cookies travel only over encrypted connections.
|
|
276
|
+
- The memory session store is process-local; use a shared durable store when revocation must work across multiple processes/containers.
|
|
277
|
+
- Authentication establishes identity/session state. Fine-grained authorization remains a route guard, action, API, or application policy concern.
|
|
186
278
|
|
|
187
279
|
## create-bcp-app
|
|
188
280
|
|
|
189
|
-
When `JWT Cookie` authentication is selected, generated applications use `createAuth()` internally and expose helpers from `lib/auth.ts
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
import {
|
|
193
|
-
auth,
|
|
194
|
-
getSession,
|
|
195
|
-
login,
|
|
196
|
-
logout,
|
|
197
|
-
rotateSession,
|
|
198
|
-
} from "@/lib/auth";
|
|
199
|
-
```
|
|
281
|
+
When `JWT Cookie` authentication is selected, generated applications use `createAuth()` internally and expose application helpers from `lib/auth.ts`.
|
|
200
282
|
|
|
201
283
|
The generated `authenticateCredentials()` intentionally returns `null` until the application implements its own user lookup and password verification strategy.
|
|
284
|
+
|
|
285
|
+
Generated projects stay stateless by default. Applications can opt into a session store explicitly without changing the public auth entrypoint.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"versionTarget": "0.2.
|
|
4
|
+
"versionTarget": "0.2.5",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
6
|
"sections": [
|
|
7
7
|
{
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
{
|
|
37
37
|
"id": "authentication",
|
|
38
38
|
"title": "Authentication",
|
|
39
|
-
"description": "Authentication
|
|
39
|
+
"description": "Authentication Platform v2, revocable session stores, guest/auth route guards and JWT cookie sessions.",
|
|
40
40
|
"pages": [
|
|
41
41
|
{ "route": "/docs/authentication", "source": "authentication.md", "title": "Authentication" },
|
|
42
|
+
{ "route": "/docs/auth-session-store", "source": "auth-session-store.md", "title": "Auth Session Stores" },
|
|
42
43
|
{ "route": "/docs/auth-route-guards", "source": "auth-route-guards.md", "title": "Auth Route Guards" },
|
|
43
44
|
{ "route": "/docs/session-auth", "source": "session-auth.md", "title": "JWT Sessions" }
|
|
44
45
|
]
|
|
@@ -105,7 +106,8 @@
|
|
|
105
106
|
}
|
|
106
107
|
],
|
|
107
108
|
"releases": [
|
|
108
|
-
{ "route": "/releases/0.2.
|
|
109
|
+
{ "route": "/releases/0.2.5", "source": "releases/0.2.5.md", "version": "0.2.5", "state": "unreleased" },
|
|
110
|
+
{ "route": "/releases/0.2.4", "source": "releases/0.2.4.md", "version": "0.2.4" },
|
|
109
111
|
{ "route": "/releases/0.2.3", "source": "releases/0.2.3.md", "version": "0.2.3" },
|
|
110
112
|
{ "route": "/releases/0.2.2", "source": "releases/0.2.2.md", "version": "0.2.2" },
|
|
111
113
|
{ "route": "/releases/0.2.1", "source": "releases/0.2.1.md", "version": "0.2.1" },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
|
-
"baseline": "
|
|
6
|
+
"baseline": "authentication-platform-v2",
|
|
7
7
|
"runtime": {
|
|
8
8
|
"node": ">=24.11.0",
|
|
9
9
|
"react": "19",
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
"formActions": true,
|
|
48
48
|
"middlewareV2": true,
|
|
49
49
|
"jwtCookieSessions": true,
|
|
50
|
+
"authenticationPlatformV2": true,
|
|
51
|
+
"authSessionStore": true,
|
|
52
|
+
"authSessionRevocation": true,
|
|
53
|
+
"authLogoutAll": true,
|
|
54
|
+
"authIdleTimeout": true,
|
|
55
|
+
"authGuestGuard": true,
|
|
50
56
|
"databaseMigrations": true,
|
|
51
57
|
"databaseAdapterContract": true,
|
|
52
58
|
"databasePostgresql": true,
|
|
@@ -86,7 +92,7 @@
|
|
|
86
92
|
"s3-compatible"
|
|
87
93
|
],
|
|
88
94
|
"compatibility": {
|
|
89
|
-
"previousBaseline": "0.2.
|
|
95
|
+
"previousBaseline": "0.2.4",
|
|
90
96
|
"intentionalBreakingChangesFromPreviousBaseline": false,
|
|
91
97
|
"migrationGuide": "migration-0.2.md"
|
|
92
98
|
},
|
|
@@ -99,7 +105,9 @@
|
|
|
99
105
|
"apiReference": "api-reference.md",
|
|
100
106
|
"environmentValidation": "environment-validation.md",
|
|
101
107
|
"applicationPackaging": "application-packaging.md",
|
|
108
|
+
"authentication": "authentication.md",
|
|
109
|
+
"authSessionStore": "auth-session-store.md",
|
|
102
110
|
"migrationGuide": "migration-0.2.md",
|
|
103
|
-
"releaseNotes": "releases/0.2.
|
|
111
|
+
"releaseNotes": "releases/0.2.5.md"
|
|
104
112
|
}
|
|
105
113
|
}
|