Skip to content

Commit

Permalink
feat(server-auth): Part 1/3: dbAuth middleware support (web side chan…
Browse files Browse the repository at this point in the history
…ges) (#10444)

Closes #10445

**Part 1/3: dbAuth middleware support**
~1. Updates dbAuthHandler to handle POST requests for login, logout,
signup via the middleware~
taking this out of this PR, and going to PR separately. 
2. Updates the dbAuth web client to speak to middleware instead of
graphql
3. Implements fetching current user from middleware

**What it does not have:**
- actual middleware
- when SSR/RSC is enabled AND you're logged in, graphql requests will
fail with 500, because auth context hasn't been updated yet to support
cookies
(https://github.com/orgs/redwoodjs/projects/18/views/1?query=is%3Aopen+sort%3Aupdated-desc&pane=issue&itemId=59446357)

**Before merging this:** 
- [x] Validate graphql auth is not broken
~- [ ] Validate webAuthN + graphql is not broken~
~-[ ] Merge dbAuthHandler and tests again!~
Moved to separate PR
  • Loading branch information
dac09 committed Apr 15, 2024
1 parent 4c42be2 commit a6b6136
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 37 deletions.
4 changes: 4 additions & 0 deletions .changesets/10444.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- feat(server-auth): Part 1/3: dbAuth middleware support (web side changes) (#10444) by @dac09
Adds ability to `createMiddlewareAuth` in dbAuth client which:
1. Updates the dbAuth web client to speak to middleware instead of graphql
2. Implements fetching current user from middleware
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { act, renderHook } from '@testing-library/react'

import type { CustomProviderHooks, DbAuthClientArgs } from '../dbAuth'
import { createDbAuthClient, createMiddlewareAuth } from '../dbAuth'

import { fetchMock } from './dbAuth.test'

const defaultArgs = {
fetchConfig: {
credentials: 'include' as const,
},
}

export function getMwDbAuth(
args: DbAuthClientArgs & CustomProviderHooks = defaultArgs,
) {
// We have to create a special createDbAuthClient with middleware = true
const dbAuthClient = createDbAuthClient({ ...args, middleware: true })
const { useAuth, AuthProvider } = createMiddlewareAuth(dbAuthClient, {
useCurrentUser: args.useCurrentUser,
useHasRole: args.useHasRole,
})
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
})

return result
}

// These tests are on top of the other tests in dbAuth.test.ts
// They test the middleware specific things about the dbAuth client

describe('dbAuth web ~ cookie/middleware auth', () => {
it('will create a middleware version of the auth client', async () => {
const { current: dbAuthInstance } = getMwDbAuth()

// Middleware auth clients should not return tokens
expect(await dbAuthInstance.getToken()).toBeNull()

let currentUser
await act(async () => {
currentUser = await dbAuthInstance.getCurrentUser()
})

expect(globalThis.fetch).toHaveBeenCalledWith(
// Doesn't speak to graphql!
'/middleware/dbauth/currentUser',
expect.objectContaining({
credentials: 'include',
method: 'GET', // in mw auth, we use GET for currentUser
}),
)

expect(currentUser).toEqual({
id: 'middleware-user-555',
username: 'user@middleware.auth',
})
})

it('can still override getCurrentUser', async () => {
const mockedCustomCurrentUser = jest.fn()
const { current: dbAuthInstance } = getMwDbAuth({
useCurrentUser: mockedCustomCurrentUser,
})
await act(async () => {
await dbAuthInstance.getCurrentUser()
})

expect(mockedCustomCurrentUser).toHaveBeenCalled()
})

it('allows you to override the middleware endpoint', async () => {
const auth = getMwDbAuth({
dbAuthUrl: '/hello/handsome',
}).current

await act(async () => await auth.forgotPassword('username'))

expect(fetchMock).toHaveBeenCalledWith(
'/hello/handsome',
expect.any(Object),
)
})

it('calls login at the middleware endpoint', async () => {
const auth = getMwDbAuth().current

await act(
async () =>
await auth.logIn({ username: 'username', password: 'password' }),
)

expect(globalThis.fetch).toHaveBeenCalledWith(
'/middleware/dbauth',
expect.any(Object),
)
})

it('calls middleware endpoint for logout', async () => {
const auth = getMwDbAuth().current
await act(async () => {
await auth.logOut()
})

expect(globalThis.fetch).toHaveBeenCalledWith('/middleware/dbauth', {
body: '{"method":"logout"}',
credentials: 'include',
method: 'POST',
})
})

it('calls reset password at the correct endpoint', async () => {
const auth = getMwDbAuth().current

await act(
async () =>
await auth.resetPassword({
resetToken: 'reset-token',
password: 'password',
}),
)

expect(globalThis.fetch).toHaveBeenCalledWith(
'/middleware/dbauth',
expect.objectContaining({
body: '{"resetToken":"reset-token","password":"password","method":"resetPassword"}',
}),
)
})

it('passes through fetchOptions to signup calls', async () => {
const auth = getMwDbAuth().current

await act(
async () =>
await auth.signUp({
username: 'username',
password: 'password',
}),
)

expect(globalThis.fetch).toHaveBeenCalledWith(
'/middleware/dbauth',
expect.objectContaining({
method: 'POST',
body: '{"username":"username","password":"password","method":"signup"}',
}),
)
})
})
60 changes: 37 additions & 23 deletions packages/auth-providers/dbAuth/web/src/__tests__/dbAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { renderHook, act } from '@testing-library/react'

import type { CurrentUser } from '@redwoodjs/auth'

import type { DbAuthClientArgs } from '../dbAuth'
import type { CustomProviderHooks, DbAuthClientArgs } from '../dbAuth'
import { createDbAuthClient, createAuth } from '../dbAuth'

globalThis.RWJS_API_URL = '/.redwood/functions'
Expand All @@ -20,7 +20,7 @@ interface User {

let loggedInUser: User | undefined

const fetchMock = jest.fn()
export const fetchMock = jest.fn()
fetchMock.mockImplementation(async (url, options) => {
const body = options?.body ? JSON.parse(options.body) : {}

Expand Down Expand Up @@ -63,7 +63,26 @@ fetchMock.mockImplementation(async (url, options) => {
return {
ok: true,
text: () => '',
json: () => ({ data: { redwood: { currentUser: loggedInUser } } }),
json: () => ({
data: {
redwood: {
currentUser: loggedInUser,
},
},
}),
}
}

if (url.includes('middleware/dbauth/currentUser')) {
return {
ok: true,
text: () => '',
json: () => ({
currentUser: {
id: 'middleware-user-555',
username: 'user@middleware.auth',
},
}),
}
}

Expand All @@ -79,16 +98,11 @@ beforeEach(() => {
loggedInUser = undefined
})

const defaultArgs: DbAuthClientArgs & {
useCurrentUser?: () => Promise<CurrentUser>
useHasRole?: (
currentUser: CurrentUser | null,
) => (rolesToCheck: string | string[]) => boolean
} = {
const defaultArgs: DbAuthClientArgs & CustomProviderHooks = {
fetchConfig: { credentials: 'include' },
}

function getDbAuth(args = defaultArgs) {
export function getDbAuth(args = defaultArgs) {
const dbAuthClient = createDbAuthClient(args)
const { useAuth, AuthProvider } = createAuth(dbAuthClient, {
useHasRole: args.useHasRole,
Expand All @@ -101,7 +115,7 @@ function getDbAuth(args = defaultArgs) {
return result
}

describe('dbAuth', () => {
describe('dbAuth web client', () => {
it('sets a default credentials value if not included', async () => {
const authRef = getDbAuth({ fetchConfig: {} })

Expand All @@ -113,7 +127,7 @@ describe('dbAuth', () => {
await authRef.current.getToken()
})

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth?method=getToken`,
{
credentials: 'same-origin',
Expand All @@ -126,7 +140,7 @@ describe('dbAuth', () => {

await act(async () => await auth.forgotPassword('username'))

expect(fetchMock).toBeCalledWith(
expect(fetchMock).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -143,7 +157,7 @@ describe('dbAuth', () => {

expect(fetchMock).toHaveBeenCalledTimes(1)

expect(fetchMock).toBeCalledWith(
expect(fetchMock).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth?method=getToken`,
{
credentials: 'include',
Expand All @@ -152,21 +166,21 @@ describe('dbAuth', () => {
})

it('passes through fetchOptions to login calls', async () => {
const auth = (await getDbAuth()).current
const auth = getDbAuth().current

await act(
async () =>
await auth.logIn({ username: 'username', password: 'password' }),
)

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
}),
)

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -180,7 +194,7 @@ describe('dbAuth', () => {
await auth.logOut()
})

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -198,7 +212,7 @@ describe('dbAuth', () => {
}),
)

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -216,7 +230,7 @@ describe('dbAuth', () => {
}),
)

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -228,7 +242,7 @@ describe('dbAuth', () => {
const auth = getDbAuth().current
await act(async () => await auth.validateResetToken('token'))

expect(globalThis.fetch).toBeCalledWith(
expect(globalThis.fetch).toHaveBeenCalledWith(
`${globalThis.RWJS_API_URL}/auth`,
expect.objectContaining({
credentials: 'include',
Expand All @@ -241,7 +255,7 @@ describe('dbAuth', () => {

await act(async () => await auth.forgotPassword('username'))

expect(fetchMock).toBeCalledWith(
expect(fetchMock).toHaveBeenCalledWith(
'/.redwood/functions/dbauth',
expect.objectContaining({
credentials: 'same-origin',
Expand Down Expand Up @@ -326,7 +340,7 @@ describe('dbAuth', () => {
expect(authRef.current.hasRole('user')).toBeFalsy()

await act(async () => {
authRef.current.logIn({
await authRef.current.logIn({
username: 'auth-test',
password: 'ThereIsNoSpoon',
})
Expand Down
Loading

0 comments on commit a6b6136

Please sign in to comment.