Skip to content
This repository was archived by the owner on Jun 29, 2025. It is now read-only.

Commit faea1ab

Browse files
committed
feat: use cookies for authentication
1 parent 71658ad commit faea1ab

File tree

12 files changed

+193
-74
lines changed

12 files changed

+193
-74
lines changed

backend/package-lock.json

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"class-transformer": "^0.5.1",
2929
"class-validator": "^0.13.2",
3030
"content-disposition": "^0.5.4",
31+
"cookie-parser": "^1.4.6",
3132
"mime-types": "^2.1.35",
3233
"moment": "^2.29.4",
3334
"multer": "^1.4.5-lts.1",
@@ -47,6 +48,7 @@
4748
"@nestjs/schematics": "^9.0.3",
4849
"@nestjs/testing": "^9.2.1",
4950
"@types/archiver": "^5.3.1",
51+
"@types/cookie-parser": "^1.4.3",
5052
"@types/cron": "^2.0.0",
5153
"@types/express": "^4.17.14",
5254
"@types/mime-types": "^2.1.1",

backend/prisma/schema.prisma

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ model User {
2727
}
2828

2929
model RefreshToken {
30-
token String @id @default(uuid())
30+
id String @id @default(uuid())
31+
token String @unique @default(uuid())
3132
createdAt DateTime @default(now())
3233
3334
expiresAt DateTime

backend/src/auth/auth.controller.ts

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import {
55
HttpCode,
66
Patch,
77
Post,
8+
Req,
9+
Res,
10+
UnauthorizedException,
811
UseGuards,
912
} from "@nestjs/common";
1013
import { Throttle } from "@nestjs/throttler";
1114
import { User } from "@prisma/client";
15+
import { Request, Response } from "express";
1216
import { ConfigService } from "src/config/config.service";
1317
import { AuthService } from "./auth.service";
1418
import { AuthTotpService } from "./authTotp.service";
@@ -17,7 +21,6 @@ import { AuthRegisterDTO } from "./dto/authRegister.dto";
1721
import { AuthSignInDTO } from "./dto/authSignIn.dto";
1822
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
1923
import { EnableTotpDTO } from "./dto/enableTotp.dto";
20-
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
2124
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
2225
import { VerifyTotpDTO } from "./dto/verifyTotp.dto";
2326
import { JwtGuard } from "./guard/jwt.guard";
@@ -32,24 +35,59 @@ export class AuthController {
3235

3336
@Throttle(10, 5 * 60)
3437
@Post("signUp")
35-
async signUp(@Body() dto: AuthRegisterDTO) {
38+
async signUp(
39+
@Body() dto: AuthRegisterDTO,
40+
@Res({ passthrough: true }) response: Response
41+
) {
3642
if (!this.config.get("ALLOW_REGISTRATION"))
3743
throw new ForbiddenException("Registration is not allowed");
38-
return this.authService.signUp(dto);
44+
const result = await this.authService.signUp(dto);
45+
46+
response = this.addTokensToResponse(
47+
response,
48+
result.accessToken,
49+
result.refreshToken
50+
);
51+
52+
return result;
3953
}
4054

4155
@Throttle(10, 5 * 60)
4256
@Post("signIn")
4357
@HttpCode(200)
44-
signIn(@Body() dto: AuthSignInDTO) {
45-
return this.authService.signIn(dto);
58+
async signIn(
59+
@Body() dto: AuthSignInDTO,
60+
@Res({ passthrough: true }) response: Response
61+
) {
62+
const result = await this.authService.signIn(dto);
63+
64+
if (result.accessToken && result.refreshToken) {
65+
response = this.addTokensToResponse(
66+
response,
67+
result.accessToken,
68+
result.refreshToken
69+
);
70+
}
71+
72+
return result;
4673
}
4774

4875
@Throttle(10, 5 * 60)
4976
@Post("signIn/totp")
5077
@HttpCode(200)
51-
signInTotp(@Body() dto: AuthSignInTotpDTO) {
52-
return this.authTotpService.signInTotp(dto);
78+
async signInTotp(
79+
@Body() dto: AuthSignInTotpDTO,
80+
@Res({ passthrough: true }) response: Response
81+
) {
82+
const result = await this.authTotpService.signInTotp(dto);
83+
84+
response = this.addTokensToResponse(
85+
response,
86+
result.accessToken,
87+
result.refreshToken
88+
);
89+
90+
return result;
5391
}
5492

5593
@Patch("password")
@@ -60,13 +98,33 @@ export class AuthController {
6098

6199
@Post("token")
62100
@HttpCode(200)
63-
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
101+
async refreshAccessToken(
102+
@Req() request: Request,
103+
@Res({ passthrough: true }) response: Response
104+
) {
105+
if (!request.cookies.refresh_token) throw new UnauthorizedException();
106+
64107
const accessToken = await this.authService.refreshAccessToken(
65-
body.refreshToken
108+
request.cookies.refresh_token
66109
);
110+
response.cookie("access_token", accessToken, { httpOnly: true });
67111
return { accessToken };
68112
}
69113

114+
@Post("signOut")
115+
async signOut(
116+
@Req() request: Request,
117+
@Res({ passthrough: true }) response: Response
118+
) {
119+
await this.authService.signOut(request.cookies.access_token);
120+
response.cookie("access_token", "accessToken", { maxAge: -1 });
121+
response.cookie("refresh_token", "", {
122+
path: "/api/auth/token",
123+
httpOnly: true,
124+
maxAge: -1,
125+
});
126+
}
127+
70128
@Post("totp/enable")
71129
@UseGuards(JwtGuard)
72130
async enableTotp(@GetUser() user: User, @Body() body: EnableTotpDTO) {
@@ -85,4 +143,19 @@ export class AuthController {
85143
// Note: We use VerifyTotpDTO here because it has both fields we need: password and totp code
86144
return this.authTotpService.disableTotp(user, body.password, body.code);
87145
}
146+
147+
private addTokensToResponse(
148+
response: Response,
149+
accessToken: string,
150+
refreshToken: string
151+
) {
152+
response.cookie("access_token", accessToken);
153+
response.cookie("refresh_token", refreshToken, {
154+
path: "/api/auth/token",
155+
httpOnly: true,
156+
maxAge: 60 * 60 * 24 * 30 * 3,
157+
});
158+
159+
return response;
160+
}
88161
}

backend/src/auth/auth.service.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export class AuthService {
3434
},
3535
});
3636

37-
const accessToken = await this.createAccessToken(user);
38-
const refreshToken = await this.createRefreshToken(user.id);
37+
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
38+
user.id
39+
);
40+
const accessToken = await this.createAccessToken(user, refreshTokenId);
3941

4042
return { accessToken, refreshToken };
4143
} catch (e) {
@@ -71,8 +73,10 @@ export class AuthService {
7173
return { loginToken };
7274
}
7375

74-
const accessToken = await this.createAccessToken(user);
75-
const refreshToken = await this.createRefreshToken(user.id);
76+
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
77+
user.id
78+
);
79+
const accessToken = await this.createAccessToken(user, refreshTokenId);
7680

7781
return { accessToken, refreshToken };
7882
}
@@ -89,11 +93,12 @@ export class AuthService {
8993
});
9094
}
9195

