Skip to content

Commit

Permalink
chore: migrate FailedAuthRequest to pg (#9500)
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanh committed Mar 14, 2024
1 parent c417b45 commit efc0dc9
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 28 deletions.
2 changes: 1 addition & 1 deletion packages/server/__tests__/globalSetup.ts
Expand Up @@ -9,7 +9,7 @@ async function setup() {
// so the safety checks will eventually fail if run too much

await Promise.all([
r.table('FailedAuthRequest').delete().run(),
pg.deleteFrom('FailedAuthRequest').execute(),
r.table('PasswordResetRequest').delete().run(),
pg.deleteFrom('SAMLDomain').where('domain', '=', 'example.com').execute()
])
Expand Down
7 changes: 1 addition & 6 deletions packages/server/database/types/FailedAuthRequest.ts
@@ -1,20 +1,15 @@
import generateUID from '../../generateUID'

interface Input {
id?: string
ip: string
email: string
time?: Date
}

export default class FailedAuthRequest {
id: string
ip: string
email: string
time: Date
constructor(input: Input) {
const {id, email, ip, time} = input
this.id = id ?? generateUID()
const {email, ip, time} = input
this.email = email
this.ip = ip
this.time = time ?? new Date()
Expand Down
52 changes: 32 additions & 20 deletions packages/server/graphql/mutations/helpers/attemptLogin.ts
@@ -1,44 +1,56 @@
import bcrypt from 'bcryptjs'
import {sql} from 'kysely'
import ms from 'ms'
import {AuthenticationError, Threshold} from 'parabol-client/types/constEnums'
import sleep from 'parabol-client/utils/sleep'
import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums'
import getRethink from '../../../database/rethinkDriver'
import {RDatum} from '../../../database/stricterR'
import getKysely from '../../../postgres/getKysely'
import AuthIdentityLocal from '../../../database/types/AuthIdentityLocal'
import AuthToken from '../../../database/types/AuthToken'
import FailedAuthRequest from '../../../database/types/FailedAuthRequest'
import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails'

const logFailedLogin = async (ip: string, email: string) => {
const r = await getRethink()
const pg = getKysely()
if (ip) {
const failedAuthRequest = new FailedAuthRequest({ip, email})
await r.table('FailedAuthRequest').insert(failedAuthRequest).run()
await pg.insertInto('FailedAuthRequest').values(failedAuthRequest).execute()
}
}

const attemptLogin = async (denormEmail: string, password: string, ip = '') => {
const r = await getRethink()
const pg = getKysely()
const yesterday = new Date(Date.now() - ms('1d'))
const email = denormEmail.toLowerCase().trim()

const existingUser = await getUserByEmail(email)
const {failOnAccount, failOnTime} = await r({
failOnAccount: r
.table('FailedAuthRequest')
.getAll(ip, {index: 'ip'})
.filter({email})
.filter((row: RDatum) => row('time').ge(yesterday))
.count()
.ge(Threshold.MAX_ACCOUNT_PASSWORD_ATTEMPTS) as unknown as boolean,
failOnTime: r
.table('FailedAuthRequest')
.getAll(ip, {index: 'ip'})
.filter((row: RDatum) => row('time').ge(yesterday))
.count()
.ge(Threshold.MAX_DAILY_PASSWORD_ATTEMPTS) as unknown as boolean
}).run()
const {failOnAccount, failOnTime} = (await pg
.with('byAccount', (qb) =>
qb
.selectFrom('FailedAuthRequest')
.select((eb) => eb.fn.count<number>('id').as('attempts'))
.where('ip', '=', ip)
.where('email', '=', email)
.where('time', '>=', yesterday)
)
.with('byTime', (qb) =>
qb
.selectFrom('FailedAuthRequest')
.select((eb) => eb.fn.count<number>('id').as('attempts'))
.where('ip', '=', ip)
.where('time', '>=', yesterday)
)
.selectFrom(['byAccount', 'byTime'])
.select(({ref}) => [
sql<boolean>`${ref('byAccount.attempts')} >= ${Threshold.MAX_ACCOUNT_PASSWORD_ATTEMPTS}`.as(
'failOnAccount'
),
sql<boolean>`${ref('byTime.attempts')} >= ${Threshold.MAX_DAILY_PASSWORD_ATTEMPTS}`.as(
'failOnTime'
)
])
.executeTakeFirst()) as {failOnAccount: boolean; failOnTime: boolean}

if (failOnAccount || failOnTime) {
await sleep(1000)
// silently fail to trick security researchers
Expand Down
4 changes: 3 additions & 1 deletion packages/server/graphql/mutations/resetPassword.ts
Expand Up @@ -2,6 +2,7 @@ import bcrypt from 'bcryptjs'
import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql'
import {Security, Threshold} from 'parabol-client/types/constEnums'
import {AuthIdentityTypeEnum} from '../../../client/types/constEnums'
import getKysely from '../../postgres/getKysely'
import getRethink from '../../database/rethinkDriver'
import AuthIdentityLocal from '../../database/types/AuthIdentityLocal'
import AuthToken from '../../database/types/AuthToken'
Expand Down Expand Up @@ -37,6 +38,7 @@ const resetPassword = {
if (process.env.AUTH_INTERNAL_DISABLED === 'true') {
return {error: {message: 'Resetting password is disabled'}}
}
const pg = getKysely()
const r = await getRethink()
const resetRequest = (await r
.table('PasswordResetRequest')
Expand Down Expand Up @@ -73,7 +75,7 @@ const resetPassword = {
localIdentity.isEmailVerified = true
await Promise.all([
updateUser({identities}, userId),
r.table('FailedAuthRequest').getAll(email, {index: 'email'}).delete().run()
pg.deleteFrom('FailedAuthRequest').where('email', '=', email).execute()
])
context.authToken = new AuthToken({sub: userId, tms, rol})
await blacklistJWT(userId, context.authToken.iat, context.socketId)
Expand Down
@@ -0,0 +1,28 @@
import {Client} from 'pg'
import getPgConfig from '../getPgConfig'

export async function up() {
const client = new Client(getPgConfig())
await client.connect()
await client.query(`
CREATE TABLE "FailedAuthRequest" (
"id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"email" "citext" NOT NULL,
"ip" "inet" NOT NULL,
"time" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS "idx_FailedAuthRequest_email" ON "FailedAuthRequest"("email");
CREATE INDEX IF NOT EXISTS "idx_FailedAuthRequest_ip" ON "FailedAuthRequest"("ip");
`)
await client.end()
}

export async function down() {
const client = new Client(getPgConfig())
await client.connect()
await client.query(`
DROP TABLE IF EXISTS "FailedAuthRequest";
`)
await client.end()
}
1 change: 1 addition & 0 deletions yarn.lock
Expand Up @@ -11300,6 +11300,7 @@ draft-js-utils@^1.4.0:

"draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6":
version "0.10.5"
uid "025fddba56f21aaf3383aee778e0b17025c9a7bc"
resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc"
dependencies:
fbjs "^0.8.15"
Expand Down

0 comments on commit efc0dc9

Please sign in to comment.