import { TFANewTOTP, Server, TFAOption, UserRegisterInfo, TFAWebAuthRegister } from "@hibas123/openauth-internalapi"; import type { SessionContext } from "../index"; import TwoFactorModel, { ITOTP, IWebAuthn, TFATypes } from "../../../models/twofactor"; import moment = require("moment"); import * as speakeasy from "speakeasy"; import * as qrcode from "qrcode"; import config from "../../../config"; import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'; import type { RegistrationResponseJSON } from '@simplewebauthn/typescript-types'; import Logging from "@hibas123/nodelogging"; import { Binary } from "mongodb"; import { RequireLogin } from "../../../helper/login"; export default class TFAService extends Server.TFAService { @RequireLogin() AddBackupCodes(name: string, ctx: SessionContext): Promise { throw new Error("Method not implemented."); } @RequireLogin() RemoveBackupCodes(id: string, ctx: SessionContext): Promise { throw new Error("Method not implemented."); } @RequireLogin() async GetOptions(ctx: SessionContext): Promise { let twofactor = await TwoFactorModel.find({ user: ctx.user._id, valid: true }); let expired = twofactor.filter((e) => e.expires ? moment().isAfter(moment(e.expires)) : false ); await Promise.all( expired.map((e) => { e.valid = false; return TwoFactorModel.save(e); }) ); twofactor = twofactor.filter((e) => e.valid); let tfa = twofactor.map((e) => { return { id: e._id.toString(), name: e.name, tfatype: e.type as number, expires: e.expires?.valueOf() }; }); return tfa; } @RequireLogin() async Delete(id: string, ctx: SessionContext): Promise { let twofactor = await TwoFactorModel.findById(id); if (!twofactor || !twofactor.user.equals(ctx.user._id)) throw new Error("Invalid ID"); twofactor.valid = false; await TwoFactorModel.save(twofactor); } @RequireLogin() async AddTOTP(name: string, ctx: SessionContext): Promise { //Generating new let secret = speakeasy.generateSecret({ name: config.core.name, issuer: config.core.name, otpauth_url: true }); let twofactor = TwoFactorModel.new({ name: name, user: ctx.user._id, type: TFATypes.TOTP, valid: false, data: secret.base32, }); let dataurl = await qrcode.toDataURL(secret.otpauth_url); await TwoFactorModel.save(twofactor); return { id: twofactor._id.toString(), qr: dataurl, secret: secret.base32 } } @RequireLogin() async VerifyTOTP(id: string, code: string, ctx: SessionContext): Promise { let twofactor = await TwoFactorModel.findById(id); if (!twofactor || !twofactor.user.equals(ctx.user._id)) throw new Error("Invalid ID"); let verified = speakeasy.totp.verify({ secret: twofactor.data, encoding: "base32", token: code, }); if (!verified) throw new Error("Invalid code"); twofactor.valid = true; twofactor.expires = undefined; await TwoFactorModel.save(twofactor); } @RequireLogin() async AddWebauthn(name: string, ctx: SessionContext): Promise { // TODO: Get already registered options const rpID = new URL(config.core.url).hostname; const options = generateRegistrationOptions({ rpName: config.core.name, rpID, userID: ctx.user.uid, userName: ctx.user.username, attestationType: 'direct', userDisplayName: ctx.user.name, excludeCredentials: [], authenticatorSelection: { userVerification: "required", requireResidentKey: false, residentKey: "discouraged", authenticatorAttachment: "cross-platform" } }) const twofactor = TwoFactorModel.new({ name, type: TFATypes.WEBAUTHN, user: ctx.user._id, valid: false, data: { challenge: options.challenge } }); await TwoFactorModel.save(twofactor); Logging.debug(twofactor); return { id: twofactor._id.toString(), challenge: JSON.stringify(options) }; } @RequireLogin() async VerifyWebAuthn(id: string, registration: string, ctx: SessionContext): Promise { let twofactor = await TwoFactorModel.findById(id) as IWebAuthn; if (!twofactor || !twofactor.user.equals(ctx.user._id)) throw new Error("Invalid ID"); const rpID = new URL(config.core.url).hostname; const response = JSON.parse(registration) as RegistrationResponseJSON; let verification = await verifyRegistrationResponse({ response, expectedChallenge: twofactor.data.challenge, expectedOrigin: config.core.url, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; //TODO: Check if already registered! // TwoFactorModel.find({ // data: { // } // }) twofactor.data = { device: { credentialPublicKey: new Binary(credentialPublicKey), credentialID: new Binary(credentialID), counter: verification.registrationInfo.counter, transports: response.response.transports as any[] } } twofactor.valid = true; await TwoFactorModel.save(twofactor); } else { throw new Error("Invalid response"); } } }