Skip to content

Commit f5cc3d9

Browse files
SpecialAroCopilot
andauthored
Patch adonis5-jwt to fix jwt errors spamming (#163)
* Patch adonis5-jwt to fix jwt errors spamming * Update tests/functional/api/register.spec.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 52dfc44 commit f5cc3d9

5 files changed

Lines changed: 187 additions & 6 deletions

File tree

app/Controllers/Http/UserController.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ export default class UsersController {
9191
}
9292

9393
// Generate new auth token
94-
const token = await auth.use('jwt').login(user, { payload: {} });
94+
const token = await auth.use('jwt').login(user, {
95+
payload: {
96+
nonce: crypto.randomUUID(),
97+
},
98+
});
9599

96100
return response.send({
97101
message: 'Successfully created account',
@@ -140,7 +144,11 @@ export default class UsersController {
140144
}
141145

142146
// Generate token
143-
const token = await auth.use('jwt').login(user, { payload: {} });
147+
const token = await auth.use('jwt').login(user, {
148+
payload: {
149+
nonce: crypto.randomUUID(),
150+
},
151+
});
144152

145153
return response.send({
146154
message: 'Successfully logged in',
@@ -228,7 +236,11 @@ export default class UsersController {
228236
return response.send('Missing or invalid api token');
229237
}
230238

231-
const token = await auth.use('jwt').generate(user, { payload: {} });
239+
const token = await auth.use('jwt').generate(user, {
240+
payload: {
241+
nonce: crypto.randomUUID(),
242+
},
243+
});
232244

233245
return response.send({
234246
token: token.accessToken,

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@
110110
"aws-sdk",
111111
"bcrypt",
112112
"sqlite3"
113-
]
113+
],
114+
"patchedDependencies": {
115+
"adonis5-jwt@1.1.7": "patches/adonis5-jwt@1.1.7.patch"
116+
}
114117
}
115118
}

patches/adonis5-jwt@1.1.7.patch

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
diff --git a/build/lib/Guards/JwtGuard.js b/build/lib/Guards/JwtGuard.js
2+
index e369820d726e37c4d924d18d879e847e3accf669..058180d34043122c46711f551cab0442de66d28a 100644
3+
--- a/build/lib/Guards/JwtGuard.js
4+
+++ b/build/lib/Guards/JwtGuard.js
5+
@@ -216,27 +216,37 @@ class JWTGuard extends Base_1.BaseGuard {
6+
/**
7+
* Generate a JWT and refresh token
8+
*/
9+
- const tokenInfo = await this.generateTokenForPersistance(expiresIn, refreshTokenExpiresIn, payload, rememberMe);
10+
let providerToken;
11+
- if (!this.config.persistJwt) {
12+
- /**
13+
- * Persist refresh token ONLY to the database.
14+
- */
15+
- providerToken = new ProviderToken_1.ProviderToken(name, tokenInfo.refreshTokenHash, userId, this.tokenType);
16+
- providerToken.expiresAt = tokenInfo.refreshTokenExpiresAt;
17+
- providerToken.meta = meta;
18+
- await this.tokenProvider.write(providerToken);
19+
- }
20+
- else {
21+
- /**
22+
- * Persist JWT token and refresh token to the database
23+
- */
24+
- providerToken = new JwtProviderToken_1.JwtProviderToken(name, tokenInfo.accessTokenHash, userId, this.tokenType);
25+
- providerToken.expiresAt = tokenInfo.expiresAt;
26+
- providerToken.refreshToken = tokenInfo.refreshTokenHash;
27+
- providerToken.refreshTokenExpiresAt = tokenInfo.refreshTokenExpiresAt;
28+
- providerToken.meta = meta;
29+
- await this.tokenProvider.write(providerToken);
30+
+ let tokenInfo;
31+
+ for (let attempts = 0; attempts < 3; attempts++) {
32+
+ tokenInfo = await this.generateTokenForPersistance(expiresIn, refreshTokenExpiresIn, payload, rememberMe);
33+
+ if (!this.config.persistJwt) {
34+
+ /**
35+
+ * Persist refresh token ONLY to the database.
36+
+ */
37+
+ providerToken = new ProviderToken_1.ProviderToken(name, tokenInfo.refreshTokenHash, userId, this.tokenType);
38+
+ providerToken.expiresAt = tokenInfo.refreshTokenExpiresAt;
39+
+ providerToken.meta = meta;
40+
+ }
41+
+ else {
42+
+ /**
43+
+ * Persist JWT token and refresh token to the database
44+
+ */
45+
+ providerToken = new JwtProviderToken_1.JwtProviderToken(name, tokenInfo.accessTokenHash, userId, this.tokenType);
46+
+ providerToken.expiresAt = tokenInfo.expiresAt;
47+
+ providerToken.refreshToken = tokenInfo.refreshTokenHash;
48+
+ providerToken.refreshTokenExpiresAt = tokenInfo.refreshTokenExpiresAt;
49+
+ providerToken.meta = meta;
50+
+ }
51+
+ try {
52+
+ await this.tokenProvider.write(providerToken);
53+
+ break;
54+
+ }
55+
+ catch (error) {
56+
+ if (attempts === 2 || !this.isUniqueConstraintError(error)) {
57+
+ throw error;
58+
+ }
59+
+ }
60+
}
61+
/**
62+
* Construct a new API Token instance
63+
@@ -337,7 +347,8 @@ class JWTGuard extends Base_1.BaseGuard {
64+
}
65+
let accessTokenBuilder = new jose_1.SignJWT({ data: payload, ...payload })
66+
.setProtectedHeader({ alg: this.config.algorithmJwt || "RS256", typ: "JWT" })
67+
- .setIssuedAt();
68+
+ .setIssuedAt()
69+
+ .setJti((0, uuid_1.v4)());
70+
if (this.config.issuer) {
71+
accessTokenBuilder = accessTokenBuilder.setIssuer(this.config.issuer);
72+
}
73+
@@ -377,6 +388,17 @@ class JWTGuard extends Base_1.BaseGuard {
74+
generateHash(token) {
75+
return (0, crypto_1.createHash)("sha256").update(token).digest("hex");
76+
}
77+
+ /**
78+
+ * Detect duplicate token persistence errors across supported databases.
79+
+ */
80+
+ isUniqueConstraintError(error) {
81+
+ const code = error?.code;
82+
+ const message = error?.message;
83+
+ return code === "SQLITE_CONSTRAINT" || code === "ER_DUP_ENTRY" || code === "23505"
84+
+ || (typeof message === "string" && (message.includes("UNIQUE constraint failed")
85+
+ || message.includes("duplicate key value violates unique constraint")
86+
+ || message.includes("Duplicate entry")));
87+
+ }
88+
/**
89+
* Converts expiry duration to an absolute date/time value
90+
*/

pnpm-lock.yaml

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { test } from '@japa/runner';
2+
import UserFactory from 'Database/factories/UserFactory';
3+
import crypto from 'node:crypto';
4+
5+
function createLegacyApiPassword(password: string) {
6+
return crypto.createHash('sha256').update(password).digest('base64');
7+
}
8+
9+
function createBasicAuthHeader(email: string, password: string) {
10+
return `Basic ${Buffer.from(`${email}:${password}`).toString('base64')}`;
11+
}
12+
13+
test.group('API / Auth token uniqueness', () => {
14+
test('returns unique persisted JWTs for repeated login requests', async ({
15+
client,
16+
assert,
17+
}) => {
18+
const user = await UserFactory.create();
19+
const authorization = createBasicAuthHeader(
20+
user.email,
21+
createLegacyApiPassword('password'),
22+
);
23+
24+
const [firstResponse, secondResponse] = await Promise.all([
25+
client.post('/v1/auth/login').header('Authorization', authorization),
26+
client.post('/v1/auth/login').header('Authorization', authorization),
27+
]);
28+
29+
firstResponse.assertStatus(200);
30+
secondResponse.assertStatus(200);
31+
32+
const firstBody = JSON.parse(firstResponse.text());
33+
const secondBody = JSON.parse(secondResponse.text());
34+
35+
assert.notEqual(firstBody.token, secondBody.token);
36+
});
37+
38+
test('returns unique persisted JWTs for repeated new token requests', async ({
39+
client,
40+
assert,
41+
}) => {
42+
const user = await UserFactory.create();
43+
const loginResponse = await client
44+
.post('/v1/auth/login')
45+
.header(
46+
'Authorization',
47+
createBasicAuthHeader(user.email, createLegacyApiPassword('password')),
48+
);
49+
50+
loginResponse.assertStatus(200);
51+
const { token } = JSON.parse(loginResponse.text());
52+
53+
const [firstResponse, secondResponse] = await Promise.all([
54+
client
55+
.get('/v1/me/newtoken')
56+
.header('Authorization', `Bearer ${token}`),
57+
client
58+
.get('/v1/me/newtoken')
59+
.header('Authorization', `Bearer ${token}`),
60+
]);
61+
62+
firstResponse.assertStatus(200);
63+
secondResponse.assertStatus(200);
64+
65+
const firstBody = JSON.parse(firstResponse.text());
66+
const secondBody = JSON.parse(secondResponse.text());
67+
68+
assert.notEqual(firstBody.token, secondBody.token);
69+
});
70+
});

0 commit comments

Comments
 (0)