next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

feat(frameworks): add @auth/fastify

Open hillac opened this issue 1 year ago โ€ข 37 comments

โ˜•๏ธ Reasoning

This PR is a port of @rexfordessilfie's @auth/express to Fastify. It uses similar translation layer with Web API Request and Response to fastify types. I opted to make it a plugin.

Implementation

  • Introduce a toWebRequest helper that converts an FastifyRequest into a web
  • Introduce a toFastifyReply helper that converts a web Request into an FastifyReply
  • Introduce an FastifyAuth(authConfig) initializer which returns a fastify plugin function of type FastifyPluginAsync to fulfill authentication requests. Under the hood, it:
  • It calls toWebRequest(request) to get a web Request
  • Forwards the web request to Auth (from @auth/core) to get back a web Response
  • Forwards the web response along with Fastify's reply to toFastifyReply(response, reply) to respond to fulfill the request

Tests

  • Tests the toWebRequest and toFastifyReply helpers to ensure that they forward all headers, and body and in the right format (depending on content-type)
  • Tests the full login flow of a credentials provider
  • Tests the getSession by mocking the session

Documentation

~I still haven't finished converting the express docs to fastify, just the main example is done so far.~

Notes

  • In the docs, I've added the trust proxy for the https issue. I've yet to test if this is actually required in fastify.

  • For the async handlers, I've used return body instead of reply.send(body). I'm not sure whats preferred.

  • The body is unknown type in FastifyRequest so I checked typeof req.body === 'object' && req.body !== null in encodeRequestBody. I'm not sure if this is ok. In order to test encodeUrlEncoded, I had to add checks for the body type.

  • For the response tests, the fastify injector returned the body as a string, so I had to stringify the expected value in the test equality. It also added ; charset=utf-8 to the end of the content type header, so I stripped that off for the test equality. I'm not sure if this is an issue.

  • @fastify/formbody might need to be a peer dependency, I'm not sure.

๐Ÿงข Checklist

  • [x] Documentation
  • [x] Tests
  • [x] Ready to be merged

๐ŸŽซ Affected issues

๐Ÿ“Œ Resources

hillac avatar Jan 09 '24 02:01 hillac

The latest updates on your projects. Learn more about Vercel for Git โ†—๏ธŽ

Name Status Preview Comments Updated (UTC)
auth-docs โœ… Ready (Inspect) Visit Preview ๐Ÿ’ฌ Add feedback Oct 1, 2024 6:24am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
next-auth-docs โฌœ๏ธ Ignored (Inspect) Visit Preview Oct 1, 2024 6:24am

vercel[bot] avatar Jan 09 '24 02:01 vercel[bot]

@hillac is attempting to deploy a commit to the authjs Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar Jan 09 '24 02:01 vercel[bot]

@ianschmitz How does this compare to the method you used?

hillac avatar Jan 09 '24 03:01 hillac

@ianschmitz How does this compare to the method you used?

I didn't get a chance to take it all the way to production, so I don't think I have a ton of feedback to give

ianschmitz avatar Jan 09 '24 03:01 ianschmitz

So first of all, thanks for contributing this! I'm working on a fastify application as we speak and had been putting off integrating Auth.js :joy:

Anyway, I did some testing and it seems it's having an issue with esm/cjs and @auth/core. Error message printed when starting:

