packages/fxa-shared/db/models/auth/security-event.ts (168 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import crypto from 'crypto';
import ip from 'ip';
import { BaseAuthModel, Proc } from './base-auth';
import { uuidTransformer } from '../../transformers';
import { convertError } from '../../mysql';
// These are the values of the `securityEventNames` table in the fxa DB. The
// numeric id is a MySQL auto_increment'd value. It's best to run the
// migrations and then add the id values here when adding more event types.
export const EVENT_NAMES = {
'account.create': 1,
'account.login': 2,
'account.reset': 3,
'emails.clearBounces': 4,
'account.enable': 5,
'account.disable': 6,
'account.login.failure': 7, // User attempted to login but failed
'account.two_factor_added': 8,
'account.two_factor_requested': 9,
'account.two_factor_challenge_failure': 10,
'account.two_factor_challenge_success': 11,
'account.two_factor_removed': 12,
'account.password_reset_requested': 13,
'account.password_reset_success': 14,
'account.recovery_key_added': 15,
'account.recovery_key_challenge_failure': 16,
'account.recovery_key_challenge_success': 17,
'account.recovery_key_removed': 18,
'account.password_added': 19,
'account.password_changed': 20,
'account.secondary_email_added': 21,
'account.secondary_email_removed': 22,
'account.primary_secondary_swapped': 23,
'account.password_reset_otp_sent': 24,
'account.password_reset_otp_verified': 25,
'session.destroy': 26,
'account.recovery_phone_send_code': 27,
'account.recovery_phone_setup_complete': 28,
'account.recovery_phone_signin_complete': 29,
'account.recovery_phone_signin_failed': 30,
'account.recovery_phone_removed': 31,
'account.recovery_codes_replaced': 32,
'account.recovery_codes_created': 33,
'account.recovery_codes_signin_complete': 34,
'account.must_reset': 35,
} as const;
export type SecurityEventNames = keyof typeof EVENT_NAMES;
function ipHmac(key: Buffer, uid: Buffer, addr: string) {
if (ip.isV4Format(addr)) {
addr = '::' + addr;
}
const hmac = crypto.createHmac('sha256', key);
hmac.update(uid);
hmac.update(ip.toBuffer(addr));
return hmac.digest();
}
export class SecurityEvent extends BaseAuthModel {
public static tableName = 'securityEvents';
public static idColumn = ['uid', 'ipAddrHmac', 'createdAt'];
protected $uuidFields = ['uid', 'ipAddrHmac', 'tokenVerificationId'];
protected $intBoolFields = ['verified'];
// table fields
uid?: string;
ipAddrHmac?: string;
ipAddr?: string;
tokenVerificationId?: string;
name!: SecurityEventNames;
createdAt!: number;
verified!: boolean;
additionalInfo!: any; // JSON data for additional info about the security event (ie phone number, user agent, etc)
static async create({
uid,
name,
ipAddr,
ipHmacKey,
tokenId,
additionalInfo,
}: {
uid: string;
name: SecurityEventNames;
ipAddr: string;
ipHmacKey: string;
tokenId?: string;
additionalInfo?: any;
}) {
const id = uuidTransformer.to(uid);
const ipAddrHmac = ipHmac(Buffer.from(ipHmacKey), id, ipAddr);
let result;
try {
result = await this.callProcedure(
Proc.CreateSecurityEvent,
id,
tokenId ? uuidTransformer.to(tokenId) : null,
EVENT_NAMES[name],
ipAddrHmac,
Date.now(),
ipAddr,
additionalInfo ? JSON.stringify(additionalInfo) : null
);
} catch (e) {
console.error(e);
throw convertError(e);
}
return !!result;
}
static async findByUid(uid: string) {
const id = uuidTransformer.to(uid);
return this.query()
.select(
'securityEventNames.name as name',
'securityEvents.verified as verified',
'securityEvents.createdAt as createdAt',
'securityEvents.ipAddr as ipAddr',
'securityEvents.additionalInfo as additionalInfo'
)
.leftJoin(
'securityEventNames',
'securityEvents.nameId',
'securityEventNames.id'
)
.where('securityEvents.uid', id)
.orderBy('securityEvents.createdAt', 'DESC')
.limit(20);
}
static async findByUidAndIP(uid: string, ipAddr: string, ipHmacKey: string) {
const id = uuidTransformer.to(uid);
const ipAddrHmac = ipHmac(Buffer.from(ipHmacKey), id, ipAddr);
return SecurityEvent.query()
.select(
'securityEventNames.name as name',
'securityEvents.verified as verified',
'securityEvents.createdAt as createdAt',
'securityEvents.ipAddr as ipAddr',
'securityEvents.additionalInfo as additionalInfo'
)
.leftJoin(
'securityEventNames',
'securityEvents.nameId',
'securityEventNames.id'
)
.where('securityEvents.uid', id)
.andWhere('securityEvents.ipAddrHmac', ipAddrHmac)
.orderBy('securityEvents.createdAt', 'DESC')
.limit(20);
}
static async findByUidAndIPAndVerifiedLogin(
uid: string,
ipAddr: string,
ipHmacKey: string
) {
const id = uuidTransformer.to(uid);
const ipAddrHmac = ipHmac(Buffer.from(ipHmacKey), id, ipAddr);
return SecurityEvent.query()
.select(
'securityEventNames.name as name',
'securityEvents.verified as verified',
'securityEvents.createdAt as createdAt',
'securityEvents.ipAddr as ipAddr',
'securityEvents.additionalInfo as additionalInfo'
)
.leftJoin(
'securityEventNames',
'securityEvents.nameId',
'securityEventNames.id'
)
.where('securityEvents.uid', id)
.where('securityEvents.verified', 1)
.where('securityEvents.nameId', EVENT_NAMES['account.login'])
.andWhere('securityEvents.ipAddrHmac', ipAddrHmac)
.orderBy('securityEvents.createdAt', 'DESC')
.limit(20);
}
}