refactor: extract totp operations in seperate service
This commit is contained in:
@@ -13,10 +13,6 @@ import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
||||
import { authenticator, totp } from "otplib";
|
||||
import * as qrcode from "qrcode-svg";
|
||||
import * as crypto from "crypto";
|
||||
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -81,61 +77,6 @@ export class AuthService {
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
async signInTotp(dto: AuthSignInTotpDTO) {
|
||||
if (!dto.email && !dto.username)
|
||||
throw new BadRequestException("Email or username is required");
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email: dto.email }, { username: dto.username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !(await argon.verify(user.password, dto.password)))
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
|
||||
const token = await this.prisma.loginToken.findFirst({
|
||||
where: {
|
||||
token: dto.loginToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token || token.userId != user.id || token.used)
|
||||
throw new UnauthorizedException("Invalid login token");
|
||||
|
||||
if (token.expiresAt < new Date())
|
||||
throw new UnauthorizedException("Login token expired");
|
||||
|
||||
// Check the TOTP code
|
||||
const { totpSecret } = await this.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { totpSecret: true },
|
||||
});
|
||||
|
||||
if (!totpSecret) {
|
||||
throw new BadRequestException("TOTP is not enabled");
|
||||
}
|
||||
|
||||
const decryptedSecret = this.decryptTotpSecret(totpSecret, dto.password);
|
||||
|
||||
const expected = authenticator.generate(decryptedSecret);
|
||||
|
||||
if (dto.totp !== expected) {
|
||||
throw new BadRequestException("Invalid code");
|
||||
}
|
||||
|
||||
// Set the login token to used
|
||||
await this.prisma.loginToken.update({
|
||||
where: { token: token.token },
|
||||
data: { used: true },
|
||||
});
|
||||
|
||||
const accessToken = await this.createAccessToken(user);
|
||||
const refreshToken = await this.createRefreshToken(user.id);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
async updatePassword(user: User, oldPassword: string, newPassword: string) {
|
||||
if (!(await argon.verify(user.password, oldPassword)))
|
||||
throw new ForbiddenException("Invalid password");
|
||||
@@ -192,151 +133,4 @@ export class AuthService {
|
||||
|
||||
return loginToken;
|
||||
}
|
||||
|
||||
encryptTotpSecret(totpSecret: string, password: string) {
|
||||
let iv = this.config.get("TOTP_SECRET");
|
||||
iv = Buffer.from(iv, "base64");
|
||||
const key = crypto
|
||||
.createHash("sha256")
|
||||
.update(String(password))
|
||||
.digest("base64")
|
||||
.substr(0, 32);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
|
||||
let encrypted = cipher.update(totpSecret);
|
||||
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
|
||||
return encrypted.toString("base64");
|
||||
}
|
||||
|
||||
decryptTotpSecret(encryptedTotpSecret: string, password: string) {
|
||||
let iv = this.config.get("TOTP_SECRET");
|
||||
iv = Buffer.from(iv, "base64");
|
||||
const key = crypto
|
||||
.createHash("sha256")
|
||||
.update(String(password))
|
||||
.digest("base64")
|
||||
.substr(0, 32);
|
||||
|
||||
const encryptedText = Buffer.from(encryptedTotpSecret, "base64");
|
||||
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
|
||||
return decrypted.toString();
|
||||
}
|
||||
|
||||
async enableTotp(user: User, password: string) {
|
||||
if (!(await argon.verify(user.password, 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");
|
||||
}
|
||||
|
||||
// TODO: Maybe make the issuer configurable with env vars?
|
||||
const secret = authenticator.generateSecret();
|
||||
const encryptedSecret = this.encryptTotpSecret(secret, password);
|
||||
|
||||
const otpURL = totp.keyuri(
|
||||
user.username || user.email,
|
||||
"pingvin-share",
|
||||
secret
|
||||
);
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
totpEnabled: true,
|
||||
totpSecret: encryptedSecret,
|
||||
},
|
||||
});
|
||||
|
||||
// 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"),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Maybe require a token to verify that the user who started enabling totp is the one who is verifying it?
|
||||
async verifyTotp(user: User, password: string, code: string) {
|
||||
if (!(await argon.verify(user.password, 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 decryptedSecret = this.decryptTotpSecret(totpSecret, password);
|
||||
|
||||
const expected = authenticator.generate(decryptedSecret);
|
||||
|
||||
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 (!(await argon.verify(user.password, 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 decryptedSecret = this.decryptTotpSecret(totpSecret, password);
|
||||
|
||||
const expected = authenticator.generate(decryptedSecret);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user