@shipfox/api-auth 10.2.0 → 12.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 +40 -0
- package/README.md +48 -32
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +16 -8
- package/dist/core/auth.js.map +1 -1
- package/dist/core/job-lease-token.d.ts +1 -1
- package/dist/core/job-lease-token.d.ts.map +1 -1
- package/dist/core/job-lease-token.js +1 -1
- package/dist/core/job-lease-token.js.map +1 -1
- package/dist/core/jwt.d.ts +10 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +3 -2
- package/dist/core/jwt.js.map +1 -1
- package/dist/core/runner-session-token.d.ts +1 -1
- package/dist/core/runner-session-token.d.ts.map +1 -1
- package/dist/core/runner-session-token.js +1 -1
- package/dist/core/runner-session-token.js.map +1 -1
- package/dist/db/rate-limits.d.ts +6 -15
- package/dist/db/rate-limits.d.ts.map +1 -1
- package/dist/db/rate-limits.js +17 -18
- package/dist/db/rate-limits.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +3 -7
- package/dist/db/refresh-tokens.d.ts.map +1 -1
- package/dist/db/refresh-tokens.js +3 -9
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts +1 -4
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +5 -29
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/auth/lease-token-auth.d.ts +1 -1
- package/dist/presentation/auth/lease-token-auth.js +1 -1
- package/dist/presentation/auth/lease-token-auth.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +32 -53
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/core/auth.test.ts +29 -10
- package/src/core/auth.ts +27 -12
- package/src/core/job-lease-token.ts +1 -1
- package/src/core/jwt.test.ts +20 -4
- package/src/core/jwt.ts +2 -1
- package/src/core/runner-session-token.ts +1 -1
- package/src/db/rate-limits.ts +28 -42
- package/src/db/refresh-tokens.test.ts +0 -19
- package/src/db/refresh-tokens.ts +3 -26
- package/src/presentation/auth/jwt-auth.test.ts +33 -8
- package/src/presentation/auth/jwt-auth.ts +3 -26
- package/src/presentation/auth/lease-token-auth.ts +1 -1
- package/src/presentation/routes/administration.test.ts +9 -1
- package/src/presentation/routes/password/change-password.test.ts +6 -0
- package/src/presentation/routes/password/password-reset-confirm.test.ts +14 -0
- package/src/presentation/routes/rate-limit.ts +31 -64
- package/src/presentation/routes/registration/signup.test.ts +2 -2
- package/src/presentation/routes/session/logout.test.ts +6 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -723,7 +723,8 @@ describe('Auth administration routes', () => {
|
|
|
723
723
|
headers: {authorization: `Bearer ${target.token}`},
|
|
724
724
|
});
|
|
725
725
|
const suspendedLogin = await login(app, {email: target.email, password: target.password});
|
|
726
|
-
expect(suspendedMe.statusCode).toBe(
|
|
726
|
+
expect(suspendedMe.statusCode).toBe(200);
|
|
727
|
+
expect(suspendedMe.json().user).toMatchObject({id: target.userId, status: 'suspended'});
|
|
727
728
|
expect(suspendedLogin.statusCode).toBe(401);
|
|
728
729
|
|
|
729
730
|
const suspendedDbUser = (
|
|
@@ -841,6 +842,13 @@ describe('Auth administration routes', () => {
|
|
|
841
842
|
});
|
|
842
843
|
expect(secondRefresh.statusCode).toBe(401);
|
|
843
844
|
|
|
845
|
+
const revokedAccess = await app.inject({
|
|
846
|
+
method: 'GET',
|
|
847
|
+
url: '/auth/me',
|
|
848
|
+
headers: {authorization: `Bearer ${newLogin.json().token}`},
|
|
849
|
+
});
|
|
850
|
+
expect(revokedAccess.statusCode).toBe(200);
|
|
851
|
+
|
|
844
852
|
const repeatedRevoke = await app.inject({
|
|
845
853
|
method: 'POST',
|
|
846
854
|
url: `/admin/auth/users/${target.userId}/revoke-sessions`,
|
|
@@ -48,6 +48,11 @@ describe('POST /auth/change-password', () => {
|
|
|
48
48
|
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
49
49
|
payload: {},
|
|
50
50
|
});
|
|
51
|
+
const access = await app.inject({
|
|
52
|
+
method: 'GET',
|
|
53
|
+
url: '/auth/me',
|
|
54
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
55
|
+
});
|
|
51
56
|
const oldPasswordValid = await verifyPassword({
|
|
52
57
|
password: account.password,
|
|
53
58
|
hash: updatedUser?.hashedPassword ?? '',
|
|
@@ -62,6 +67,7 @@ describe('POST /auth/change-password', () => {
|
|
|
62
67
|
expect(oldPasswordValid).toBe(false);
|
|
63
68
|
expect(newPasswordValid).toBe(true);
|
|
64
69
|
expect(refresh.statusCode).toBe(200);
|
|
70
|
+
expect(access.statusCode).toBe(200);
|
|
65
71
|
});
|
|
66
72
|
|
|
67
73
|
test('transforms invalid current password into 401', async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {AUTH_PASSWORD_RESET_SEND_REQUESTED} from '@shipfox/api-auth-dto';
|
|
2
2
|
import type {FastifyInstance} from 'fastify';
|
|
3
3
|
import {
|
|
4
|
+
cookieHeader,
|
|
4
5
|
createAuthTestApp,
|
|
5
6
|
extractToken,
|
|
6
7
|
getSetCookie,
|
|
@@ -42,12 +43,25 @@ describe('POST /auth/password-reset/confirm', () => {
|
|
|
42
43
|
payload: {token: resetToken, new_password: 'reset password is long'},
|
|
43
44
|
});
|
|
44
45
|
const loginRes = await login(app, {email: account.email, password: 'reset password is long'});
|
|
46
|
+
const oldRefresh = await app.inject({
|
|
47
|
+
method: 'POST',
|
|
48
|
+
url: '/auth/refresh',
|
|
49
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
50
|
+
payload: {},
|
|
51
|
+
});
|
|
52
|
+
const oldAccess = await app.inject({
|
|
53
|
+
method: 'GET',
|
|
54
|
+
url: '/auth/me',
|
|
55
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
56
|
+
});
|
|
45
57
|
|
|
46
58
|
expect(confirm.statusCode).toBe(200);
|
|
47
59
|
expect(confirm.json().token).toBeDefined();
|
|
48
60
|
expect(confirm.json().user.email).toBe(account.email);
|
|
49
61
|
expect(getSetCookie(confirm)).toContain('shipfox_refresh_token=');
|
|
50
62
|
expect(loginRes.statusCode).toBe(200);
|
|
63
|
+
expect(oldRefresh.statusCode).toBe(401);
|
|
64
|
+
expect(oldAccess.statusCode).toBe(200);
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
test('transforms invalid token into 410', async () => {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';
|
|
2
|
+
import {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';
|
|
2
3
|
import {
|
|
3
4
|
type AuthRateLimitAction,
|
|
4
|
-
AuthRateLimitExceededError,
|
|
5
5
|
type AuthRateLimitPolicy,
|
|
6
6
|
type AuthRateLimitScope,
|
|
7
|
-
AuthRateLimitUnavailableError,
|
|
8
7
|
checkAuthRateLimit,
|
|
9
8
|
} from '#core/rate-limit.js';
|
|
10
9
|
|
|
@@ -49,68 +48,36 @@ async function enforceRateLimit(params: {
|
|
|
49
48
|
const policy = policies[params.action][params.scope];
|
|
50
49
|
if (!policy) return;
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (error instanceof AuthRateLimitUnavailableError) {
|
|
86
|
-
params.request.log.error(
|
|
87
|
-
{
|
|
88
|
-
action: error.action,
|
|
89
|
-
scope: error.scope,
|
|
90
|
-
route: routeName(params.request),
|
|
91
|
-
identifierHmacPrefix: error.identifierHmacPrefix,
|
|
92
|
-
err: error,
|
|
93
|
-
},
|
|
94
|
-
'Auth rate limiter unavailable',
|
|
95
|
-
);
|
|
96
|
-
throw new ClientError(
|
|
97
|
-
'Authentication rate limiter unavailable',
|
|
98
|
-
'auth-rate-limit-unavailable',
|
|
99
|
-
{
|
|
100
|
-
status: 503,
|
|
101
|
-
data: {
|
|
102
|
-
action: error.action,
|
|
103
|
-
scope: error.scope,
|
|
104
|
-
route: routeName(params.request),
|
|
105
|
-
identifierHmacPrefix: error.identifierHmacPrefix,
|
|
106
|
-
},
|
|
107
|
-
cause: error,
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
51
|
+
await enforceSharedRateLimit({
|
|
52
|
+
request: params.request,
|
|
53
|
+
reply: params.reply,
|
|
54
|
+
check: () =>
|
|
55
|
+
checkAuthRateLimit({
|
|
56
|
+
action: params.action,
|
|
57
|
+
scope: params.scope,
|
|
58
|
+
identifier: params.identifier,
|
|
59
|
+
...policy,
|
|
60
|
+
}),
|
|
61
|
+
route: routeName(params.request),
|
|
62
|
+
unavailableCode: 'auth-rate-limit-unavailable',
|
|
63
|
+
unavailableMessage: 'Authentication rate limiter unavailable',
|
|
64
|
+
setRetryAfter: (reply, retryAfterSeconds) => {
|
|
65
|
+
reply.header('Retry-After', String(retryAfterSeconds));
|
|
66
|
+
},
|
|
67
|
+
logWarn: (request, context) => {
|
|
68
|
+
request.log.warn(context, 'Auth rate limit blocked request');
|
|
69
|
+
},
|
|
70
|
+
logError: (request, context) => {
|
|
71
|
+
request.log.error(context, 'Auth rate limiter unavailable');
|
|
72
|
+
},
|
|
73
|
+
createClientError: (presentation, cause) =>
|
|
74
|
+
new ClientError(presentation.message, presentation.code, {
|
|
75
|
+
status: presentation.status,
|
|
76
|
+
...(presentation.details ? {details: presentation.details} : {}),
|
|
77
|
+
data: presentation.data,
|
|
78
|
+
cause,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
114
81
|
}
|
|
115
82
|
|
|
116
83
|
export function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {
|
|
@@ -165,7 +165,7 @@ describe('POST /auth/signup', () => {
|
|
|
165
165
|
alreadyMember: false,
|
|
166
166
|
}));
|
|
167
167
|
listMembershipsByUserMock.mockResolvedValueOnce({
|
|
168
|
-
memberships: [{workspaceId, role: 'admin' as const}],
|
|
168
|
+
memberships: [{workspaceId, role: 'admin' as const, workspaceStatus: 'active'}],
|
|
169
169
|
});
|
|
170
170
|
const res = await app.inject({
|
|
171
171
|
method: 'POST',
|
|
@@ -190,7 +190,7 @@ describe('POST /auth/signup', () => {
|
|
|
190
190
|
workspace_id: workspaceId,
|
|
191
191
|
});
|
|
192
192
|
expect(body.accept_error).toBeUndefined();
|
|
193
|
-
expect(claims.memberships).toEqual([{workspaceId, role: 'admin'}]);
|
|
193
|
+
expect(claims.memberships).toEqual([{workspaceId, role: 'admin', workspaceStatus: 'active'}]);
|
|
194
194
|
expect(capturedMail()).toHaveLength(0);
|
|
195
195
|
});
|
|
196
196
|
|
|
@@ -36,10 +36,16 @@ describe('POST /auth/logout', () => {
|
|
|
36
36
|
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
37
37
|
payload: {},
|
|
38
38
|
});
|
|
39
|
+
const access = await app.inject({
|
|
40
|
+
method: 'GET',
|
|
41
|
+
url: '/auth/me',
|
|
42
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
43
|
+
});
|
|
39
44
|
|
|
40
45
|
expect(res.statusCode).toBe(204);
|
|
41
46
|
expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=;');
|
|
42
47
|
expect(refresh.statusCode).toBe(401);
|
|
48
|
+
expect(access.statusCode).toBe(200);
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
test('returns 204 when the refresh cookie is missing', async () => {
|
|
@@ -85,5 +85,16 @@ describe('POST /auth/refresh', () => {
|
|
|
85
85
|
|
|
86
86
|
expect(res.statusCode).toBe(503);
|
|
87
87
|
expect(res.json().code).toBe('auth-dependency-unavailable');
|
|
88
|
+
expect(res.headers['set-cookie']).toBeUndefined();
|
|
89
|
+
|
|
90
|
+
const retry = await app.inject({
|
|
91
|
+
method: 'POST',
|
|
92
|
+
url: '/auth/refresh',
|
|
93
|
+
headers: {cookie: cookieHeader(account.refreshCookie)},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(retry.statusCode).toBe(200);
|
|
97
|
+
expect(retry.json().token).toBeDefined();
|
|
98
|
+
expect(retry.headers['set-cookie']).toContain('shipfox_refresh_token=');
|
|
88
99
|
});
|
|
89
100
|
});
|