Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate FailedAuthRequest to pg #9500

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/server/__tests__/globalSetup.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id now managed by Postgres

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
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an equivalent query to what we were doing with Rethink. Just 1 trip and returns an object with these two fields. While the Kysely is pretty readable, here's what the query compiles to:

WITH "byAccount" AS (
    SELECT
        COUNT("id") AS "attempts"
    FROM
        "FailedAuthRequest"
    WHERE
        "ip" = $1
        AND "email" = $2
        AND "time" >= $3
),
"byTime" AS (
    SELECT
        COUNT("id") AS "attempts"
    FROM
        "FailedAuthRequest"
    WHERE
        "ip" = $4
        AND "time" >= $5
)
SELECT
    "byAccount"."attempts" >= $6 AS "failOnAccount",
    "byTime"."attempts" >= $7 AS "failOnTime"
FROM
    "byAccount",
    "byTime";

.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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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()
}
65 changes: 44 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3203,6 +3203,13 @@
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8"
integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==

"@fastify/merge-json-schemas@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz#3551857b8a17a24e8c799e9f51795edb07baa0bc"
integrity sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==
dependencies:
fast-deep-equal "^3.1.3"

"@floating-ui/core@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366"
Expand Down Expand Up @@ -8595,7 +8602,7 @@ ajv-keywords@^5.0.0:
dependencies:
fast-deep-equal "^3.1.3"

ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5:
ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
Expand All @@ -8605,7 +8612,7 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"

ajv@^8.0.0, ajv@^8.12.0, ajv@^8.6.0, ajv@^8.8.0:
ajv@^8.0.0, ajv@^8.10.0, ajv@^8.12.0, ajv@^8.6.0, ajv@^8.8.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
Expand Down Expand Up @@ -11223,6 +11230,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 Expand Up @@ -11960,14 +11968,18 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==

fast-json-stringify@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-1.21.0.tgz#51bc8c6d77d8c7b2cc7e5fa754f7f909f9e1262f"
integrity sha512-xY6gyjmHN3AK1Y15BCbMpeO9+dea5ePVsp3BouHCdukcx0hOHbXwFhRodhcI0NpZIgDChSeAKkHW9YjKvhwKBA==
fast-json-stringify@^5.7.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz#e9f77dc0b4face74351320c3618f1d869de5cb18"
integrity sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==
dependencies:
ajv "^6.11.0"
deepmerge "^4.2.2"
string-similarity "^4.0.1"
"@fastify/merge-json-schemas" "^0.1.0"
ajv "^8.10.0"
ajv-formats "^2.1.1"
fast-deep-equal "^3.1.3"
fast-uri "^2.1.0"
json-schema-ref-resolver "^1.0.1"
rfdc "^1.2.0"

fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
Expand All @@ -11991,6 +12003,11 @@ fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3:
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==

fast-uri@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.3.0.tgz#bdae493942483d299e7285dcb4627767d42e2793"
integrity sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==

fast-url-parser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
Expand Down Expand Up @@ -12894,15 +12911,14 @@ graphql-executor@0.0.22:
resolved "https://registry.yarnpkg.com/graphql-executor/-/graphql-executor-0.0.22.tgz#14bc466bb27ab38346998e0b375cba55685eed94"
integrity sha512-WbKSnSHFn6REKKH4T6UAwDM3mLUnYMQlQLNG0Fw+Lkb3ilCnL3m5lkJ7411LAI9sF7BvPbthovVZhsEUh9Xfag==

graphql-jit@^0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.7.4.tgz#bc8ccf79596d13dff3835902a466f9a5ecc3a8c1"
integrity sha512-kWyHmsQtKMD6xcKDgf4dgPLyIZhviqA6IWGdnA0ElL9wgrIOTxf3eI4c0/U3tnoAU3t09zliVCfDkfIptzYjIA==
graphql-jit@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.8.4.tgz#53c2e43b90ec98ea0942f4062516de910fbff709"
integrity sha512-4KRrJ1ROy3Usgbl3eAoUMfdfZCRjkcw9cCGT7QwTUIHm9dPGaSaldxzGUttyjErU0rsYEb6WWyb6mMh5r6lEoQ==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
fast-json-stringify "^1.21.0"
"@graphql-typed-document-node/core" "^3.2.0"
fast-json-stringify "^5.7.0"
generate-function "^2.3.1"
json-schema "^0.4.0"
lodash.memoize "^4.1.2"
lodash.merge "4.6.2"
lodash.mergewith "4.6.2"
Expand Down Expand Up @@ -14679,6 +14695,13 @@ json-schema-merge-allof@^0.8.1:
json-schema-compare "^0.2.2"
lodash "^4.17.20"

json-schema-ref-resolver@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz#6586f483b76254784fc1d2120f717bdc9f0a99bf"
integrity sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==
dependencies:
fast-deep-equal "^3.1.3"

json-schema-to-ts@^2.6.2-beta.0:
version "2.8.2"
resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-2.8.2.tgz#233b810b73f01e0ab93ad06ddccb1c2b98f23b8d"
Expand Down Expand Up @@ -18974,6 +18997,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==

rfdc@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==

rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
Expand Down Expand Up @@ -19897,11 +19925,6 @@ string-similarity@^3.0.0:
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7"
integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ==

string-similarity@^4.0.1:
version "4.0.4"
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==

"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
Expand Down
Loading