Skip to content

Commit

Permalink
feat: use cookies for authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Jan 4, 2023
1 parent 71658ad commit faea1ab
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 74 deletions.
56 changes: 56 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"content-disposition": "^0.5.4",
"cookie-parser": "^1.4.6",
"mime-types": "^2.1.35",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
Expand All @@ -47,6 +48,7 @@
"@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.2.1",
"@types/archiver": "^5.3.1",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.0",
"@types/express": "^4.17.14",
"@types/mime-types": "^2.1.1",
Expand Down
3 changes: 2 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ model User {
}

model RefreshToken {
token String @id @default(uuid())
id String @id @default(uuid())
token String @unique @default(uuid())
createdAt DateTime @default(now())
expiresAt DateTime
Expand Down
91 changes: 82 additions & 9 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import {
HttpCode,
Patch,
Post,
Req,
Res,
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import { User } from "@prisma/client";
import { Request, Response } from "express";
import { ConfigService } from "src/config/config.service";
import { AuthService } from "./auth.service";
import { AuthTotpService } from "./authTotp.service";
Expand All @@ -17,7 +21,6 @@ import { AuthRegisterDTO } from "./dto/authRegister.dto";
import { AuthSignInDTO } from "./dto/authSignIn.dto";
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
import { EnableTotpDTO } from "./dto/enableTotp.dto";
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
import { VerifyTotpDTO } from "./dto/verifyTotp.dto";
import { JwtGuard } from "./guard/jwt.guard";
Expand All @@ -32,24 +35,59 @@ export class AuthController {

@Throttle(10, 5 * 60)
@Post("signUp")
async signUp(@Body() dto: AuthRegisterDTO) {
async signUp(
@Body() dto: AuthRegisterDTO,
@Res({ passthrough: true }) response: Response
) {
if (!this.config.get("ALLOW_REGISTRATION"))
throw new ForbiddenException("Registration is not allowed");
return this.authService.signUp(dto);
const result = await this.authService.signUp(dto);

response = this.addTokensToResponse(
response,
result.accessToken,
result.refreshToken
);

return result;
}

@Throttle(10, 5 * 60)
@Post("signIn")
@HttpCode(200)
signIn(@Body() dto: AuthSignInDTO) {
return this.authService.signIn(dto);
async signIn(
@Body() dto: AuthSignInDTO,
@Res({ passthrough: true }) response: Response
) {
const result = await this.authService.signIn(dto);

if (result.accessToken && result.refreshToken) {
response = this.addTokensToResponse(
response,
result.accessToken,
result.refreshToken
);
}

return result;
}

@Throttle(10, 5 * 60)
@Post("signIn/totp")
@HttpCode(200)
signInTotp(@Body() dto: AuthSignInTotpDTO) {
return this.authTotpService.signInTotp(dto);
async signInTotp(
@Body() dto: AuthSignInTotpDTO,
@Res({ passthrough: true }) response: Response
) {
const result = await this.authTotpService.signInTotp(dto);

response = this.addTokensToResponse(
response,
result.accessToken,
result.refreshToken
);

return result;
}

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

@Post("token")
@HttpCode(200)
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
async refreshAccessToken(
@Req() request: Request,
@Res({ passthrough: true }) response: Response
) {
if (!request.cookies.refresh_token) throw new UnauthorizedException();

const accessToken = await this.authService.refreshAccessToken(
body.refreshToken
request.cookies.refresh_token
);
response.cookie("access_token", accessToken, { httpOnly: true });
return { accessToken };
}

@Post("signOut")
async signOut(
@Req() request: Request,
@Res({ passthrough: true }) response: Response
) {
await this.authService.signOut(request.cookies.access_token);
response.cookie("access_token", "accessToken", { maxAge: -1 });
response.cookie("refresh_token", "", {
path: "/api/auth/token",
httpOnly: true,
maxAge: -1,
});
}

@Post("totp/enable")
@UseGuards(JwtGuard)
async enableTotp(@GetUser() user: User, @Body() body: EnableTotpDTO) {
Expand All @@ -85,4 +143,19 @@ export class AuthController {
// Note: We use VerifyTotpDTO here because it has both fields we need: password and totp code
return this.authTotpService.disableTotp(user, body.password, body.code);
}

private addTokensToResponse(
response: Response,
accessToken: string,
refreshToken: string
) {
response.cookie("access_token", accessToken);
response.cookie("refresh_token", refreshToken, {
path: "/api/auth/token",
httpOnly: true,
maxAge: 60 * 60 * 24 * 30 * 3,
});

return response;
}
}
38 changes: 26 additions & 12 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export class AuthService {
},
});

const accessToken = await this.createAccessToken(user);
const refreshToken = await this.createRefreshToken(user.id);
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
user.id
);
const accessToken = await this.createAccessToken(user, refreshTokenId);

return { accessToken, refreshToken };
} catch (e) {
Expand Down Expand Up @@ -71,8 +73,10 @@ export class AuthService {
return { loginToken };
}

const accessToken = await this.createAccessToken(user);
const refreshToken = await this.createRefreshToken(user.id);
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
user.id
);
const accessToken = await this.createAccessToken(user, refreshTokenId);

return { accessToken, refreshToken };
}
Expand All @@ -89,11 +93,12 @@ export class AuthService {
});
}