92-
async createAccessToken(user: User) {
96+
async createAccessToken(user: User, refreshTokenId: string) {
9397
return this.jwtService.sign(
9498
{
9599
sub: user.id,
96100
email: user.email,
101+
refreshTokenId,
97102
},
98103
{
99104
expiresIn: "15min",
@@ -102,6 +107,14 @@ export class AuthService {
102107
);
103108
}
104109

110+
async signOut(accessToken: string) {
111+
const { refreshTokenId } = this.jwtService.decode(accessToken) as {
112+
refreshTokenId: string;
113+
};
114+
115+
await this.prisma.refreshToken.delete({ where: { id: refreshTokenId } });
116+
}
117+
105118
async refreshAccessToken(refreshToken: string) {
106119
const refreshTokenMetaData = await this.prisma.refreshToken.findUnique({
107120
where: { token: refreshToken },
@@ -111,17 +124,18 @@ export class AuthService {
111124
if (!refreshTokenMetaData || refreshTokenMetaData.expiresAt < new Date())
112125
throw new UnauthorizedException();
113126

114-
return this.createAccessToken(refreshTokenMetaData.user);
127+
return this.createAccessToken(
128+
refreshTokenMetaData.user,
129+
refreshTokenMetaData.id
130+
);
115131
}
116132

117133
async createRefreshToken(userId: string) {
118-
const refreshToken = (
119-
await this.prisma.refreshToken.create({
120-
data: { userId, expiresAt: moment().add(3, "months").toDate() },
121-
})
122-
).token;
134+
const { id, token } = await this.prisma.refreshToken.create({
135+
data: { userId, expiresAt: moment().add(3, "months").toDate() },
136+
});
123137

124-
return refreshToken;
138+
return { refreshTokenId: id, refreshToken: token };
125139
}
126140

127141
async createLoginToken(userId: string) {

backend/src/auth/authTotp.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ export class AuthTotpService {
7171
data: { used: true },
7272
});
7373

74-
const accessToken = await this.authService.createAccessToken(user);
75-
const refreshToken = await this.authService.createRefreshToken(user.id);
74+
const { refreshToken, refreshTokenId } =
75+
await this.authService.createRefreshToken(user.id);
76+
const accessToken = await this.authService.createAccessToken(
77+
user,
78+
refreshTokenId
79+
);
7680

7781
return { accessToken, refreshToken };
7882
}

backend/src/auth/dto/refreshAccessToken.dto.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

backend/src/auth/strategy/jwt.strategy.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Injectable } from "@nestjs/common";
22
import { PassportStrategy } from "@nestjs/passport";
33
import { User } from "@prisma/client";
4-
import { ExtractJwt, Strategy } from "passport-jwt";
4+
import { Request } from "express";
5+
import { Strategy } from "passport-jwt";
56
import { ConfigService } from "src/config/config.service";
67
import { PrismaService } from "src/prisma/prisma.service";
78

@@ -10,11 +11,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
1011
constructor(config: ConfigService, private prisma: PrismaService) {
1112
config.get("JWT_SECRET");
1213
super({
13-
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
14+
jwtFromRequest: JwtStrategy.extractJWT,
1415
secretOrKey: config.get("JWT_SECRET"),
1516
});
1617
}
1718

19+
private static extractJWT(req: Request) {
20+
if (!req.cookies.access_token) return null;
21+
return req.cookies.access_token;
22+
}
23+
1824
async validate(payload: { sub: string }) {
1925
const user: User = await this.prisma.user.findUnique({
2026
where: { id: payload.sub },

0 commit comments

Comments
 (0)