OpenAuth_server/Backend/src/api/jrpc/services/twofactor.ts

195 lines
6.0 KiB
TypeScript

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<SessionContext> {
@RequireLogin()
AddBackupCodes(name: string, ctx: SessionContext): Promise<string[]> {
throw new Error("Method not implemented.");
}
@RequireLogin()
RemoveBackupCodes(id: string, ctx: SessionContext): Promise<void> {
throw new Error("Method not implemented.");
}
@RequireLogin()
async GetOptions(ctx: SessionContext): Promise<TFAOption[]> {
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<TFAOption>((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<void> {
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<TFANewTOTP> {
//Generating new
let secret = speakeasy.generateSecret({
name: config.core.name,
issuer: config.core.name,
otpauth_url: true
});
let twofactor = TwoFactorModel.new(<ITOTP>{
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<void> {
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<TFAWebAuthRegister> {
// 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<void> {
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");
}
}
}