async createAccessToken(user: User) {
async createAccessToken(user: User, refreshTokenId: string) {
return this.jwtService.sign(
{
sub: user.id,
email: user.email,
refreshTokenId,
},
{
expiresIn: "15min",
Expand All @@ -102,6 +107,14 @@ export class AuthService {
);
}

async signOut(accessToken: string) {
const { refreshTokenId } = this.jwtService.decode(accessToken) as {
refreshTokenId: string;
};

await this.prisma.refreshToken.delete({ where: { id: refreshTokenId } });
}

async refreshAccessToken(refreshToken: string) {
const refreshTokenMetaData = await this.prisma.refreshToken.findUnique({
where: { token: refreshToken },
Expand All @@ -111,17 +124,18 @@ export class AuthService {
if (!refreshTokenMetaData || refreshTokenMetaData.expiresAt < new Date())
throw new UnauthorizedException();

return this.createAccessToken(refreshTokenMetaData.user);
return this.createAccessToken(
refreshTokenMetaData.user,
refreshTokenMetaData.id
);
}

async createRefreshToken(userId: string) {
const refreshToken = (
await this.prisma.refreshToken.create({
data: { userId, expiresAt: moment().add(3, "months").toDate() },
})
).token;
const { id, token } = await this.prisma.refreshToken.create({
data: { userId, expiresAt: moment().add(3, "months").toDate() },
});

return refreshToken;
return { refreshTokenId: id, refreshToken: token };
}

async createLoginToken(userId: string) {
Expand Down
8 changes: 6 additions & 2 deletions backend/src/auth/authTotp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ export class AuthTotpService {
data: { used: true },
});

const accessToken = await this.authService.createAccessToken(user);
const refreshToken = await this.authService.createRefreshToken(user.id);
const { refreshToken, refreshTokenId } =
await this.authService.createRefreshToken(user.id);
const accessToken = await this.authService.createAccessToken(
user,
refreshTokenId
);

return { accessToken, refreshToken };
}
Expand Down
6 changes: 0 additions & 6 deletions backend/src/auth/dto/refreshAccessToken.dto.ts

This file was deleted.

10 changes: 8 additions & 2 deletions backend/src/auth/strategy/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { User } from "@prisma/client";
import { ExtractJwt, Strategy } from "passport-jwt";
import { Request } from "express";
import { Strategy } from "passport-jwt";
import { ConfigService } from "src/config/config.service";
import { PrismaService } from "src/prisma/prisma.service";

Expand All @@ -10,11 +11,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService, private prisma: PrismaService) {
config.get("JWT_SECRET");
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
jwtFromRequest: JwtStrategy.extractJWT,
secretOrKey: config.get("JWT_SECRET"),
});
}

private static extractJWT(req: Request) {
if (!req.cookies.access_token) return null;
return req.cookies.access_token;
}

async validate(payload: { sub: string }) {
const user: User = await this.prisma.user.findUnique({
where: { id: payload.sub },
Expand Down
Loading

0 comments on commit faea1ab

Please sign in to comment.