Skip to content

Commit 7aaec29

Browse files
committed
fix: Refactor auth payloads and renew token once per request
1 parent 221f574 commit 7aaec29

File tree

5 files changed

+45
-56
lines changed

5 files changed

+45
-56
lines changed

src/middleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { withAuth } from 'next-auth/middleware';
22
import { type JWT } from 'next-auth/jwt';
3+
import { parseJwt } from '@/utils/jwt';
34

45
const isAdmin = (token: JWT | null) => token?.user.permissions.is_admin ?? false;
56
const isStaff = (token: JWT | null) => token?.user.permissions.is_staff ?? false;
@@ -41,7 +42,7 @@ export default withAuth({
4142
const now = Date.now() / 1000;
4243

4344
// Check if refresh token is still valid
44-
if (token && now > token.account.refresh_token_exp) {
45+
if (token && now > parseJwt(token.refreshToken).exp) {
4546
return false;
4647
}
4748

src/types/next-auth.d.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/* eslint-disable */
2-
import { type Account } from 'next-auth';
1+
import 'next-auth';
32

43
type UserId = {
54
first_name: string;
@@ -21,28 +20,18 @@ declare module 'next-auth' {
2120

2221
interface Profile extends UserId { }
2322

24-
interface Account {
25-
provider: 'vatsim';
26-
type: 'oauth';
27-
providerAccountId: string;
28-
access_token: string;
29-
access_token_exp: number;
30-
refresh_token: string;
31-
refresh_token_exp: number;
32-
}
33-
3423
interface Session {
3524
user: Profile;
36-
access_token: string;
37-
refresh_token: string;
25+
accessToken: string;
3826
}
3927
}
4028

41-
declare module "next-auth/jwt" {
29+
declare module 'next-auth/jwt' {
4230
interface User extends UserId { }
4331

4432
interface JWT {
4533
user: User;
46-
account: Account;
34+
accessToken: string;
35+
refreshToken: string;
4736
}
4837
}

src/utils/auth.ts

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AuthOptions } from 'next-auth';
2+
import { parseJwt } from '@/utils/jwt';
23
import type { UserId } from '@/types/next-auth';
34

45
type RefreshedTokens = {
@@ -7,19 +8,7 @@ type RefreshedTokens = {
78
profile: UserId;
89
}
910

10-
type JWTPayload = {
11-
token_type: 'access' | 'refresh';
12-
exp: number;
13-
iat: number;
14-
jti: string;
15-
user_id: number;
16-
}
17-
18-
function parseJwt(token: string): JWTPayload {
19-
const base64Url = token.split('.')[1];
20-
const base64 = base64Url.replace('-', '+').replace('_', '/');
21-
return JSON.parse(atob(base64));
22-
}
11+
const refreshTokenPromiseCache: { [key: string]: Promise<Response> } = {};
2312

2413
export const authOptions: AuthOptions = {
2514
session: {
@@ -34,7 +23,7 @@ export const authOptions: AuthOptions = {
3423
return {
3524
...session,
3625
user: token.user,
37-
access_token: token.account.access_token,
26+
accessToken: token.accessToken,
3827
};
3928
}
4029
return session;
@@ -47,39 +36,35 @@ export const authOptions: AuthOptions = {
4736

4837
return {
4938
user: user as UserId,
50-
account: {
51-
...account,
52-
access_token_exp: parseJwt(account.access_token).exp,
53-
refresh_token_exp: parseJwt(account.refresh_token).exp,
54-
},
39+
accessToken: account.access_token!,
40+
refreshToken: account.refresh_token!,
5541
};
5642
}
5743

5844
// Access token is expired :(
59-
if (Date.now() / 1000 > token.account.access_token_exp) {
60-
const body = JSON.stringify({ refresh: token.account.refresh_token });
45+
if (Date.now() / 1000 > parseJwt(token.accessToken).exp) {
46+
const body = JSON.stringify({ refresh: token.refreshToken });
47+
48+
const tokenId = parseJwt(token.refreshToken).jti;
6149

6250
// Obtain new token pair and new profile data
63-
const { access, refresh, profile } = await fetch(
64-
`${process.env.NEXT_PUBLIC_API_URL}/auth/token/refresh/`,
65-
{
66-
method: 'POST',
67-
headers: { 'Content-Type': 'application/json' },
68-
body,
69-
},
70-
)
71-
.then<RefreshedTokens>((resp) => resp.json());
51+
if (!refreshTokenPromiseCache[tokenId]) {
52+
refreshTokenPromiseCache[tokenId] = fetch(
53+
`${process.env.NEXT_PUBLIC_API_URL}/auth/token/refresh/`,
54+
{
55+
method: 'POST',
56+
headers: { 'Content-Type': 'application/json' },
57+
body,
58+
},
59+
);
60+
}
61+
const resp = await refreshTokenPromiseCache[tokenId];
62+
const { access, refresh, profile } = await resp.json() as RefreshedTokens;
7263

7364
return {
74-
...token,
7565
user: profile,
76-
account: {
77-
...token.account,
78-
access_token: access,
79-
access_token_exp: parseJwt(access).exp,
80-
refresh_token: refresh,
81-
refresh_token_exp: parseJwt(refresh).exp,
82-
},
66+
accessToken: access,
67+
refreshToken: refresh,
8368
};
8469
}
8570

src/utils/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function fetchApi<T extends Record<string, unknown> | unknown[]>(ro
3232
}
3333

3434
if (session) {
35-
headers.append('Authorization', `Bearer ${session.access_token}`);
35+
headers.append('Authorization', `Bearer ${session.accessToken}`);
3636
}
3737

3838
return fetch(`${process.env.NEXT_PUBLIC_API_URL}/api${route}`, { ...config, headers })

src/utils/jwt.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type JWTPayload = {
2+
token_type: 'access' | 'refresh';
3+
exp: number;
4+
iat: number;
5+
jti: string;
6+
user_id: number;
7+
}
8+
9+
export function parseJwt(token: string): JWTPayload {
10+
const payload = token.split('.')[1];
11+
const text = Buffer.from(payload, 'base64').toString();
12+
13+
return JSON.parse(text);
14+
}

0 commit comments

Comments
 (0)