195 lines
6.0 KiB
TypeScript
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");
|
|
}
|
|
}
|
|
}
|