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

fix(dbAuth): Don't throw errors under normal auth flow conditions #10927

Merged
merged 2 commits into from
Jul 9, 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
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ describe('dbAuthMiddleware', () => {
expect(res).toEqual({ passthrough: true })
})

it('When it has a cookie header, decrypts and sets server auth context', async () => {
const cookieHeader = 'session=this_is_the_only_correct_session'
it('decrypts and sets server auth context when it has a cookie header with session and auth-provider cookies', async () => {
const cookieHeader =
'session=this_is_the_only_correct_session;auth-provider=dbAuth'

const options: DbAuthMiddlewareOptions = {
getCurrentUser: vi.fn(async () => {
Expand All @@ -91,7 +92,7 @@ describe('dbAuthMiddleware', () => {
const res = await middleware(mwReq, MiddlewareResponse.next())

expect(mwReq.serverAuthState.get()).toEqual({
cookieHeader: 'session=this_is_the_only_correct_session',
cookieHeader,
currentUser: {
email: 'user-1@example.com',
id: 'mocked-current-user-1',
Expand Down Expand Up @@ -120,7 +121,8 @@ describe('dbAuthMiddleware', () => {
})

it('Will use the cookie name option correctly', async () => {
const cookieHeader = 'bazinga_8911=this_is_the_only_correct_session'
const cookieHeader =
'bazinga_8911=this_is_the_only_correct_session;auth-provider=dbAuth'

const options: DbAuthMiddlewareOptions = {
getCurrentUser: vi.fn(async () => {
Expand All @@ -143,7 +145,7 @@ describe('dbAuthMiddleware', () => {
const res = await middleware(mwReq, MiddlewareResponse.next())

expect(mwReq.serverAuthState.get()).toEqual({
cookieHeader: 'bazinga_8911=this_is_the_only_correct_session',
cookieHeader,
currentUser: {
email: 'user-1@example.com',
id: 'mocked-current-user-1',
Expand Down Expand Up @@ -382,6 +384,7 @@ describe('dbAuthMiddleware', () => {
const res = await middleware(req, MiddlewareResponse.next())
expect(res?.body).toEqual(resetToken)
})

it('handles a getToken request', async () => {
const cookieHeader =
'session=ko6iXKV11DSjb6kFJ4iwcf1FEqa5wPpbL1sdtKiV51Y=|cQaYkOPG/r3ILxWiFiz90w=='
Expand Down Expand Up @@ -504,7 +507,7 @@ describe('dbAuthMiddleware', () => {
// })
})

describe('handle exception cases', async () => {
describe('exception case handling', async () => {
const unauthenticatedServerAuthState = {
...middlewareDefaultAuthProviderState,
cookieHeader: null,
Expand Down Expand Up @@ -602,6 +605,182 @@ describe('dbAuthMiddleware', () => {
])
})

it('handles a GET request with some cookies, but no auth related cookies', async () => {
const request = new Request(
'http://localhost:8911/functions/bad-cookie',
{
method: 'GET',
headers: {
Cookie: 'not-auth=some-value;other-cookie=foobar',
},
},
)

const mwReq = new MWRequest(request)

const options: DbAuthMiddlewareOptions = {
cookieName: 'session_8911',
getCurrentUser: async () => {
return {}
},
dbAuthHandler: async () => {
return {
body: JSON.stringify({}),
headers: {},
statusCode: 200,
}
},
}
const [middleware] = initDbAuthMiddleware(options)

const res = await middleware(mwReq, MiddlewareResponse.next())
expect(res).toBeDefined()

const serverAuthState = mwReq.serverAuthState.get()
expect(serverAuthState).toEqual({
...unauthenticatedServerAuthState,
cookieHeader: 'not-auth=some-value;other-cookie=foobar',
})

expect(res?.toResponse().headers.getSetCookie()).toEqual([
// Not setting any cookies to expire
])
})

it('handles a GET request with auth-provider cookie, but no session cookie', async () => {
const request = new Request(
'http://localhost:8911/functions/bad-cookie',
{
method: 'GET',
headers: {
Cookie: 'not-auth=some-value;auth-provider=dbAuth',
},
},
)

const mwReq = new MWRequest(request)

const options: DbAuthMiddlewareOptions = {
cookieName: 'session_8911',
getCurrentUser: async () => {
return {}
},
dbAuthHandler: async () => {
return {
body: JSON.stringify({}),
headers: {},
statusCode: 200,
}
},
}
const [middleware] = initDbAuthMiddleware(options)

const res = await middleware(mwReq, MiddlewareResponse.next())
expect(res).toBeDefined()

const serverAuthState = mwReq.serverAuthState.get()
expect(serverAuthState).toEqual({
...unauthenticatedServerAuthState,
cookieHeader: 'not-auth=some-value;auth-provider=dbAuth',
})

expect(res?.toResponse().headers.getSetCookie()).toEqual([
// Expired cookies, will be removed by browser
'session_8911=; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
'auth-provider=; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
])
})

it('handles a GET request with valid session cookie, but no auth-provider cookie', async () => {
const request = new Request(
'http://localhost:8911/functions/bad-cookie',
{
method: 'GET',
headers: {
Cookie: 'session_8911=this_is_the_only_correct_session',
},
},
)

const mwReq = new MWRequest(request)

const options: DbAuthMiddlewareOptions = {
cookieName: 'session_8911',
getCurrentUser: async () => {
return {}
},
dbAuthHandler: async () => {
return {
body: JSON.stringify({}),
headers: {},
statusCode: 200,
}
},
}
const [middleware] = initDbAuthMiddleware(options)

const res = await middleware(mwReq, MiddlewareResponse.next())
expect(res).toBeDefined()

const serverAuthState = mwReq.serverAuthState.get()
expect(serverAuthState).toEqual({
...unauthenticatedServerAuthState,
cookieHeader: 'session_8911=this_is_the_only_correct_session',
})

// Because we don't have the dbAuth auth-provider cookie set the code
// should not expire the session cookie, because it could belong to
// someone else (i.e. not dbAuth)
expect(res?.toResponse().headers.getSetCookie()).toEqual([
// Don't set any cookies to expire
])
})

it('handles a GET request with invalid session cookie and no auth-provider cookie', async () => {
const request = new Request(
'http://localhost:8911/functions/bad-cookie',
{
method: 'GET',
headers: {
Cookie: 'session_8911=invalid',
},
},
)

const mwReq = new MWRequest(request)

const options: DbAuthMiddlewareOptions = {
cookieName: 'session_8911',
getCurrentUser: async () => {
return {}
},
dbAuthHandler: async () => {
return {
body: JSON.stringify({}),
headers: {},
statusCode: 200,
}
},
}
const [middleware] = initDbAuthMiddleware(options)

const res = await middleware(mwReq, MiddlewareResponse.next())
expect(res).toBeDefined()

const serverAuthState = mwReq.serverAuthState.get()
expect(serverAuthState).toEqual({
...unauthenticatedServerAuthState,
cookieHeader: 'session_8911=invalid',
})

// Because we don't have the dbAuth auth-provider cookie set the code
// should not expire the session cookie, because it could belong to
// someone else (i.e. not dbAuth)
expect(res?.toResponse().headers.getSetCookie()).toEqual([
// Don't set any cookies to expire
])
})

it('handles a GET request with no cookies', async () => {
const request = new Request('http://localhost:8911/functions/no-cookie', {
method: 'GET',
Expand Down
86 changes: 60 additions & 26 deletions packages/auth-providers/dbAuth/middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@ export const initDbAuthMiddleware = ({
// Short circuit here ...
// if the call came from packages/auth-providers/dbAuth/web/src/getCurrentUserFromMiddleware.ts
if (req.url.includes(`${dbAuthUrl}/currentUser`)) {
const { currentUser } = await validateSession({
const validatedSession = await validateSession({
req,
cookieName,
getCurrentUser,
})

return new MiddlewareResponse(JSON.stringify({ currentUser }))
if (validatedSession) {
return new MiddlewareResponse(
JSON.stringify({ currentUser: validatedSession.currentUser }),
)
} else {
return new MiddlewareResponse(JSON.stringify({ currentUser: null }))
}
} else {
const output = await dbAuthHandler(req)
console.log('output', output)
Expand All @@ -72,20 +78,30 @@ export const initDbAuthMiddleware = ({

const cookieHeader = req.headers.get('Cookie')

if (!cookieHeader) {
// If there is no 'auth-provider' cookie, then the user is not
// authenticated
if (!cookieHeader?.includes('auth-provider')) {
// Let the AuthContext fallback to its default value
return res
}

// 👇 Authenticated request
try {
// Call the dbAuth auth decoder. For dbAuth we have direct access to the `dbAuthSession` function.
// Other providers will be slightly different.
const { currentUser, decryptedSession } = await validateSession({
req,
cookieName,
getCurrentUser,
})
// At this point there might, or might not, be a dbAuth session cookie
// available.
// We treat the absence of the dbAuth session cookie the same way we treat
// an invalid session cookie – we clear server auth state and auth related
// cookies

// Call the dbAuth auth decoder. For dbAuth we have direct access to the
// `dbAuthSession` function.
// Other providers will be slightly different.
const validatedSession = await validateSession({
req,
cookieName,
getCurrentUser,
})

if (validatedSession) {
const { currentUser, decryptedSession } = validatedSession

req.serverAuthState.set({
currentUser,
Expand All @@ -96,9 +112,8 @@ export const initDbAuthMiddleware = ({
cookieHeader,
roles: getRoles(decryptedSession),
})
} catch (e) {
} else {
// Clear server auth context
console.error('Error decrypting dbAuth cookie \n', e)
req.serverAuthState.clear()

// Note we have to use ".unset" and not ".clear"
Expand Down Expand Up @@ -126,33 +141,52 @@ async function validateSession({
cookieName,
getCurrentUser,
}: ValidateParams) {
const decryptedSession = dbAuthSession(
req as Request,
cookieNameCreator(cookieName),
)
let decryptedSession: any

try {
// If there's no session cookie the return value will be `null`.
// If there is a session cookie, but it can't be decrypted, an error will
// be thrown
decryptedSession = dbAuthSession(
req as Request,
cookieNameCreator(cookieName),
)
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.debug('Could not decrypt dbAuth session', e)
}

return undefined
}

// So that it goes into the catch block
if (!decryptedSession) {
throw new Error(
'No decrypted session found. Check passed in cookie name option to ' +
`middleware. Looking for "${cookieName}"`,
)
if (process.env.NODE_ENV === 'development') {
console.debug(
'No dbAuth session cookie found. Looking for a cookie named:',
cookieName,
)
}

return undefined
}

const currentUser = await getCurrentUser(
decryptedSession,
{
type: 'dbAuth',
schema: 'cookie',
// @MARK: We pass the entire cookie header as a token. This isn't actually the token!
// At this point the Cookie header is guaranteed, because otherwise a decryptionError would be thrown
// @MARK: We pass the entire cookie header as a token. This isn't
// actually the token!
// At this point the Cookie header is guaranteed, because otherwise a
// decryptionError would have been thrown
token: req.headers.get('Cookie') as string,
},
{
// MWRequest is a superset of Request
event: req as Request,
event: req,
},
)

return { currentUser, decryptedSession }
}

Expand Down
Loading