{"level":50,"time":1705315403580,"pid":1221767,"hostname":"ndo4","err":
{"type":"Error","message":"No \"exports\" main defined in /opt/ndomino/sveltekasten-
rss/node_modules/@auth/core/package.json","stack":"Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: 
No \"exports\" main defined in /opt/ndomino/sveltekasten-
rss/node_modules/@auth/core/package.json\n    at __node_internal_captureLargerStackTrace 
(node:internal/errors:497:5)\n    at new NodeError (node:internal/errors:406:5)\n    at 
exportsNotFound (node:internal/modules/esm/resolve:268:10)\n    at packageExportsResolve 
(node:internal/modules/esm/resolve:542:13)\n    at resolveExports 
(node:internal/modules/cjs/loader:547:36)\n    at Module._findPath 
(node:internal/modules/cjs/loader:621:31)\n    at Module._resolveFilename 
(node:internal/modules/cjs/loader:1034:27)\n    at a._resolveFilename 
(/opt/ndomino/sveltekasten-
rss/node_modules/.pnpm/[email protected]/node_modules/tsx/dist/cjs/index.cjs:1:1729)\n    at 
Module._load (node:internal/modules/cjs/loader:901:27)\n    at Module.require 
(node:internal/modules/cjs/loader:1115:19)\n    at require 
(node:internal/modules/helpers:130:18)\n   

Looks like its trying to resolve an import via esm/resolve helper, then falling back to cjs loader and then failing.. ./node_modules/@auth/core does contain the full package and normal package.json with the export map as expected, of course theres no main export, but it should pick up the import key :thinking:

My fastify application is rather simple/straightforward atm and all ESM as far as I can tell

  • "type": "module" in root package.json
  • using import everywhere
  • Running it with tsx watch src/index.ts

Here's my entrypoint file, I've checked out your branch, built the fastify adapter and pnpm link-ed it into my project to get this:

import Fastify from "fastify"
import { dirname, join } from "path"
import { fileURLToPath } from "url"
import { updateJob } from "@jobs/cron-update"
import autoLoad from "@fastify/autoload"
import formbodyParser from "@fastify/formbody"

import GitHub from "@auth/fastify/providers/github"
import { FastifyAuth } from "@auth/fastify"

const fastify = Fastify({ logger: { level: "warn" } })
const _dirname = dirname(fileURLToPath(import.meta.url))

fastify.register(formbodyParser)

fastify.register(
  FastifyAuth({
    providers: [
      GitHub({
        clientId: process.env.GITHUB_ID,
        clientSecret: process.env.GITHUB_SECRET,
      }),
    ],
  }),
  { prefix: "/api/auth" },
)

fastify.register(autoLoad, {
  dir: join(_dirname, "routes"),
})

fastify.register(autoLoad, {
  dir: join(_dirname, "plugins"),
})
;(async function () {
  const port = process.env.PORT ? parseInt(process.env.PORT) : 8000
  try {
    await fastify.listen({ port, host: "0.0.0.0" })
    console.log(`
  ๐Ÿš€ Server ready at: http://0.0.0.0:${port}
  โŒ› Next cron run at: ${updateJob.nextRun()}
  `)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
})()

Am I missing anything? What did your example / development application look like and how did you run it? Maybe this is just a tsx issue :thinking:

ndom91 avatar Jan 15 '24 10:01 ndom91

@ndom91 Here is a demo: https://github.com/hillac/authjs-fastify-demo I'm not 100% if I've done the auth decorator in the idiomatic way, but it works for me. I'm on node 20.9.0.

Also, as was discussed with @auth/express, it might be nicer for users if the auth decorator is part of @auth/fastify in the plugin. I left it out for now to copy @auth/express.

hillac avatar Jan 15 '24 13:01 hillac

@hillac okay great, thanks for the repo example. Turns out I was just having some issues with the fastify autoload plugin. Seems to all work now!

ndom91 avatar Jan 15 '24 18:01 ndom91

Regarding your question about the decorator - while I agree, we should try to get as much as possible in the plugin itself, this "authenticate" decorator seems like it would potentially be very different from user to user, no? :thinking:

ndom91 avatar Jan 15 '24 18:01 ndom91

Also looks like the toFastifyReply behaviour was correct when using an async cb - https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution

ndom91 avatar Jan 15 '24 18:01 ndom91

Codecov Report

Attention: Patch coverage is 94.40789% with 17 lines in your changes missing coverage. Please review.

Project coverage is 39.63%. Comparing base (0b94eab) to head (302eb31). Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...es/frameworks-fastify/src/lib/http-api-adapters.ts 85.71% 14 Missing :warning:
packages/frameworks-fastify/src/index.ts 99.01% 2 Missing :warning:
packages/frameworks-fastify/src/adapters.ts 0.00% 1 Missing :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9587      +/-   ##
==========================================
+ Coverage   39.07%   39.63%   +0.55%     
==========================================
  Files         189      193       +4     
  Lines       29753    30057     +304     
  Branches     1281     1313      +32     
==========================================
+ Hits        11625    11912     +287     
- Misses      18128    18145      +17     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov[bot] avatar Mar 22 '24 08:03 codecov[bot]

No dependency changes detected. Learn more about Socket for GitHub โ†—๏ธŽ

๐Ÿ‘ No dependency changes detected in pull request

socket-security[bot] avatar Mar 22 '24 08:03 socket-security[bot]

Should we register @fastify/formbody in the plugin, and include it as a peer dependency? This would make setup easier.

hillac avatar Mar 25 '24 09:03 hillac

Should we register @fastify/formbody in the plugin, and include it as a peer dependency? This would make setup easier.

Yeah so this is to auto-parse x-www-form-urlencoded bodies correctly, right?

I figure since this is an authentication library backend, folks will be pointing forms (username / password, or just email address with OAuth provideres, etc.) at it often. So makes sense to me :+1:

ndom91 avatar Mar 25 '24 18:03 ndom91

Yeah so this is to auto-parse x-www-form-urlencoded bodies correctly, right?

Yep

I figure since this is an authentication library backend, folks will be pointing forms (username / password, or just email address with OAuth provideres, etc.) at it often. So makes sense to me ๐Ÿ‘

Yeah, it is definitely needed since the oauth sign in request has to come from a html form in my experience. The redirect fails with an AJAX request.

I made it a peer dependency but thoughts on whether it should just be a normal dependency?

hillac avatar Mar 26 '24 12:03 hillac

Yeah I mean if its going to fail in its current state without it installed, I'd say go for it.

What I was implying in my previous comment also is that this doesn't seem like a throw-away dependency, it adds value and seems like it will be useful more than just in this one use-case if folks are integrating form-based sign-in with their Fastify backend, ya know.

ndom91 avatar Mar 26 '24 15:03 ndom91

I tried to find is there someone are doing integration with Fastify. Finally, found this PR. I'm looking forward for Auth.js support with Fastify.

wiput1999 avatar Apr 11 '24 18:04 wiput1999

  • 1

Niklas2290 avatar May 05 '24 10:05 Niklas2290

Any updates on this PR?

Talento90 avatar May 08 '24 15:05 Talento90

Are there any updates on this PR? Looking forward to seeing this move forward to use it in my production apps.

ax-at avatar May 11 '24 06:05 ax-at

I think it's ready, just waiting for a review I guess.

hillac avatar May 12 '24 15:05 hillac

Hey yeah this looks pretty good already! We'd really appreciate some prep in the new docs for Fastify as well

Can you add support for a fastify docs tabs for in the /docs/../Code/index.tsx component? (see: https://github.com/nextauthjs/next-auth/blob/main/docs%2Fcomponents%2FCode%2Findex.tsx).

Also maybe then add fastify example tabs to some of the initial setup docs / code examples in pages like /docs/pages/getting-started/installation.mdx and session-management/*.mdx? Once you add support for the fastify Code tab component (i.e. Code.Fastify), itll be super straight forward to add a fastify Tab in those docs pages

ndom91 avatar May 12 '24 21:05 ndom91

The information in the getting started pages seems kind of redundant to the information already in the api reference page.

hillac avatar May 14 '24 11:05 hillac

Ok, everything's ready

hillac avatar May 24 '24 12:05 hillac

Is anything else needed to get this through?

hillac avatar Jun 05 '24 00:06 hillac

Is anything else needed to get this through?

any updates on this?

stonelotus avatar Jun 14 '24 15:06 stonelotus

@ndom91 @balazsorban44 @ThangHuuVu Anything else needed to get this merged?

hillac avatar Jun 24 '24 00:06 hillac

Oops, didn't realize renaming a branch would close the PR

hillac avatar Jul 01 '24 00:07 hillac

When is it expected to be published/approved?

fastuptime avatar Jul 09 '24 11:07 fastuptime

When is it expected to be published/approved?

@ndom91 @balazsorban44 @ThangHuuVu

hillac avatar Jul 11 '24 11:07 hillac

Sorry didn't mean to totally approve yet, just wanted to remove my one "requested changes" instance.

Anyway, it does look pretty good already! I just committed a few small tweaks.

What I'd like to see in order to merge this is at least an example app under apps/examples/fastify/* (see Qwik PR as example here). That's something we'll then sync automatically to a separate next-auth/fastify-auth-example repository (see .github/sync.yml). From there we can deploy that example app to a vercel demo project to point folks to.

And then beyond that we also like to have a separate dev app under apps/dev/fastify/* which doesn't have to be nice looking and doesn't get synced anywhere, it can also point to the workspace:* versions of everything (as opposed to the actual release versions in the example app(s)) and we can use to run in development to actually exercise all the @auth/fastify functionality.

ndom91 avatar Jul 23 '24 19:07 ndom91