Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
43 changes: 23 additions & 20 deletions packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ interface ForgotPasswordFlowOptions<TUser = Record<string | number, any>> {
* Needs to be explicitly set to false to disable the flow
*/
enabled?: boolean
handler: (user: TUser) => any
handler: (user: TUser, token: string) => any
errors?: {
usernameNotFound?: string
usernameRequired?: string
Expand Down Expand Up @@ -172,6 +172,12 @@ export interface DbAuthHandlerOptions<TUser = Record<string | number, any>> {
* ie. if your Prisma model is named `UserCredential` this value would be `userCredential`, as in `db.userCredential`
*/
credentialModelAccessor?: keyof PrismaClient
/**
* The fields that are allowed to be returned from the user table when
* invoking handlers that return a user object (like forgotPassword and signup)
* Defaults to `id` and `email` if not set at all.
*/
allowedUserFields?: string[]
/**
* A map of what dbAuth calls a field to what your database calls it.
* `id` is whatever column you use to uniquely identify a user (probably
Expand Down Expand Up @@ -276,6 +282,8 @@ interface DbAuthSession<TIdType> {
id: TIdType
}

const DEFAULT_ALLOWED_USER_FIELDS = ['id', 'email']

export class DbAuthHandler<
TUser extends Record<string | number, any>,
TIdType = any
Expand All @@ -288,6 +296,7 @@ export class DbAuthHandler<
db: PrismaClient
dbAccessor: any
dbCredentialAccessor: any
allowedUserFields: string[]
headerCsrfToken: string | undefined
hasInvalidSession: boolean
session: DbAuthSession<TIdType> | undefined
Expand Down Expand Up @@ -382,6 +391,8 @@ export class DbAuthHandler<
: null
this.headerCsrfToken = this.event.headers['csrf-token']
this.hasInvalidSession = false
this.allowedUserFields =
this.options.allowedUserFields || DEFAULT_ALLOWED_USER_FIELDS

const sessionExpiresAt = new Date()
sessionExpiresAt.setSeconds(
Expand Down Expand Up @@ -537,26 +548,13 @@ export class DbAuthHandler<
throw new DbAuthError.GenericError()
}

// Temporarily set the token on the user back to the raw token so it's
// available to the handler.
user.resetToken = token
// call user-defined handler in their functions/auth.js
const response = await (
this.options.forgotPassword as ForgotPasswordFlowOptions
).handler(this._sanitizeUser(user))

// remove resetToken and resetTokenExpiresAt if in the body of the
// forgotPassword handler response
let responseObj = response
if (typeof response === 'object') {
responseObj = Object.assign(response, {
[this.options.authFields.resetToken]: undefined,
[this.options.authFields.resetTokenExpiresAt]: undefined,
})
}
).handler(this._sanitizeUser(user), token)

return [
response ? JSON.stringify(responseObj) : '',
response ? JSON.stringify(response) : '',
{
...this._deleteSessionHeader,
},
Expand Down Expand Up @@ -1094,11 +1092,16 @@ export class DbAuthHandler<
].join(';')
}

// removes sensitive fields from user before sending over the wire
// removes any fields not explicitly allowed to be sent to the client before
// sending a response over the wire
_sanitizeUser(user: Record<string, unknown>) {
const sanitized = JSON.parse(JSON.stringify(user))
delete sanitized[this.options.authFields.hashedPassword]
delete sanitized[this.options.authFields.salt]

Object.keys(sanitized).forEach((key) => {
if (!this.allowedUserFields.includes(key)) {
delete sanitized[key]
}
})

return sanitized
}
Expand Down Expand Up @@ -1446,7 +1449,7 @@ export class DbAuthHandler<
SetCookieHeader & CsrfTokenHeader,
{ statusCode: number }
] {
const sessionData = { id: user[this.options.authFields.id] }
const sessionData = this._sanitizeUser(user)

// TODO: this needs to go into graphql somewhere so that each request makes a new CSRF token and sets it in both the encrypted session and the csrf-token header
const csrfToken = DbAuthHandler.CSRF_TOKEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,27 +818,29 @@ describe('dbAuth', () => {
event.body = JSON.stringify({
username: user.email,
})
options.forgotPassword.handler = (handlerUser) => {
options.forgotPassword.handler = (handlerUser, token) => {
expect(handlerUser.id).toEqual(user.id)
expect(token).toMatch(/^[A-Za-z0-9/+]{16}$/)
}
const dbAuth = new DbAuthHandler(event, context, options)
await dbAuth.forgotPassword()
expect.assertions(1)
expect.assertions(2)
})

it('invokes forgotPassword.handler() with the raw resetToken', async () => {
const user = await createDbUser()
event.body = JSON.stringify({
username: user.email,
})
options.forgotPassword.handler = (handlerUser) => {
// user should have the raw resetToken NOT the hash
options.forgotPassword.handler = (handlerUser, token) => {
// tokens should be the raw resetToken NOT the hash
// resetToken consists of 16 base64 characters
expect(handlerUser.resetToken).toMatch(/^\w{16}$/)
expect(handlerUser.resetToken).toBeUndefined()
expect(token).toMatch(/^[A-Za-z0-9/+]{16}$/)
}
const dbAuth = new DbAuthHandler(event, context, options)
await dbAuth.forgotPassword()
expect.assertions(1)
expect.assertions(2)
})

it('removes the token from the forgotPassword response', async () => {
Expand Down Expand Up @@ -1016,7 +1018,7 @@ describe('dbAuth', () => {

const response = await dbAuth.login()

expect(response[0]).toEqual({ id: user.id })
expect(response[0].id).toEqual(user.id)
})

it('returns a CSRF token in the header', async () => {
Expand Down Expand Up @@ -2753,4 +2755,33 @@ describe('dbAuth', () => {
expect(response.body).toEqual('{"error":"bad"}')
})
})

describe('_sanitizeUser', () => {
it('removes all but the default fields [id, email] on user', () => {
const dbAuth = new DbAuthHandler(event, context, options)
const user = {
id: 1,
email: '[email protected]',
password: 'secret',
}

expect(dbAuth._sanitizeUser(user).id).toEqual(user.id)
expect(dbAuth._sanitizeUser(user).email).toEqual(user.email)
expect(dbAuth._sanitizeUser(user).secret).toBeUndefined()
})

it('removes any fields not explictly allowed in allowedUserFields', () => {
options.allowedUserFields = ['foo']
const dbAuth = new DbAuthHandler(event, context, options)
const user = {
id: 1,
email: '[email protected]',
foo: 'bar',
}

expect(dbAuth._sanitizeUser(user).id).toBeUndefined()
expect(dbAuth._sanitizeUser(user).email).toBeUndefined()
expect(dbAuth._sanitizeUser(user).foo).toEqual('bar')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ export const handler = async (
// https://example.com/reset-password?resetToken=${user.resetToken}
//
// Whatever is returned from this function will be returned from
// the `forgotPassword()` function that is destructured from `useAuth()`
// the `forgotPassword()` function that is destructured from `useAuth()`.
// You could use this return value to, for example, show the email
// address in a toast message so the user will know it worked and where
// to look for the email.
handler: (user) => {
//
// Note that this return value is sent to the client in *plain text*
// so don't include anything you wouldn't want prying eyes to see. The
// `user` here has been sanitized to only include the fields listed in
// `allowedUserFields` so it should be safe to return as-is.
handler: (user, _resetToken) => {
// TODO: Send user an email/message with a link to reset their password,
// including the `resetToken`. The URL should look something like:
// `http://localhost:8910/reset-password?resetToken=${resetToken}`

return user
},

Expand Down Expand Up @@ -153,6 +162,12 @@ export const handler = async (
resetTokenExpiresAt: 'resetTokenExpiresAt',
},

// A list of fields on your user object that are safe to return to the
// client when invoking a handler that returns a user (like forgotPassword
// and signup). This list should be as small as possible to be sure not to
// leak any sensitive information to the client.
allowedUserFields = ['id', 'email'],

// Specifies attributes on the cookie that dbAuth sets in order to remember
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
cookie: {
Expand Down