@shipfox/api-auth 13.1.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +35 -0
- package/README.md +41 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/administration.d.ts +24 -2
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +126 -3
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts +31 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +70 -2
- package/dist/core/auth.js.map +1 -1
- package/dist/core/errors.d.ts +15 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/jwt.d.ts +2 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +22 -1
- package/dist/core/jwt.js.map +1 -1
- package/dist/db/admin-command.d.ts +12 -4
- package/dist/db/admin-command.d.ts.map +1 -1
- package/dist/db/admin-command.js +10 -1
- package/dist/db/admin-command.js.map +1 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +4 -1
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts +3 -0
- package/dist/db/admin-user-moderation.d.ts.map +1 -1
- package/dist/db/admin-user-moderation.js +6 -2
- package/dist/db/admin-user-moderation.js.map +1 -1
- package/dist/db/impersonation.d.ts +55 -0
- package/dist/db/impersonation.d.ts.map +1 -0
- package/dist/db/impersonation.js +212 -0
- package/dist/db/impersonation.js.map +1 -0
- package/dist/db/schema/admin-command-results.d.ts +16 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -1
- package/dist/db/schema/admin-command-results.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/index.d.ts +1 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +4 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +8 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.js +2 -2
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +26 -2
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +2 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +127 -19
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts +8 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +44 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/presentation/routes/session/me.d.ts.map +1 -1
- package/dist/presentation/routes/session/me.js +2 -1
- package/dist/presentation/routes/session/me.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/config.ts +4 -0
- package/src/core/administration.ts +172 -1
- package/src/core/auth.test.ts +79 -0
- package/src/core/auth.ts +98 -1
- package/src/core/errors.ts +35 -0
- package/src/core/jwt.test.ts +92 -0
- package/src/core/jwt.ts +38 -9
- package/src/db/admin-command.ts +35 -6
- package/src/db/admin-grants.ts +5 -1
- package/src/db/admin-user-moderation.ts +8 -2
- package/src/db/impersonation.ts +323 -0
- package/src/db/schema/admin-command-results.ts +18 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +12 -3
- package/src/metrics/index.ts +2 -0
- package/src/metrics/instance.ts +13 -2
- package/src/presentation/auth/bearer-token-auth.ts +4 -4
- package/src/presentation/auth/jwt-auth.test.ts +191 -0
- package/src/presentation/auth/jwt-auth.ts +19 -1
- package/src/presentation/routes/administration.test.ts +828 -3
- package/src/presentation/routes/administration.ts +130 -11
- package/src/presentation/routes/rate-limit.ts +40 -0
- package/src/presentation/routes/session/login.test.ts +5 -0
- package/src/presentation/routes/session/me.test.ts +24 -0
- package/src/presentation/routes/session/me.ts +1 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/test/routes.ts +43 -16
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
AUTH_USER,
|
|
4
|
+
adoptAdministrationActorGuard,
|
|
5
|
+
requireAdministrationActor,
|
|
6
|
+
} from '@shipfox/api-auth-context';
|
|
2
7
|
import {
|
|
3
8
|
adminBootstrapStateSchema,
|
|
4
9
|
administratorUserLookupQuerySchema,
|
|
@@ -8,6 +13,8 @@ import {
|
|
|
8
13
|
bootstrapAdminOwnerResponseSchema,
|
|
9
14
|
grantAdminRoleBodySchema,
|
|
10
15
|
grantAdminRoleResponseSchema,
|
|
16
|
+
impersonateResponseSchema,
|
|
17
|
+
impersonateUserBodySchema,
|
|
11
18
|
listAdminGrantsQuerySchema,
|
|
12
19
|
listAdminGrantsResponseSchema,
|
|
13
20
|
reactivateAdministratorUserBodySchema,
|
|
@@ -17,6 +24,7 @@ import {
|
|
|
17
24
|
revokeAdministratorUserSessionsResponseSchema,
|
|
18
25
|
suspendAdministratorUserBodySchema,
|
|
19
26
|
} from '@shipfox/api-auth-dto';
|
|
27
|
+
import type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';
|
|
20
28
|
import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
|
|
21
29
|
import {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';
|
|
22
30
|
import type {FastifyRequest} from 'fastify';
|
|
@@ -27,6 +35,7 @@ import {
|
|
|
27
35
|
findAdministratorUserSummary,
|
|
28
36
|
getAdminBootstrapState,
|
|
29
37
|
grantAdministratorRole,
|
|
38
|
+
impersonateUser,
|
|
30
39
|
listAdministratorGrantSummaries,
|
|
31
40
|
reactivateAdministratorUser,
|
|
32
41
|
revokeAdministratorGrant,
|
|
@@ -44,12 +53,20 @@ import {
|
|
|
44
53
|
AdminGrantNotFoundError,
|
|
45
54
|
AdminIdempotencyKeyReuseError,
|
|
46
55
|
AdminRoleRequiredError,
|
|
56
|
+
CannotImpersonateAdministratorError,
|
|
57
|
+
CannotImpersonateSelfError,
|
|
58
|
+
EmailNotVerifiedError,
|
|
59
|
+
ImpersonationDisabledError,
|
|
60
|
+
ImpersonationExpiredError,
|
|
61
|
+
ImpersonationTargetNotActiveError,
|
|
47
62
|
InvalidAdminBootstrapTokenError,
|
|
63
|
+
InvalidCredentialsError,
|
|
48
64
|
LastAdminOwnerError,
|
|
49
65
|
UserNotFoundError,
|
|
50
66
|
} from '#core/errors.js';
|
|
51
67
|
import {getClientContext} from '#presentation/auth/jwt-auth.js';
|
|
52
|
-
import {
|
|
68
|
+
import {toUserDto} from '#presentation/dto/user.js';
|
|
69
|
+
import {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';
|
|
53
70
|
|
|
54
71
|
const idempotencyKeyMaxLength = 256;
|
|
55
72
|
|
|
@@ -159,6 +176,40 @@ function translateAdministrationError(error: unknown): never {
|
|
|
159
176
|
{status: 409},
|
|
160
177
|
);
|
|
161
178
|
}
|
|
179
|
+
if (error instanceof ImpersonationDisabledError) {
|
|
180
|
+
throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});
|
|
181
|
+
}
|
|
182
|
+
if (error instanceof CannotImpersonateSelfError) {
|
|
183
|
+
throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {
|
|
184
|
+
status: 403,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (error instanceof CannotImpersonateAdministratorError) {
|
|
188
|
+
throw new ClientError(
|
|
189
|
+
'Cannot impersonate an administrator',
|
|
190
|
+
'cannot-impersonate-administrator',
|
|
191
|
+
{status: 403},
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (error instanceof ImpersonationTargetNotActiveError) {
|
|
195
|
+
throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
|
|
196
|
+
status: 403,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// The mint primitive re-checks login eligibility (active, verified) on the
|
|
200
|
+
// row it reads, and a concurrent suspension or unverification between the
|
|
201
|
+
// in-transaction ladder read and that read surfaces these errors. Map them
|
|
202
|
+
// to the same documented client error instead of a generic 500.
|
|
203
|
+
if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
|
|
204
|
+
throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
|
|
205
|
+
status: 403,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (error instanceof ImpersonationExpiredError) {
|
|
209
|
+
throw new ClientError('Impersonation session has expired', 'impersonation-expired', {
|
|
210
|
+
status: 410,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
162
213
|
throw error;
|
|
163
214
|
}
|
|
164
215
|
|
|
@@ -362,20 +413,88 @@ const revokeRoute = defineRoute({
|
|
|
362
413
|
},
|
|
363
414
|
});
|
|
364
415
|
|
|
365
|
-
export const administrationRoutes: RouteGroup = {
|
|
416
|
+
export const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({
|
|
366
417
|
prefix: '/admin/auth/admin-grants',
|
|
367
418
|
auth: AUTH_USER,
|
|
368
419
|
routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],
|
|
369
|
-
};
|
|
420
|
+
});
|
|
370
421
|
|
|
371
|
-
export const administrationBootstrapRoutes: RouteGroup = {
|
|
422
|
+
export const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({
|
|
372
423
|
prefix: '/admin/auth',
|
|
373
424
|
auth: AUTH_USER,
|
|
374
425
|
routes: [bootstrapStateRoute],
|
|
375
|
-
};
|
|
426
|
+
});
|
|
376
427
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
428
|
+
function createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {
|
|
429
|
+
return defineRoute({
|
|
430
|
+
method: 'POST',
|
|
431
|
+
path: '/:userId/impersonate',
|
|
432
|
+
description:
|
|
433
|
+
'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',
|
|
434
|
+
schema: {
|
|
435
|
+
params: z.object({userId: z.string().uuid()}),
|
|
436
|
+
body: impersonateUserBodySchema,
|
|
437
|
+
response: {200: impersonateResponseSchema},
|
|
438
|
+
},
|
|
439
|
+
// The limiter runs ahead of the impersonated-session rejection: a request
|
|
440
|
+
// carrying an already-issued impersonated token must consume the
|
|
441
|
+
// `impersonate` IP and actor buckets instead of being rejected by the
|
|
442
|
+
// guard first, or marked-session probes would bypass the limiter entirely.
|
|
443
|
+
// The guard still runs before the authorization inside the command.
|
|
444
|
+
preHandler: [
|
|
445
|
+
createAuthActorRateLimitPreHandler('impersonate'),
|
|
446
|
+
(request) => {
|
|
447
|
+
requireAdministrationActor(request);
|
|
448
|
+
return undefined;
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
errorHandler: translateAdministrationError,
|
|
452
|
+
handler: async (request) => {
|
|
453
|
+
const client = getClientContext(request);
|
|
454
|
+
const result = await impersonateUser({
|
|
455
|
+
actorId: requireActorId(request),
|
|
456
|
+
...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),
|
|
457
|
+
targetUserId: request.params.userId,
|
|
458
|
+
reason: request.body.reason,
|
|
459
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
460
|
+
// Fastify's default request IDs are process-local counters that reset
|
|
461
|
+
// after a redeploy, so a replay could collide with an earlier mint's
|
|
462
|
+
// ID and suppress the required failure event in the ambiguous-COMMIT
|
|
463
|
+
// reconciliation. The correlation is a fresh process-independent UUID
|
|
464
|
+
// per invocation.
|
|
465
|
+
correlationId: randomUUID(),
|
|
466
|
+
workspaces,
|
|
467
|
+
});
|
|
468
|
+
// `server_time` is the issuer's clock at response time: the banner
|
|
469
|
+
// derives its countdown and Extend availability from this anchor.
|
|
470
|
+
return {
|
|
471
|
+
token: result.token,
|
|
472
|
+
expires_at: result.expiresAt.toISOString(),
|
|
473
|
+
server_time: new Date().toISOString(),
|
|
474
|
+
impersonator_id: result.impersonatorId,
|
|
475
|
+
user: toUserDto(result.user),
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function createAdministrationUserRoutes(
|
|
482
|
+
workspaces: WorkspacesInterModuleClient,
|
|
483
|
+
): RouteGroup[] {
|
|
484
|
+
return [
|
|
485
|
+
adoptAdministrationActorGuard({
|
|
486
|
+
prefix: '/admin/auth/users',
|
|
487
|
+
auth: AUTH_USER,
|
|
488
|
+
routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],
|
|
489
|
+
}),
|
|
490
|
+
// The impersonate route mounts outside the adopted guard on purpose: its
|
|
491
|
+
// limiter must run before the impersonated-session rejection (see
|
|
492
|
+
// `createImpersonateUserRoute`), so the guard is applied positionally
|
|
493
|
+
// there. Every other route under `/admin/auth/users` keeps the adoption.
|
|
494
|
+
{
|
|
495
|
+
prefix: '/admin/auth/users',
|
|
496
|
+
auth: AUTH_USER,
|
|
497
|
+
routes: [createImpersonateUserRoute(workspaces)],
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {getUserContext} from '@shipfox/api-auth-context';
|
|
1
2
|
import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';
|
|
2
3
|
import {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';
|
|
3
4
|
import {
|
|
@@ -28,6 +29,16 @@ const policies: Record<
|
|
|
28
29
|
lookup: {
|
|
29
30
|
ip: {limit: 60, windowSeconds: 5 * 60},
|
|
30
31
|
},
|
|
32
|
+
impersonate: {
|
|
33
|
+
// Bounds mint and probing attempts: the IP bucket bounds aggregate source
|
|
34
|
+
// traffic, and the actor bucket is a per-actor cap that prevents an actor
|
|
35
|
+
// from evading the bound by rotating IPs (every request still consumes the
|
|
36
|
+
// IP bucket first, so a shared NAT cannot exhaust it faster by pooling
|
|
37
|
+
// actors). Ladder denials by an identifiable actor role are audited as
|
|
38
|
+
// `failed` events.
|
|
39
|
+
ip: {limit: 20, windowSeconds: 15 * 60},
|
|
40
|
+
actor: {limit: 20, windowSeconds: 15 * 60},
|
|
41
|
+
},
|
|
31
42
|
};
|
|
32
43
|
|
|
33
44
|
interface EmailBody {
|
|
@@ -111,3 +122,32 @@ export function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {
|
|
|
111
122
|
});
|
|
112
123
|
};
|
|
113
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Rate limits an authenticated route by source IP and by actor. Every request
|
|
128
|
+
* consumes the IP bucket, which bounds aggregate source traffic; the actor
|
|
129
|
+
* bucket is an additional per-actor cap that prevents an actor from evading
|
|
130
|
+
* the bound by rotating IPs. Runs after authentication (auth is an `onRequest`
|
|
131
|
+
* hook), so the user context is set.
|
|
132
|
+
*/
|
|
133
|
+
export function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {
|
|
134
|
+
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
|
135
|
+
await enforceRateLimit({
|
|
136
|
+
request,
|
|
137
|
+
reply,
|
|
138
|
+
action,
|
|
139
|
+
scope: 'ip',
|
|
140
|
+
identifier: request.ip,
|
|
141
|
+
});
|
|
142
|
+
const client = getUserContext(request);
|
|
143
|
+
if (client) {
|
|
144
|
+
await enforceRateLimit({
|
|
145
|
+
request,
|
|
146
|
+
reply,
|
|
147
|
+
action,
|
|
148
|
+
scope: 'actor',
|
|
149
|
+
identifier: client.userId,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type {FastifyInstance} from 'fastify';
|
|
2
|
+
import {verifyUserToken} from '#core/jwt.js';
|
|
2
3
|
import {
|
|
3
4
|
createAuthTestApp,
|
|
4
5
|
listMembershipsByUserMock,
|
|
5
6
|
login,
|
|
7
|
+
ROUTE_TEST_SECRET,
|
|
6
8
|
resetCapturedMail,
|
|
7
9
|
signup,
|
|
8
10
|
uniqueEmail,
|
|
@@ -36,6 +38,9 @@ describe('POST /auth/login', () => {
|
|
|
36
38
|
expect(res.json().token).toBeDefined();
|
|
37
39
|
expect(res.json().user.email).toBe(email);
|
|
38
40
|
expect(res.json().admin_role).toBeNull();
|
|
41
|
+
expect(res.json().impersonator_id).toBeUndefined();
|
|
42
|
+
const claims = await verifyUserToken({token: res.json().token, secret: ROUTE_TEST_SECRET});
|
|
43
|
+
expect(claims.impersonatorId).toBeUndefined();
|
|
39
44
|
expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
|
|
40
45
|
expect(res.headers['set-cookie']).toContain('HttpOnly');
|
|
41
46
|
expect(res.headers['set-cookie']).toContain('Secure');
|
|
@@ -35,6 +35,30 @@ describe('GET /auth/me', () => {
|
|
|
35
35
|
expect(res.statusCode).toBe(200);
|
|
36
36
|
expect(res.json().user.email).toBe(account.email);
|
|
37
37
|
expect(res.json().admin_role).toBeNull();
|
|
38
|
+
expect(res.json().impersonator_id).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns impersonator_id for a marked session token', async () => {
|
|
42
|
+
const account = await createVerifiedSession('me-impersonated');
|
|
43
|
+
const impersonatorId = crypto.randomUUID();
|
|
44
|
+
const token = await signUserToken({
|
|
45
|
+
userId: account.userId,
|
|
46
|
+
impersonatorId,
|
|
47
|
+
email: account.email,
|
|
48
|
+
memberships: [],
|
|
49
|
+
secret: ROUTE_TEST_SECRET,
|
|
50
|
+
expiresIn: '15m',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const res = await app.inject({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
url: '/auth/me',
|
|
56
|
+
headers: {authorization: `Bearer ${token}`},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(res.statusCode).toBe(200);
|
|
60
|
+
expect(res.json().user.email).toBe(account.email);
|
|
61
|
+
expect(res.json().impersonator_id).toBe(impersonatorId);
|
|
38
62
|
});
|
|
39
63
|
|
|
40
64
|
test('returns the current Auth-owned role for dashboard presentation', async () => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type {FastifyInstance} from 'fastify';
|
|
2
|
+
import {verifyUserToken} from '#core/jwt.js';
|
|
2
3
|
import {
|
|
3
4
|
cookieHeader,
|
|
4
5
|
createAuthTestApp,
|
|
5
6
|
listMembershipsByUserMock,
|
|
7
|
+
ROUTE_TEST_SECRET,
|
|
6
8
|
resetCapturedMail,
|
|
7
9
|
signupVerifyLogin,
|
|
8
10
|
} from '#test/routes.js';
|
|
@@ -34,6 +36,15 @@ describe('POST /auth/refresh', () => {
|
|
|
34
36
|
expect(res.statusCode).toBe(200);
|
|
35
37
|
expect(res.json().token).toBeDefined();
|
|
36
38
|
expect(res.json().user.email).toBe(account.email);
|
|
39
|
+
// The refresh contract stays unmarked: the response never exposes
|
|
40
|
+
// impersonator_id and rotation re-issues an ordinary access token.
|
|
41
|
+
// refreshAccessToken signs from (user, memberships, sessionId) and never
|
|
42
|
+
// sees the access token, so a marker cannot survive rotation here (ADR 0014
|
|
43
|
+
// keeps impersonated sessions access-token-only; refresh restores the
|
|
44
|
+
// administrator's session).
|
|
45
|
+
expect(res.json().impersonator_id).toBeUndefined();
|
|
46
|
+
const claims = await verifyUserToken({token: res.json().token, secret: ROUTE_TEST_SECRET});
|
|
47
|
+
expect(claims.impersonatorId).toBeUndefined();
|
|
37
48
|
expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
|
|
38
49
|
expect(res.headers['set-cookie']).toContain('HttpOnly');
|
|
39
50
|
expect(res.headers['set-cookie']).toContain('Secure');
|
package/test/routes.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
|
11
11
|
import {
|
|
12
12
|
administrationBootstrapRoutes,
|
|
13
13
|
administrationRoutes,
|
|
14
|
-
|
|
14
|
+
createAdministrationUserRoutes,
|
|
15
15
|
} from '#presentation/routes/administration.js';
|
|
16
16
|
import {buildAuthRoutes} from '#presentation/routes/index.js';
|
|
17
17
|
|
|
@@ -21,6 +21,20 @@ const testConfig = vi.hoisted(
|
|
|
21
21
|
challenges: Map<string, string>;
|
|
22
22
|
mailer: Mailer;
|
|
23
23
|
clientBaseUrl: string;
|
|
24
|
+
authConfig: {
|
|
25
|
+
ADMIN_BOOTSTRAP_TOKEN: string;
|
|
26
|
+
AUTH_JWT_EXPIRES_IN: string;
|
|
27
|
+
AUTH_IMPERSONATION_ENABLED: boolean;
|
|
28
|
+
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: number;
|
|
29
|
+
AUTH_REFRESH_ROTATION_GRACE_SECONDS: number;
|
|
30
|
+
AUTH_REFRESH_COOKIE_NAME: string;
|
|
31
|
+
AUTH_PASSWORD_ENABLED: boolean;
|
|
32
|
+
AUTH_SIGNUP_GATE_ENABLED: boolean;
|
|
33
|
+
AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: string;
|
|
34
|
+
AUTH_SIGNUP_ALLOWED_EMAILS: string;
|
|
35
|
+
AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: string | undefined;
|
|
36
|
+
CLIENT_BASE_URL: string;
|
|
37
|
+
};
|
|
24
38
|
} => {
|
|
25
39
|
const captured: MailMessage[] = [];
|
|
26
40
|
const mailer: Mailer = {
|
|
@@ -29,11 +43,26 @@ const testConfig = vi.hoisted(
|
|
|
29
43
|
return Promise.resolve();
|
|
30
44
|
},
|
|
31
45
|
};
|
|
46
|
+
const authConfig = {
|
|
47
|
+
ADMIN_BOOTSTRAP_TOKEN: 'test-bootstrap-token',
|
|
48
|
+
AUTH_JWT_EXPIRES_IN: '15m',
|
|
49
|
+
AUTH_IMPERSONATION_ENABLED: true,
|
|
50
|
+
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
51
|
+
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
52
|
+
AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
|
|
53
|
+
AUTH_PASSWORD_ENABLED: true,
|
|
54
|
+
AUTH_SIGNUP_GATE_ENABLED: false,
|
|
55
|
+
AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: '',
|
|
56
|
+
AUTH_SIGNUP_ALLOWED_EMAILS: '',
|
|
57
|
+
AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: undefined,
|
|
58
|
+
CLIENT_BASE_URL: 'https://app.example.test',
|
|
59
|
+
};
|
|
32
60
|
return {
|
|
33
61
|
captured,
|
|
34
62
|
challenges: new Map(),
|
|
35
63
|
mailer,
|
|
36
|
-
clientBaseUrl:
|
|
64
|
+
clientBaseUrl: authConfig.CLIENT_BASE_URL,
|
|
65
|
+
authConfig,
|
|
37
66
|
};
|
|
38
67
|
},
|
|
39
68
|
);
|
|
@@ -52,19 +81,7 @@ const workspaceTestDoubles = vi.hoisted(() => {
|
|
|
52
81
|
const workspaces = workspaceTestDoubles as unknown as WorkspacesInterModuleClient;
|
|
53
82
|
|
|
54
83
|
vi.mock('#config.js', () => ({
|
|
55
|
-
config:
|
|
56
|
-
ADMIN_BOOTSTRAP_TOKEN: 'test-bootstrap-token',
|
|
57
|
-
AUTH_JWT_EXPIRES_IN: '15m',
|
|
58
|
-
AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: 14,
|
|
59
|
-
AUTH_REFRESH_ROTATION_GRACE_SECONDS: 30,
|
|
60
|
-
AUTH_REFRESH_COOKIE_NAME: 'shipfox_refresh_token',
|
|
61
|
-
AUTH_PASSWORD_ENABLED: true,
|
|
62
|
-
AUTH_SIGNUP_GATE_ENABLED: false,
|
|
63
|
-
AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: '',
|
|
64
|
-
AUTH_SIGNUP_ALLOWED_EMAILS: '',
|
|
65
|
-
AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: undefined,
|
|
66
|
-
CLIENT_BASE_URL: testConfig.clientBaseUrl,
|
|
67
|
-
},
|
|
84
|
+
config: testConfig.authConfig,
|
|
68
85
|
mailer: testConfig.mailer,
|
|
69
86
|
}));
|
|
70
87
|
|
|
@@ -92,6 +109,16 @@ export function resetCapturedMail(): void {
|
|
|
92
109
|
listMembershipsByUserMock.mockResolvedValue({memberships: []});
|
|
93
110
|
}
|
|
94
111
|
|
|
112
|
+
/** Flips `AUTH_IMPERSONATION_ENABLED` for the shared route-test config mock. */
|
|
113
|
+
export function setImpersonationEnabled(enabled: boolean): void {
|
|
114
|
+
testConfig.authConfig.AUTH_IMPERSONATION_ENABLED = enabled;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Flips `AUTH_JWT_EXPIRES_IN` for the shared route-test config mock. */
|
|
118
|
+
export function setAuthJwtExpiresIn(expiresIn: string): void {
|
|
119
|
+
testConfig.authConfig.AUTH_JWT_EXPIRES_IN = expiresIn;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
export function capturedMail(): MailMessage[] {
|
|
96
123
|
return testConfig.captured;
|
|
97
124
|
}
|
|
@@ -162,7 +189,7 @@ export async function createAuthTestApp(params?: {
|
|
|
162
189
|
buildAuthRoutes(true, workspaces),
|
|
163
190
|
administrationBootstrapRoutes,
|
|
164
191
|
administrationRoutes,
|
|
165
|
-
|
|
192
|
+
...createAdministrationUserRoutes(workspaces),
|
|
166
193
|
],
|
|
167
194
|
swagger: false,
|
|
168
195
|
};
|