Skip to content

Commit fe13b12

Browse files
authored
Merge pull request #6 from mkumar1205-beep/pbctf-dashboard
Refresh token fix
2 parents 93947a8 + 53c42e8 commit fe13b12

3 files changed

Lines changed: 175 additions & 157 deletions

File tree

app/api/pbctf/refresh/route.ts

Lines changed: 162 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,207 @@
11
import { cookies } from "next/headers";
2-
import { NextResponse } from "next/server";
2+
import { NextRequest, NextResponse } from "next/server";
33
import crypto from "crypto";
44
import connectDb from "@/lib/db/connection";
55
import PBCTFRefreshToken from "@/lib/db/models/CTFRefreshToken";
66
import generateAccessToken from "@/lib/pbctf/generateAccessToken";
77
import generateRefreshToken from "@/lib/pbctf/generateRefreshToken";
88

9-
export async function POST() {
10-
try {
11-
await connectDb();
12-
const cookieStore = await cookies();
13-
const refreshToken =
14-
cookieStore.get("pbctf_refresh")?.value;
15-
16-
if (!refreshToken) {
17-
return NextResponse.json(
18-
{
19-
error: "Refresh token missing",
20-
},
21-
{
22-
status: 401,
23-
}
24-
);
9+
type RefreshResult =
10+
| {
11+
success: true;
12+
accessToken: string;
13+
refreshToken: string;
2514
}
15+
| {
16+
success: false;
17+
status: number;
18+
error: string;
19+
};
20+
21+
const cookieOptions = {
22+
httpOnly: true,
23+
secure: process.env.NODE_ENV === "production",
24+
sameSite: "strict" as const,
25+
path: "/",
26+
};
27+
28+
function clearAuthCookies(response: NextResponse) {
29+
response.cookies.set("pbctf_access", "", {
30+
...cookieOptions,
31+
expires: new Date(0),
32+
});
33+
34+
response.cookies.set("pbctf_refresh", "", {
35+
...cookieOptions,
36+
expires: new Date(0),
37+
});
2638

27-
const tokenHash = crypto
28-
.createHash("sha256")
29-
.update(refreshToken)
30-
.digest("hex");
31-
32-
const matchedToken =
33-
await PBCTFRefreshToken.findOne({
34-
tokenHash,
35-
});
36-
37-
if (!matchedToken) {
38-
const response =
39-
NextResponse.json(
40-
{
41-
error:
42-
"Invalid refresh token",
43-
},
44-
{
45-
status: 401,
46-
}
47-
);
39+
return response;
40+
}
4841

49-
response.cookies.set(
50-
"pbctf_access",
51-
"",
52-
{
53-
expires: new Date(0),
54-
path: "/",
55-
httpOnly: true,
56-
sameSite: "strict",
57-
secure:
58-
process.env.NODE_ENV ===
59-
"production",
60-
}
61-
);
42+
function setAuthCookies(
43+
response: NextResponse,
44+
accessToken: string,
45+
refreshToken: string
46+
) {
47+
response.cookies.set("pbctf_access", accessToken, {
48+
...cookieOptions,
49+
maxAge: 60 * 10,
50+
});
6251

63-
response.cookies.set(
64-
"pbctf_refresh",
65-
"",
66-
{
67-
expires: new Date(0),
68-
path: "/",
69-
httpOnly: true,
70-
sameSite: "strict",
71-
secure:
72-
process.env.NODE_ENV ===
73-
"production",
74-
}
75-
);
52+
response.cookies.set("pbctf_refresh", refreshToken, {
53+
...cookieOptions,
54+
maxAge: 24 * 60 * 60,
55+
});
7656

7757
return response;
7858
}
7959

80-
if (
81-
matchedToken.expiresAt <
82-
new Date()
83-
) {
84-
await matchedToken.deleteOne();
60+
async function rotateRefreshToken(): Promise<RefreshResult> {
61+
await connectDb();
8562

86-
const response =
87-
NextResponse.json(
88-
{
89-
error:
90-
"Refresh token expired",
91-
},
92-
{
93-
status: 401,
94-
}
95-
);
63+
const cookieStore = await cookies();
64+
const refreshToken =
65+
cookieStore.get("pbctf_refresh")?.value;
9666

97-
response.cookies.set(
98-
"pbctf_access",
99-
"",
100-
{
101-
expires: new Date(0),
102-
path: "/",
103-
httpOnly: true,
104-
sameSite: "strict",
105-
secure:
106-
process.env.NODE_ENV ===
107-
"production",
108-
}
109-
);
67+
if (!refreshToken) {
68+
return {
69+
success: false,
70+
status: 401,
71+
error: "Refresh token missing",
72+
};
73+
}
11074

111-
response.cookies.set(
112-
"pbctf_refresh",
113-
"",
114-
{
115-
expires: new Date(0),
116-
path: "/",
117-
httpOnly: true,
118-
sameSite: "strict",
119-
secure:
120-
process.env.NODE_ENV ===
121-
"production",
122-
}
75+
const tokenHash = crypto
76+
.createHash("sha256")
77+
.update(refreshToken)
78+
.digest("hex");
79+
80+
const matchedToken =
81+
await PBCTFRefreshToken.findOne({
82+
tokenHash,
83+
});
84+
85+
if (!matchedToken) {
86+
return {
87+
success: false,
88+
status: 401,
89+
error: "Invalid refresh token",
90+
};
91+
}
92+
93+
if (matchedToken.expiresAt < new Date()) {
94+
await matchedToken.deleteOne();
95+
96+
return {
97+
success: false,
98+
status: 401,
99+
error: "Refresh token expired",
100+
};
101+
}
102+
103+
const newAccessToken =
104+
generateAccessToken(matchedToken.email);
105+
const newRefreshToken =
106+
generateRefreshToken();
107+
108+
const newRefreshHash = crypto
109+
.createHash("sha256")
110+
.update(newRefreshToken)
111+
.digest("hex");
112+
113+
matchedToken.tokenHash = newRefreshHash;
114+
matchedToken.expiresAt = new Date(
115+
Date.now() + 24 * 60 * 60 * 1000
123116
);
124117

125-
return response;
118+
await matchedToken.save();
119+
120+
return {
121+
success: true,
122+
accessToken: newAccessToken,
123+
refreshToken: newRefreshToken,
124+
};
126125
}
127126

128-
const newAccessToken =
129-
generateAccessToken(
130-
matchedToken.email
131-
);
127+
function getSafeNextUrl(request: NextRequest) {
128+
const next =
129+
request.nextUrl.searchParams.get("next") ||
130+
"/pbctf/dashboard";
132131

133-
const newRefreshToken =
134-
generateRefreshToken();
132+
if (!next.startsWith("/") || next.startsWith("//")) {
133+
return "/pbctf/dashboard";
134+
}
135135

136-
const newRefreshHash = crypto
137-
.createHash("sha256")
138-
.update(newRefreshToken)
139-
.digest("hex");
136+
return next;
137+
}
140138

141-
matchedToken.tokenHash =
142-
newRefreshHash;
139+
export async function GET(request: NextRequest) {
140+
try {
141+
const result = await rotateRefreshToken();
143142

144-
matchedToken.expiresAt =
145-
new Date(
146-
Date.now() + 24 * 60 * 60 * 1000
143+
if (!result.success) {
144+
const response = NextResponse.redirect(
145+
new URL("/pbctf/login", request.url)
147146
);
148147

149-
await matchedToken.save();
148+
return clearAuthCookies(response);
149+
}
150150

151-
const response =
152-
NextResponse.json({
153-
success: true,
154-
});
151+
const response = NextResponse.redirect(
152+
new URL(getSafeNextUrl(request), request.url)
153+
);
155154

156-
response.cookies.set(
157-
"pbctf_access",
158-
newAccessToken,
159-
{
160-
httpOnly: true,
161-
secure:
162-
process.env.NODE_ENV ===
163-
"production",
164-
sameSite: "strict",
165-
path: "/",
166-
maxAge: 60 * 10,
167-
}
155+
return setAuthCookies(
156+
response,
157+
result.accessToken,
158+
result.refreshToken
168159
);
160+
} catch (error) {
161+
console.error(error);
169162

170-
response.cookies.set(
171-
"pbctf_refresh",
172-
newRefreshToken,
173-
{
174-
httpOnly: true,
175-
secure:
176-
process.env.NODE_ENV ===
177-
"production",
178-
sameSite: "strict",
179-
path: "/",
180-
maxAge:
181-
24 * 60 * 60,
182-
}
163+
return NextResponse.redirect(
164+
new URL("/pbctf/login", request.url)
183165
);
166+
}
167+
}
184168

185-
return response;
169+
export async function POST() {
170+
try {
171+
const result = await rotateRefreshToken();
172+
173+
if (!result.success) {
174+
const response = NextResponse.json(
175+
{
176+
error: result.error,
177+
},
178+
{
179+
status: result.status,
180+
}
181+
);
182+
183+
return clearAuthCookies(response);
184+
}
185+
186+
const response = NextResponse.json({
187+
success: true,
188+
});
189+
190+
return setAuthCookies(
191+
response,
192+
result.accessToken,
193+
result.refreshToken
194+
);
186195
} catch (error) {
187196
console.error(error);
188197

189198
return NextResponse.json(
190199
{
191-
error:
192-
"Internal server error",
200+
error: "Internal server error",
193201
},
194202
{
195203
status: 500,
196204
}
197205
);
198206
}
199-
}
207+
}

app/pbctf/dashboard/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export default async function DashboardPage() {
1111
const user = await getCurrentUser();
1212

1313
if (!user) {
14-
redirect("/pbctf/login");
14+
redirect(
15+
"/api/pbctf/refresh?next=/pbctf/dashboard"
16+
);
1517
}
1618
const {registration, isLeader} = user;
1719

@@ -114,4 +116,4 @@ export default async function DashboardPage() {
114116
</div>
115117
</div>
116118
</div>
117-
);}
119+
);}

0 commit comments

Comments
 (0)