Hebilicious / authjs-nuxt

AuthJS edge-compatible authentication Nuxt module.
https://authjs-nuxt.pages.dev/
MIT License
248 stars 30 forks source link

Cloudflare D1 Adapter support #138

Open joebailey26 opened 7 months ago

joebailey26 commented 7 months ago

Describe the feature

Cloudflare exposes bindings via the event.context.cloudflare.env.

Aside: event.context.cloudflare.env is only available when making a request, not when directly invoking the server function code. See Issue 137

I'm using the Drizzle Adapter, but this would be the same for the D1 adapter.

I need to access the event inside my auth options, so that I can pass the database through to the adapter, like so:

const D1DB: D1Database = event.context.cloudflare.env.DB
const DB: DrizzleD1Database = drizzle(D1DB)
authOptions.adapter = DrizzleAdapter(DB)

The current NuxtAuthHandler() doesn't pass event to AuthOptions as it expects the options as a parameter instead. I can't predefine options as I need access to the event from the handler within the NuxtAuthHandler() function.

I'm not sure what the best solution would be to solve this, but I've re-written auth/[...].ts so that I can pass the event through to my options. It's a bit of a pain as the utils aren't exported directly by this package, so I just had to copy and paste them.

// auth/[...].ts

import { Auth } from '@auth/core'
import type { RuntimeConfig } from 'nuxt/schema'
import { H3Event, getRequestHeaders, getRequestURL, readRawBody } from 'h3'
import { useAuthOptions } from '../../lib/auth'

function checkOrigin (request: Request, runtimeConfig: RuntimeConfig) {
  if (process.env.NODE_ENV === 'development') { return }
  if (request.method !== 'POST') { return }
  const requestOrigin = request.headers.get('Origin')
  const serverOrigin = runtimeConfig.public?.authJs?.baseUrl
  if (serverOrigin !== requestOrigin) { throw new Error('CSRF protected') }
}
async function getRequestFromEvent (event: H3Event) {
  const url = new URL(getRequestURL(event))
  const method = event.method
  const body = method === 'POST' ? await readRawBody(event) : undefined
  return new Request(url, { headers: getRequestHeaders(event) as HeadersInit, method, body })
}

export default defineEventHandler(async (event) => {
  const runtimeConfig = useRuntimeConfig()

  const authOptions = useAuthOptions(event)

  const request = await getRequestFromEvent(event)

  if (request.url.includes('.js.map')) {
    return
  }

  checkOrigin(request, runtimeConfig)

  const response = await Auth(request, authOptions)

  return response
})
// lib/auth.ts
import type { D1Database } from '@cloudflare/workers-types'
import type { AuthConfig } from '@auth/core/types'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { drizzle, DrizzleD1Database } from 'drizzle-orm/d1'
import { H3Event } from 'h3'
import { skipCSRFCheck } from '@auth/core'

export function useAuthOptions (event: H3Event) {
  const runtimeConfig = useRuntimeConfig()

  const authOptions: AuthConfig = {
    secret: runtimeConfig.authJs.secret,
    providers: ...,
    trustHost: true,
    skipCSRFCheck
  }

  const D1DB: D1Database = event.context.cloudflare.env.DB
  const DB: DrizzleD1Database = drizzle(D1DB)
  authOptions.adapter = DrizzleAdapter(DB)

  return authOptions
}

Additional information

Hebilicious commented 6 months ago

Great work ! Would happily accept a PR to expose the internals so that's easier to implement this on your own.

However I think we should eventually make the API more flexible to enable the ability to augment the options. Maybe the options could accept a function that returns a custom AuthConfig, with the event as an argument.