Merge pull request #12678 from Budibase/vulnerability/budi-7794-invalidate-session-on-password-reset

Invalidate sessions on password reset
This commit is contained in:
Adria Navarro 2024-01-02 12:30:06 +01:00 committed by GitHub
commit d0aea11cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 4 deletions

View File

@ -1,6 +1,6 @@
import * as redis from "../redis/init" import * as redis from "../redis/init"
import * as utils from "../utils" import * as utils from "../utils"
import { Duration, DurationType } from "../utils" import { Duration } from "../utils"
const TTL_SECONDS = Duration.fromHours(1).toSeconds() const TTL_SECONDS = Duration.fromHours(1).toSeconds()
@ -32,7 +32,18 @@ export async function getCode(code: string): Promise<PasswordReset> {
const client = await redis.getPasswordResetClient() const client = await redis.getPasswordResetClient()
const value = (await client.get(code)) as PasswordReset | undefined const value = (await client.get(code)) as PasswordReset | undefined
if (!value) { if (!value) {
throw "Provided information is not valid, cannot reset password - please try again." throw new Error(
"Provided information is not valid, cannot reset password - please try again."
)
} }
return value return value
} }
/**
* Given a reset code this will invalidate it.
* @param code The code provided via the email link.
*/
export async function invalidateCode(code: string): Promise<void> {
const client = await redis.getPasswordResetClient()
await client.delete(code)
}

View File

@ -2,7 +2,7 @@ import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" import { getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"

View File

@ -79,6 +79,9 @@ export const resetUpdate = async (resetCode: string, password: string) => {
user.password = password user.password = password
user = await userSdk.db.save(user) user = await userSdk.db.save(user)
await cache.passwordReset.invalidateCode(resetCode)
await sessions.invalidateSessions(userId)
// remove password from the user before sending events // remove password from the user before sending events
delete user.password delete user.password
await events.user.passwordReset(user) await events.user.passwordReset(user)

View File

@ -0,0 +1,70 @@
import { cache, context, sessions, utils } from "@budibase/backend-core"
import { loginUser, resetUpdate } from "../auth"
import { generator, structures } from "@budibase/backend-core/tests"
import { TestConfiguration } from "../../../tests"
describe("auth", () => {
const config = new TestConfiguration()
describe("resetUpdate", () => {
it("providing a valid code will update the password", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = await config.createUser()
const previousPassword = user.password
const code = await cache.passwordReset.createCode(user._id!, {})
const newPassword = generator.hash()
await resetUpdate(code, newPassword)
const persistedUser = await config.getUser(user.email)
expect(persistedUser.password).not.toBe(previousPassword)
expect(
await utils.compare(newPassword, persistedUser.password!)
).toBeTruthy()
})
})
it("wrong code will not allow to reset the password", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const code = generator.hash()
const newPassword = generator.hash()
await expect(resetUpdate(code, newPassword)).rejects.toThrow(
"Provided information is not valid, cannot reset password - please try again."
)
})
})
it("the same code cannot be used twice", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = await config.createUser()
const code = await cache.passwordReset.createCode(user._id!, {})
const newPassword = generator.hash()
await resetUpdate(code, newPassword)
await expect(resetUpdate(code, newPassword)).rejects.toThrow(
"Provided information is not valid, cannot reset password - please try again."
)
})
})
it("updating the password will invalidate all the sessions", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = await config.createUser()
await loginUser(user)
expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(1)
const code = await cache.passwordReset.createCode(user._id!, {})
const newPassword = generator.hash()
await resetUpdate(code, newPassword)
expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(0)
})
})
})
})

View File

@ -1,6 +1,5 @@
import { structures, mocks } from "../../../tests" import { structures, mocks } from "../../../tests"
import { env, context } from "@budibase/backend-core" import { env, context } from "@budibase/backend-core"
import * as users from "../users"
import { db as userDb } from "../" import { db as userDb } from "../"
import { CloudAccount } from "@budibase/types" import { CloudAccount } from "@budibase/types"