Files
pingvin-share/backend/src/auth/authTotp.service.ts
2024-11-23 18:55:47 +01:00

168 lines
4.3 KiB
TypeScript

import {
BadRequestException,
ForbiddenException,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { User } from "@prisma/client";
import { authenticator, totp } from "otplib";
import * as qrcode from "qrcode-svg";
import { ConfigService } from "src/config/config.service";
import { PrismaService } from "src/prisma/prisma.service";
import { AuthService } from "./auth.service";
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
@Injectable()
export class AuthTotpService {
constructor(
private prisma: PrismaService,
private configService: ConfigService,
private authService: AuthService,
) {}
async signInTotp(dto: AuthSignInTotpDTO) {
const token = await this.prisma.loginToken.findFirst({
where: {
token: dto.loginToken,
},
include: {
user: true,
},
});
if (!token || token.used)
throw new UnauthorizedException("Invalid login token");
if (token.expiresAt < new Date())
throw new UnauthorizedException("Login token expired", "token_expired");
// Check the TOTP code
const { totpSecret } = token.user;
if (!totpSecret) {
throw new BadRequestException("TOTP is not enabled");
}
if (!authenticator.check(dto.totp, totpSecret)) {
throw new BadRequestException("Invalid code");
}
// Set the login token to used
await this.prisma.loginToken.update({
where: { token: token.token },
data: { used: true },
});
const { refreshToken, refreshTokenId } =
await this.authService.createRefreshToken(token.user.id);
const accessToken = await this.authService.createAccessToken(
token.user,
refreshTokenId,
);
return { accessToken, refreshToken };
}
async enableTotp(user: User, password: string) {
if (!this.authService.verifyPassword(user, password))
throw new ForbiddenException("Invalid password");
// Check if we have a secret already
const { totpVerified } = await this.prisma.user.findUnique({
where: { id: user.id },
select: { totpVerified: true },
});
if (totpVerified) {
throw new BadRequestException("TOTP is already enabled");
}
const issuer = this.configService.get("general.appName");
const secret = authenticator.generateSecret();
const otpURL = totp.keyuri(user.username || user.email, issuer, secret);
await this.prisma.user.update({
where: { id: user.id },
data: {
totpEnabled: true,
totpSecret: secret,
},
});
// TODO: Maybe we should generate the QR code on the client rather than the server?
const qrCode = new qrcode({
content: otpURL,
container: "svg-viewbox",
join: true,
}).svg();
return {
totpAuthUrl: otpURL,
totpSecret: secret,
qrCode:
"data:image/svg+xml;base64," + Buffer.from(qrCode).toString("base64"),
};
}
async verifyTotp(user: User, password: string, code: string) {
if (!this.authService.verifyPassword(user, password))
throw new ForbiddenException("Invalid password");
const { totpSecret } = await this.prisma.user.findUnique({
where: { id: user.id },
select: { totpSecret: true },
});
if (!totpSecret) {
throw new BadRequestException("TOTP is not in progress");
}
const expected = authenticator.generate(totpSecret);
if (code !== expected) {
throw new BadRequestException("Invalid code");
}
await this.prisma.user.update({
where: { id: user.id },
data: {
totpVerified: true,
},
});
return true;
}
async disableTotp(user: User, password: string, code: string) {
if (!this.authService.verifyPassword(user, password))
throw new ForbiddenException("Invalid password");
const { totpSecret } = await this.prisma.user.findUnique({
where: { id: user.id },
select: { totpSecret: true },
});
if (!totpSecret) {
throw new BadRequestException("TOTP is not enabled");
}
const expected = authenticator.generate(totpSecret);
if (code !== expected) {
throw new BadRequestException("Invalid code");
}
await this.prisma.user.update({
where: { id: user.id },
data: {
totpVerified: false,
totpEnabled: false,
totpSecret: null,
},
});
return true;
}
}