@reckona/mreact-auth 0.0.66 → 0.0.67
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/package.json +5 -4
- package/src/index.ts +426 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reckona/mreact-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"description": "Session and authorization helpers for mreact app router applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"dist/**/*.js",
|
|
26
26
|
"dist/**/*.js.map",
|
|
27
27
|
"dist/**/*.d.ts",
|
|
28
|
-
"dist/**/*.d.ts.map"
|
|
28
|
+
"dist/**/*.d.ts.map",
|
|
29
|
+
"src/**/*"
|
|
29
30
|
],
|
|
30
31
|
"type": "module",
|
|
31
32
|
"sideEffects": false,
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"access": "public"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@reckona/mreact-reactive-core": "0.0.
|
|
44
|
-
"@reckona/mreact-router": "0.0.
|
|
44
|
+
"@reckona/mreact-reactive-core": "0.0.67",
|
|
45
|
+
"@reckona/mreact-router": "0.0.67"
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMemorySessionStore,
|
|
3
|
+
createSession,
|
|
4
|
+
destroySession as destroyRouterSession,
|
|
5
|
+
getSession,
|
|
6
|
+
rotateSession as rotateRouterSession,
|
|
7
|
+
type SessionCookieOptions,
|
|
8
|
+
type SessionRecord,
|
|
9
|
+
type SessionStore,
|
|
10
|
+
} from "@reckona/mreact-router/session";
|
|
11
|
+
import { getGlobalRuntimeState } from "@reckona/mreact-reactive-core/runtime-state";
|
|
12
|
+
import { redirect } from "@reckona/mreact-router";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
createMemorySessionStore,
|
|
16
|
+
createSession,
|
|
17
|
+
destroyRouterSession as destroySession,
|
|
18
|
+
getSession,
|
|
19
|
+
rotateRouterSession as rotateSession,
|
|
20
|
+
};
|
|
21
|
+
export type { SessionCookieOptions, SessionRecord, SessionStore };
|
|
22
|
+
|
|
23
|
+
export const __MREACT_AUTH_SESSION_SCRIPT_ID = "__mreact_auth_session";
|
|
24
|
+
|
|
25
|
+
export interface AuthSessionClaims {
|
|
26
|
+
[claim: string]: unknown;
|
|
27
|
+
permissions?: readonly string[] | undefined;
|
|
28
|
+
roles?: readonly string[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuthGuardOptions {
|
|
32
|
+
forbiddenTo?: string | undefined;
|
|
33
|
+
mode?: AuthRequirementMode | undefined;
|
|
34
|
+
redirectTo?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AuthConfig {
|
|
38
|
+
forbiddenTo?: string | undefined;
|
|
39
|
+
redirectTo?: string | undefined;
|
|
40
|
+
serializeClaims?: AuthClaimsSerializer | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ResolvedAuthConfig {
|
|
44
|
+
forbiddenTo: string;
|
|
45
|
+
redirectTo: string;
|
|
46
|
+
serializeClaims: AuthClaimsSerializer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type AuthRequirement = string | readonly string[];
|
|
50
|
+
export type AuthRequirementMode = "all" | "any";
|
|
51
|
+
export type AuthClaimsSerializer = (data: unknown) => AuthSessionClaims | undefined;
|
|
52
|
+
|
|
53
|
+
export interface AuthorizationPolicy {
|
|
54
|
+
permissions?: readonly string[] | undefined;
|
|
55
|
+
roles?: readonly string[] | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AuthorizationResult =
|
|
59
|
+
| {
|
|
60
|
+
authorized: true;
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
authorized: false;
|
|
64
|
+
reason: "missing-permission" | "missing-role";
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type TryAuthResult<TData> =
|
|
68
|
+
| {
|
|
69
|
+
authorized: true;
|
|
70
|
+
session: SessionRecord<TData>;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
authorized: false;
|
|
74
|
+
reason: "missing-permission" | "missing-role" | "missing-session";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const authRuntimeStateKey = "__mreactAuthRuntimeState";
|
|
78
|
+
|
|
79
|
+
interface AuthRuntimeRequestState {
|
|
80
|
+
claims?: AuthSessionClaims | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface AuthRuntimeState {
|
|
84
|
+
browserClaims?: AuthSessionClaims | undefined;
|
|
85
|
+
currentClaims?: AuthSessionClaims | undefined;
|
|
86
|
+
storage?:
|
|
87
|
+
| {
|
|
88
|
+
getStore(): AuthRuntimeRequestState | undefined;
|
|
89
|
+
}
|
|
90
|
+
| undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let authConfig: ResolvedAuthConfig = {
|
|
94
|
+
forbiddenTo: "/forbidden",
|
|
95
|
+
redirectTo: "/login",
|
|
96
|
+
serializeClaims: defaultSerializeSessionClaims,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function configureAuth(config: AuthConfig): void {
|
|
100
|
+
authConfig = {
|
|
101
|
+
forbiddenTo: config.forbiddenTo ?? authConfig.forbiddenTo,
|
|
102
|
+
redirectTo: config.redirectTo ?? authConfig.redirectTo,
|
|
103
|
+
serializeClaims: config.serializeClaims ?? authConfig.serializeClaims,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getCurrentSession<TData>(
|
|
108
|
+
request: Request,
|
|
109
|
+
store: SessionStore<TData>,
|
|
110
|
+
options: SessionCookieOptions = {},
|
|
111
|
+
): Promise<SessionRecord<TData> | undefined> {
|
|
112
|
+
const session = await getSession(request, store, options);
|
|
113
|
+
|
|
114
|
+
setSessionClaims(session?.data);
|
|
115
|
+
|
|
116
|
+
return session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function requireSession<TData>(
|
|
120
|
+
request: Request,
|
|
121
|
+
store: SessionStore<TData>,
|
|
122
|
+
options: AuthGuardOptions = {},
|
|
123
|
+
): Promise<SessionRecord<TData>> {
|
|
124
|
+
const session = await getCurrentSession(request, store);
|
|
125
|
+
|
|
126
|
+
if (session === undefined) {
|
|
127
|
+
redirect(authRedirectTo(options), { status: 303 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return session;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function requireRole<TData extends AuthSessionClaims>(
|
|
134
|
+
request: Request,
|
|
135
|
+
store: SessionStore<TData>,
|
|
136
|
+
role: AuthRequirement,
|
|
137
|
+
options: AuthGuardOptions = {},
|
|
138
|
+
): Promise<SessionRecord<TData>> {
|
|
139
|
+
const session = await requireSession(request, store, options);
|
|
140
|
+
const result = authorizeRequirement(session.data.roles, role, "missing-role", options.mode);
|
|
141
|
+
|
|
142
|
+
if (!result.authorized) {
|
|
143
|
+
redirect(authForbiddenTo(options), { status: 303 });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return session;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function requirePermission<TData extends AuthSessionClaims>(
|
|
150
|
+
request: Request,
|
|
151
|
+
store: SessionStore<TData>,
|
|
152
|
+
permission: AuthRequirement,
|
|
153
|
+
options: AuthGuardOptions = {},
|
|
154
|
+
): Promise<SessionRecord<TData>> {
|
|
155
|
+
const session = await requireSession(request, store, options);
|
|
156
|
+
const result = authorizeRequirement(
|
|
157
|
+
session.data.permissions,
|
|
158
|
+
permission,
|
|
159
|
+
"missing-permission",
|
|
160
|
+
options.mode,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (!result.authorized) {
|
|
164
|
+
redirect(authForbiddenTo(options), { status: 303 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function tryRequireRole<TData extends AuthSessionClaims>(
|
|
171
|
+
request: Request,
|
|
172
|
+
store: SessionStore<TData>,
|
|
173
|
+
role: AuthRequirement,
|
|
174
|
+
options: Pick<AuthGuardOptions, "mode"> = {},
|
|
175
|
+
): Promise<TryAuthResult<TData>> {
|
|
176
|
+
const session = await getCurrentSession(request, store);
|
|
177
|
+
|
|
178
|
+
if (session === undefined) {
|
|
179
|
+
return { authorized: false, reason: "missing-session" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = authorizeRequirement(session.data.roles, role, "missing-role", options.mode);
|
|
183
|
+
|
|
184
|
+
return result.authorized ? { authorized: true, session } : result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function tryRequirePermission<TData extends AuthSessionClaims>(
|
|
188
|
+
request: Request,
|
|
189
|
+
store: SessionStore<TData>,
|
|
190
|
+
permission: AuthRequirement,
|
|
191
|
+
options: Pick<AuthGuardOptions, "mode"> = {},
|
|
192
|
+
): Promise<TryAuthResult<TData>> {
|
|
193
|
+
const session = await getCurrentSession(request, store);
|
|
194
|
+
|
|
195
|
+
if (session === undefined) {
|
|
196
|
+
return { authorized: false, reason: "missing-session" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = authorizeRequirement(
|
|
200
|
+
session.data.permissions,
|
|
201
|
+
permission,
|
|
202
|
+
"missing-permission",
|
|
203
|
+
options.mode,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return result.authorized ? { authorized: true, session } : result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function authorizeSession<TData extends AuthSessionClaims>(
|
|
210
|
+
data: TData,
|
|
211
|
+
policy: AuthorizationPolicy,
|
|
212
|
+
): AuthorizationResult {
|
|
213
|
+
if (!hasAll(data.roles, policy.roles)) {
|
|
214
|
+
return {
|
|
215
|
+
authorized: false,
|
|
216
|
+
reason: "missing-role",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!hasAll(data.permissions, policy.permissions)) {
|
|
221
|
+
return {
|
|
222
|
+
authorized: false,
|
|
223
|
+
reason: "missing-permission",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { authorized: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function refreshSession<TData>(
|
|
231
|
+
request: Request,
|
|
232
|
+
response: Response,
|
|
233
|
+
store: SessionStore<TData>,
|
|
234
|
+
options: SessionCookieOptions = {},
|
|
235
|
+
): Promise<SessionRecord<TData> | undefined> {
|
|
236
|
+
const session = await rotateRouterSession(request, response, store, options);
|
|
237
|
+
|
|
238
|
+
setSessionClaims(session?.data);
|
|
239
|
+
|
|
240
|
+
return session;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function revokeCurrentSession<TData>(
|
|
244
|
+
request: Request,
|
|
245
|
+
response: Response,
|
|
246
|
+
store: SessionStore<TData>,
|
|
247
|
+
options: SessionCookieOptions = {},
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
await destroyRouterSession(request, response, store, options);
|
|
250
|
+
setSessionClaims(undefined);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function getSessionClaims<TData extends AuthSessionClaims = AuthSessionClaims>():
|
|
254
|
+
| TData
|
|
255
|
+
| undefined {
|
|
256
|
+
const state = authRuntimeState();
|
|
257
|
+
const requestClaims = state.storage?.getStore()?.claims;
|
|
258
|
+
|
|
259
|
+
if (requestClaims !== undefined) {
|
|
260
|
+
return requestClaims as TData;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (typeof document === "undefined") {
|
|
264
|
+
return state.currentClaims as TData | undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (state.browserClaims === undefined) {
|
|
268
|
+
state.browserClaims = readClaimsFromDocument();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return state.browserClaims as TData | undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function __resetAuthForTesting(): void {
|
|
275
|
+
authConfig = {
|
|
276
|
+
forbiddenTo: "/forbidden",
|
|
277
|
+
redirectTo: "/login",
|
|
278
|
+
serializeClaims: defaultSerializeSessionClaims,
|
|
279
|
+
};
|
|
280
|
+
const state = authRuntimeState();
|
|
281
|
+
state.browserClaims = undefined;
|
|
282
|
+
state.currentClaims = undefined;
|
|
283
|
+
const requestState = state.storage?.getStore();
|
|
284
|
+
if (requestState !== undefined) {
|
|
285
|
+
requestState.claims = undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function authorizeRequirement(
|
|
290
|
+
available: readonly string[] | undefined,
|
|
291
|
+
requirement: AuthRequirement,
|
|
292
|
+
reason: "missing-permission" | "missing-role",
|
|
293
|
+
mode: AuthRequirementMode = "any",
|
|
294
|
+
): AuthorizationResult {
|
|
295
|
+
const required = Array.isArray(requirement) ? requirement : [requirement];
|
|
296
|
+
const authorized = mode === "all" ? hasAll(available, required) : hasAny(available, required);
|
|
297
|
+
|
|
298
|
+
return authorized ? { authorized: true } : { authorized: false, reason };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function authRedirectTo(options: AuthGuardOptions): string {
|
|
302
|
+
return options.redirectTo ?? authConfig.redirectTo;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function authForbiddenTo(options: AuthGuardOptions): string {
|
|
306
|
+
return options.forbiddenTo ?? authConfig.forbiddenTo;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function hasAll(
|
|
310
|
+
available: readonly string[] | undefined,
|
|
311
|
+
required: readonly string[] | undefined,
|
|
312
|
+
): boolean {
|
|
313
|
+
if (required === undefined || required.length === 0) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (available === undefined || available.length === 0) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const values = new Set(available);
|
|
322
|
+
|
|
323
|
+
return required.every((value) => values.has(value));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function hasAny(
|
|
327
|
+
available: readonly string[] | undefined,
|
|
328
|
+
required: readonly string[] | undefined,
|
|
329
|
+
): boolean {
|
|
330
|
+
if (required === undefined || required.length === 0) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (available === undefined || available.length === 0) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const values = new Set(available);
|
|
339
|
+
|
|
340
|
+
return required.some((value) => values.has(value));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function setSessionClaims(data: unknown): void {
|
|
344
|
+
const claims = normalizeSessionClaims(authConfig.serializeClaims(data));
|
|
345
|
+
const state = authRuntimeState();
|
|
346
|
+
const requestState = state.storage?.getStore();
|
|
347
|
+
|
|
348
|
+
if (requestState !== undefined) {
|
|
349
|
+
requestState.claims = claims;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
state.currentClaims = claims;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readClaimsFromDocument(): AuthSessionClaims | undefined {
|
|
357
|
+
const node = document.getElementById(__MREACT_AUTH_SESSION_SCRIPT_ID);
|
|
358
|
+
|
|
359
|
+
if (node?.textContent === undefined || node.textContent === "") {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(node.textContent) as unknown;
|
|
365
|
+
return normalizeSessionClaims(parsed);
|
|
366
|
+
} catch {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function defaultSerializeSessionClaims(data: unknown): AuthSessionClaims | undefined {
|
|
372
|
+
const claims = normalizeSessionClaims(data);
|
|
373
|
+
|
|
374
|
+
if (claims === undefined) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const safeClaims: AuthSessionClaims = {};
|
|
379
|
+
|
|
380
|
+
if (claims.permissions !== undefined) {
|
|
381
|
+
safeClaims.permissions = claims.permissions;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (claims.roles !== undefined) {
|
|
385
|
+
safeClaims.roles = claims.roles;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return Object.keys(safeClaims).length === 0 ? undefined : safeClaims;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeSessionClaims(value: unknown): AuthSessionClaims | undefined {
|
|
392
|
+
if (typeof value !== "object" || value === null) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const claims = value as AuthSessionClaims;
|
|
397
|
+
const roles = normalizeStringArray(claims.roles);
|
|
398
|
+
const permissions = normalizeStringArray(claims.permissions);
|
|
399
|
+
|
|
400
|
+
if (
|
|
401
|
+
(claims.roles !== undefined && roles === undefined) ||
|
|
402
|
+
(claims.permissions !== undefined && permissions === undefined)
|
|
403
|
+
) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
...claims,
|
|
409
|
+
...(permissions === undefined ? {} : { permissions }),
|
|
410
|
+
...(roles === undefined ? {} : { roles }),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeStringArray(value: unknown): readonly string[] | undefined {
|
|
415
|
+
if (value === undefined) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
420
|
+
? value
|
|
421
|
+
: undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function authRuntimeState(): AuthRuntimeState {
|
|
425
|
+
return getGlobalRuntimeState(authRuntimeStateKey, () => ({}));
|
|
426
|
+
}
|