mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-11 10:27:01 +02:00
feat: use cookies for authentication
This commit is contained in:
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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")
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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 },
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class RefreshAccessTokenDTO {
|
||||
@IsNotEmpty()
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ClassSerializerInterceptor, ValidationPipe } from "@nestjs/common";
|
||||
import { NestFactory, Reflector } from "@nestjs/core";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import * as fs from "fs";
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
@@ -9,6 +10,7 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||
|
||||
app.use(cookieParser());
|
||||
app.set("trust proxy", true);
|
||||
|
||||
await fs.promises.mkdir("./data/uploads/_temp", { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user