├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── client.d.ts ├── package.json ├── playground ├── .env.example ├── package.json ├── public │ └── index.html └── server.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── __test__ │ ├── __snapshots__ │ │ └── api.test.ts.snap │ ├── api.test.ts │ └── buildServer.ts ├── client │ ├── index.ts │ └── types.ts ├── index.ts └── middie.js ├── tsconfig.json └── tsup.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint 30 | 31 | typecheck: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set node 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 16.x 39 | 40 | - name: Setup 41 | run: npm i -g @antfu/ni 42 | 43 | - name: Install 44 | run: nci 45 | 46 | - name: Typecheck 47 | run: nr typecheck 48 | 49 | test: 50 | runs-on: ${{ matrix.os }} 51 | 52 | strategy: 53 | matrix: 54 | node: [14.x, 16.x] 55 | os: [ubuntu-latest, windows-latest, macos-latest] 56 | fail-fast: false 57 | 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Set node ${{ matrix.node }} 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node }} 64 | 65 | - name: Setup 66 | run: npm i -g @antfu/ni 67 | 68 | - name: Install 69 | run: nci 70 | 71 | - name: Build 72 | run: nr build 73 | 74 | - name: Test 75 | run: nr test 76 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16.x 19 | 20 | - run: npx changelogithub 21 | env: 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.log 5 | .env 6 | coverage 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Robert Soriano (https://github.com/wobsoriano) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-next-auth 2 | 3 | Authentication plugin for Fastify, powered by [Auth.js](https://authjs.dev/). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @auth/core fastify-next-auth 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import fastify from 'fastify' 15 | import AppleProvider from '@auth/core/providers/apple' 16 | import GoogleProvider from '@auth/core/providers/google' 17 | import EmailProvider from '@auth/core/providers/email' 18 | import AuthPlugin from 'fastify-next-auth' 19 | 20 | const app = fastify() 21 | 22 | app 23 | .register(AuthPlugin, { 24 | secret: process.env.AUTH_SECRET, 25 | trustHost: process.env.AUTH_TRUST_HOST, 26 | providers: [ 27 | // OAuth authentication providers 28 | AppleProvider({ 29 | clientId: process.env.APPLE_ID, 30 | clientSecret: process.env.APPLE_SECRET, 31 | }), 32 | GoogleProvider({ 33 | clientId: process.env.GOOGLE_ID, 34 | clientSecret: process.env.GOOGLE_SECRET, 35 | }), 36 | // Sign in with passwordless email link 37 | EmailProvider({ 38 | server: process.env.MAIL_SERVER, 39 | from: '', 40 | }), 41 | ], 42 | }) 43 | ``` 44 | 45 | Client Side Functions 46 | 47 | ```ts 48 | import { signIn, signOut } from 'fastify-next-auth/client' 49 | 50 | // Redirects to sign in page 51 | signIn() 52 | 53 | // Starts OAuth sign-in flow 54 | signIn('google') 55 | 56 | // Starts Email sign-in flow 57 | signIn('email', { email: 'hello@mail.com' }) 58 | 59 | signOut() 60 | ``` 61 | 62 | Decorators 63 | 64 | ```ts 65 | fastify.get('/api/user', async function (req) { 66 | const { user } = await this.getSession(req) 67 | return user 68 | }) 69 | ``` 70 | 71 | For more info, proceed to the [Auth.js](https://authjs.dev/) docs. 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/client/index' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-next-auth", 3 | "version": "0.7.0", 4 | "description": "Auth.js plugin for Fastify.", 5 | "author": "Robert Soriano ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/wobsoriano/fastify-next-auth.git" 10 | }, 11 | "keywords": [ 12 | "fastify", 13 | "nodejs", 14 | "oauth", 15 | "jwt", 16 | "oauth2", 17 | "authentication", 18 | "nextjs", 19 | "csrf", 20 | "oidc", 21 | "nextauth", 22 | "vue", 23 | "react", 24 | "svelte" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "exports": { 30 | ".": { 31 | "types": "./dist/index.d.ts", 32 | "require": "./dist/index.js", 33 | "import": "./dist/index.mjs" 34 | }, 35 | "./client": { 36 | "types": "./dist/client/index.d.ts", 37 | "require": "./dist/client/index.js", 38 | "import": "./dist/client/index.mjs" 39 | } 40 | }, 41 | "main": "./dist/index.js", 42 | "module": "./dist/index.mjs", 43 | "types": "./dist/index.d.ts", 44 | "files": [ 45 | "dist", 46 | "*.d.ts" 47 | ], 48 | "scripts": { 49 | "build": "tsup", 50 | "dev": "tsup --onSuccess \"pnpm --filter playground dev\"", 51 | "test": "vitest run", 52 | "prepublishOnly": "pnpm build", 53 | "lint": "eslint .", 54 | "lint:fix": "eslint . --fix", 55 | "release": "bumpp && npm publish", 56 | "update-deps": "taze -w && pnpm i", 57 | "dev:playground": "pnpm --filter playground dev" 58 | }, 59 | "peerDependencies": { 60 | "@auth/core": ">=0.10.0" 61 | }, 62 | "dependencies": { 63 | "authey": "^0.8.1", 64 | "fastify-plugin": "^4.5.0", 65 | "path-to-regexp": "^6.2.1", 66 | "reusify": "^1.0.4" 67 | }, 68 | "devDependencies": { 69 | "@antfu/eslint-config": "^0.39.6", 70 | "@auth/core": "^0.10.0", 71 | "@fastify/env": "^4.2.0", 72 | "@types/node": "^20.3.2", 73 | "@types/tap": "^15.0.8", 74 | "bumpp": "^9.1.1", 75 | "eslint": "^8.43.0", 76 | "esno": "^0.16.3", 77 | "fastify": "4.18.0", 78 | "taze": "^0.10.3", 79 | "tsup": "7.1.0", 80 | "typescript": "5.1.5", 81 | "vitest": "^0.32.2" 82 | }, 83 | "eslintConfig": { 84 | "extends": "@antfu", 85 | "rules": { 86 | "unicorn/prefer-node-protocol": "off" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID= 2 | GITHUB_CLIENT_SECRET= 3 | AUTH_TRUST_HOST=1 4 | AUTH_SECRET= 5 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "esno ./server.ts" 7 | }, 8 | "dependencies": { 9 | "@fastify/static": "^6.10.2", 10 | "fastify-next-auth": "workspace:*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |
Loading...
12 |
13 |
14 |
15 |           {{ JSON.stringify(user, null, 2) }}
16 |         
17 | 18 |
19 | 20 |
21 |
22 | 23 | 24 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /playground/server.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import Fastify from 'fastify' 4 | import fastifyEnv from '@fastify/env' 5 | import GithubProvider from '@auth/core/providers/github' 6 | import NextAuthPlugin from 'fastify-next-auth' 7 | import fastifyStatic from '@fastify/static' 8 | 9 | const schema = { 10 | type: 'object', 11 | required: ['AUTH_TRUST_HOST', 'AUTH_SECRET', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'], 12 | properties: { 13 | AUTH_TRUST_HOST: { 14 | type: 'string', 15 | default: '1', 16 | }, 17 | AUTH_SECRET: { 18 | type: 'string', 19 | }, 20 | GITHUB_CLIENT_ID: { 21 | type: 'string', 22 | }, 23 | GITHUB_CLIENT_SECRET: { 24 | type: 'string', 25 | }, 26 | }, 27 | } 28 | 29 | const fastify = Fastify({ logger: false }) 30 | 31 | async function initialize() { 32 | fastify 33 | .register(fastifyStatic, { 34 | root: path.join(dirname(fileURLToPath(import.meta.url)), 'public'), 35 | }) 36 | .register(fastifyEnv, { 37 | schema, 38 | dotenv: true, 39 | }) 40 | await fastify.after() 41 | await fastify.register(NextAuthPlugin, { 42 | secret: process.env.AUTH_SECRET, 43 | trustHost: Boolean(process.env.AUTH_TRUST_HOST), 44 | providers: [ 45 | GithubProvider({ 46 | clientId: process.env.GITHUB_CLIENT_ID as string, 47 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 48 | }), 49 | ], 50 | }) 51 | } 52 | 53 | fastify.get('/', (req, reply) => { 54 | return reply.sendFile('index.html') 55 | }) 56 | 57 | fastify.get('/api/user', async (req) => { 58 | const session = await fastify.getSession(req) 59 | return session 60 | }) 61 | 62 | initialize() 63 | 64 | async function startServer() { 65 | try { 66 | await fastify.ready() 67 | await fastify.listen({ 68 | port: 3000, 69 | }) 70 | // eslint-disable-next-line no-console 71 | console.log('listening on port 3000') 72 | } 73 | catch (err) { 74 | fastify.log.error(err) 75 | process.exit(1) 76 | } 77 | } 78 | 79 | startServer() 80 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | -------------------------------------------------------------------------------- /src/__test__/__snapshots__/api.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`REST API > GET /api/auth/providers 1`] = ` 4 | { 5 | "github": { 6 | "callbackUrl": "http://localhost/api/auth/callback/github", 7 | "id": "github", 8 | "name": "GitHub", 9 | "signinUrl": "http://localhost/api/auth/signin/github", 10 | "type": "oauth", 11 | }, 12 | } 13 | `; 14 | 15 | exports[`REST API > GET /api/auth/session 1`] = `null`; 16 | -------------------------------------------------------------------------------- /src/__test__/api.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test } from 'vitest' 2 | import { buildServer } from './buildServer' 3 | 4 | let fastify: Awaited> 5 | 6 | beforeEach(async () => { 7 | fastify = await buildServer() 8 | }) 9 | 10 | afterEach(() => { 11 | fastify.close() 12 | }) 13 | 14 | describe('REST API', () => { 15 | test('GET /api/auth/signin', async () => { 16 | const response = await fastify.inject({ 17 | method: 'GET', 18 | url: '/api/auth/signin', 19 | }) 20 | 21 | expect(response.statusCode).toBe(200) 22 | expect(response.body).toContain('Sign in with GitHub') 23 | }) 24 | 25 | test('POST /api/auth/signin/:provider', async () => { 26 | const response = await fastify.inject({ 27 | method: 'POST', 28 | url: '/api/auth/signin/github', 29 | }) 30 | 31 | expect(response.statusCode).toBe(302) 32 | }) 33 | 34 | test('GET /api/auth/signout', async () => { 35 | const response = await fastify.inject({ 36 | method: 'GET', 37 | url: '/api/auth/signout', 38 | }) 39 | 40 | expect(response.statusCode).toBe(200) 41 | expect(response.body).toContain('Are you sure you want to sign out?') 42 | expect(response.body).toContain('Sign out') 43 | }) 44 | 45 | test('POST /api/auth/signout', async () => { 46 | const response = await fastify.inject({ 47 | method: 'POST', 48 | url: '/api/auth/signout', 49 | }) 50 | 51 | expect(response.statusCode).toBe(302) 52 | }) 53 | 54 | test('GET /api/auth/providers', async () => { 55 | const response = await fastify.inject({ 56 | method: 'GET', 57 | url: '/api/auth/providers', 58 | }) 59 | 60 | expect(response.statusCode).toBe(200) 61 | expect(response.json()).toMatchSnapshot() 62 | }) 63 | 64 | test('GET /api/auth/session', async () => { 65 | const response = await fastify.inject({ 66 | method: 'GET', 67 | url: '/api/auth/session', 68 | }) 69 | 70 | expect(response.statusCode).toBe(200) 71 | expect(response.json()).toMatchSnapshot() 72 | }) 73 | 74 | test('GET /api/auth/csrf', async () => { 75 | const response = await fastify.inject({ 76 | method: 'GET', 77 | url: '/api/auth/csrf', 78 | }) 79 | 80 | expect(response.statusCode).toBe(200) 81 | expect(response.json()).toHaveProperty('csrfToken') 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /src/__test__/buildServer.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import Fastify from 'fastify' 3 | import fastifyEnv from '@fastify/env' 4 | import GithubProvider from '@auth/core/providers/github' 5 | import NextAuthPlugin from '../index' 6 | 7 | const schema = { 8 | type: 'object', 9 | required: ['NEXTAUTH_URL', 'AUTH_SECRET', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'], 10 | properties: { 11 | NEXTAUTH_URL: { 12 | type: 'string', 13 | default: 'http://localhost:3000', 14 | }, 15 | AUTH_SECRET: { 16 | type: 'string', 17 | }, 18 | GITHUB_CLIENT_ID: { 19 | type: 'string', 20 | }, 21 | GITHUB_CLIENT_SECRET: { 22 | type: 'string', 23 | }, 24 | }, 25 | } 26 | 27 | export async function buildServer() { 28 | const server = Fastify() 29 | 30 | server.register(fastifyEnv, { 31 | schema, 32 | dotenv: { 33 | path: path.join(__dirname, '../../playground/.env'), 34 | }, 35 | }) 36 | await server.after() 37 | server.register(NextAuthPlugin, { 38 | trustHost: true, 39 | secret: process.env.AUTH_SECRET, 40 | providers: [ 41 | GithubProvider({ 42 | clientId: process.env.GITHUB_CLIENT_ID!, 43 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 44 | }), 45 | ], 46 | }) 47 | 48 | return server 49 | } 50 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BuiltInProviderType, 3 | RedirectableProviderType, 4 | } from '@auth/core/providers' 5 | import type { 6 | LiteralUnion, 7 | SignInAuthorizationParams, 8 | SignInOptions, 9 | SignOutParams, 10 | } from './types' 11 | 12 | /** 13 | * Client-side method to initiate a signin flow 14 | * or send the user to the signin page listing all possible providers. 15 | * Automatically adds the CSRF token to the request. 16 | * 17 | * [Documentation](https://next-auth.js.org/getting-started/client#signin) 18 | */ 19 | export async function signIn< 20 | P extends RedirectableProviderType | undefined = undefined, 21 | >( 22 | providerId?: LiteralUnion< 23 | P extends RedirectableProviderType 24 | ? P | BuiltInProviderType 25 | : BuiltInProviderType 26 | >, 27 | options?: SignInOptions, 28 | authorizationParams?: SignInAuthorizationParams, 29 | ) { 30 | const { callbackUrl = window.location.href, redirect = true } = options ?? {} 31 | 32 | // TODO: Support custom providers 33 | const isCredentials = providerId === 'credentials' 34 | const isEmail = providerId === 'email' 35 | const isSupportingReturn = isCredentials || isEmail 36 | 37 | // TODO: Handle custom base path 38 | const signInUrl = `/api/auth/${ 39 | isCredentials ? 'callback' : 'signin' 40 | }/${providerId}` 41 | 42 | const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}` 43 | 44 | // TODO: Handle custom base path 45 | // TODO: Remove this since Sveltekit offers the CSRF protection via origin check 46 | const csrfTokenResponse = await fetch('/api/auth/csrf') 47 | const { csrfToken } = await csrfTokenResponse.json() 48 | 49 | const res = await fetch(_signInUrl, { 50 | method: 'post', 51 | headers: { 52 | 'Content-Type': 'application/x-www-form-urlencoded', 53 | 'X-Auth-Return-Redirect': '1', 54 | }, 55 | // @ts-expect-error -- ignore 56 | body: new URLSearchParams({ 57 | ...options, 58 | csrfToken, 59 | callbackUrl, 60 | }), 61 | }) 62 | 63 | const data = await res.clone().json() 64 | const error = new URL(data.url).searchParams.get('error') 65 | 66 | if (redirect || !isSupportingReturn || !error) { 67 | // TODO: Do not redirect for Credentials and Email providers by default in next major 68 | window.location.href = data.url ?? callbackUrl 69 | // If url contains a hash, the browser does not reload the page. We reload manually 70 | if (data.url.includes('#')) 71 | window.location.reload() 72 | return 73 | } 74 | 75 | return res 76 | } 77 | 78 | /** 79 | * Signs the user out, by removing the session cookie. 80 | * Automatically adds the CSRF token to the request. 81 | * 82 | * [Documentation](https://next-auth.js.org/getting-started/client#signout) 83 | */ 84 | export async function signOut(options?: SignOutParams) { 85 | const { callbackUrl = window.location.href } = options ?? {} 86 | // TODO: Custom base path 87 | // TODO: Remove this since Sveltekit offers the CSRF protection via origin check 88 | const csrfTokenResponse = await fetch('/api/auth/csrf') 89 | const { csrfToken } = await csrfTokenResponse.json() 90 | const res = await fetch('/api/auth/signout', { 91 | method: 'post', 92 | headers: { 93 | 'Content-Type': 'application/x-www-form-urlencoded', 94 | 'X-Auth-Return-Redirect': '1', 95 | }, 96 | body: new URLSearchParams({ 97 | csrfToken, 98 | callbackUrl, 99 | }), 100 | }) 101 | const data = await res.json() 102 | 103 | const url = data.url ?? callbackUrl 104 | window.location.href = url 105 | // If url contains a hash, the browser does not reload the page. We reload manually 106 | if (url.includes('#')) 107 | window.location.reload() 108 | } 109 | -------------------------------------------------------------------------------- /src/client/types.ts: -------------------------------------------------------------------------------- 1 | // Taken from next-auth/react 2 | import type { BuiltInProviderType, ProviderType } from '@auth/core/providers' 3 | 4 | /** 5 | * Util type that matches some strings literally, but allows any other string as well. 6 | * @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611 7 | */ 8 | export declare type LiteralUnion = T | (U & Record) 9 | export interface ClientSafeProvider { 10 | id: LiteralUnion 11 | name: string 12 | type: ProviderType 13 | signinUrl: string 14 | callbackUrl: string 15 | } 16 | export interface SignInOptions extends Record { 17 | /** 18 | * Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from. 19 | * 20 | * [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl) 21 | */ 22 | callbackUrl?: string 23 | /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */ 24 | redirect?: boolean 25 | } 26 | export interface SignInResponse { 27 | error: string | undefined 28 | status: number 29 | ok: boolean 30 | url: string | null 31 | } 32 | /** Match `inputType` of `new URLSearchParams(inputType)` */ 33 | export declare type SignInAuthorizationParams = string | string[][] | Record | URLSearchParams 34 | /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1) */ 35 | export interface SignOutResponse { 36 | url: string 37 | } 38 | export interface SignOutParams { 39 | /** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */ 40 | callbackUrl?: string 41 | /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */ 42 | redirect?: R 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http' 2 | import type { AuthConfig, Session } from '@auth/core/types' 3 | import type { FastifyPluginCallback, FastifyRequest } from 'fastify' 4 | import fastifyPlugin from 'fastify-plugin' 5 | import { createAuthMiddleware, getSession } from 'authey' 6 | import Middie from './middie' 7 | 8 | const plugin: FastifyPluginCallback = ( 9 | fastify, 10 | options, 11 | done, 12 | ) => { 13 | const middleware = createAuthMiddleware(options) 14 | 15 | const middie = Middie((err: Error, _req: IncomingMessage, _res: ServerResponse, next: (err?: Error) => void) => { 16 | next(err) 17 | }) 18 | 19 | middie.use(middleware) 20 | 21 | function runMiddie(req: any, reply: any, next: (err?: Error) => void) { 22 | req.raw.originalUrl = req.raw.url 23 | req.raw.id = req.id 24 | req.raw.hostname = req.hostname 25 | req.raw.ip = req.ip 26 | req.raw.ips = req.ips 27 | req.raw.log = req.log 28 | req.raw.body = req.body 29 | req.raw.query = req.query 30 | reply.raw.log = req.log 31 | for (const [key, val] of Object.entries(reply.getHeaders())) { 32 | reply.raw.setHeader(key, val) 33 | } 34 | middie.run(req.raw, reply.raw, next) 35 | } 36 | 37 | fastify.addHook('onRequest', runMiddie) 38 | // eslint-disable-next-line prefer-arrow-callback 39 | fastify.decorate('getSession', function (req: FastifyRequest) { 40 | return getSession(req.raw, options) 41 | }) 42 | 43 | done() 44 | } 45 | 46 | const fastifyNextAuth = fastifyPlugin(plugin, { 47 | fastify: '4.x', 48 | name: 'fastify-next-auth', 49 | }) 50 | 51 | export { 52 | fastifyNextAuth, 53 | AuthConfig, 54 | } 55 | 56 | export default fastifyNextAuth 57 | 58 | declare module 'fastify' { 59 | interface FastifyInstance { 60 | getSession(req: FastifyRequest): Session 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/middie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Original code by Fastify 3 | * MIT Licensed, Copyright 2017-2018 Fastify, see https://github.com/fastify/middie/blob/master/LICENSE for details 4 | * 5 | * Credits to the Fastify team for the middleware support logic: 6 | * https://github.com/fastify/middie/blob/master/lib/engine.js 7 | */ 8 | import reusify from 'reusify' 9 | import { pathToRegexp } from 'path-to-regexp' 10 | 11 | function middie(complete) { 12 | const middlewares = [] 13 | const pool = reusify(Holder) 14 | 15 | return { 16 | use, 17 | run, 18 | } 19 | 20 | function use(url, f) { 21 | if (f === undefined) { 22 | f = url 23 | url = null 24 | } 25 | 26 | let regexp 27 | if (url) { 28 | regexp = pathToRegexp(sanitizePrefixUrl(url), [], { 29 | end: false, 30 | strict: false, 31 | }) 32 | } 33 | 34 | if (Array.isArray(f)) { 35 | for (const val of f) { 36 | middlewares.push({ 37 | regexp, 38 | fn: val, 39 | }) 40 | } 41 | } 42 | else { 43 | middlewares.push({ 44 | regexp, 45 | fn: f, 46 | }) 47 | } 48 | 49 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 50 | return this 51 | } 52 | 53 | function run(req, res, ctx) { 54 | if (!middlewares.length) { 55 | complete(null, req, res, ctx) 56 | return 57 | } 58 | 59 | req.originalUrl = req.url 60 | 61 | const holder = pool.get() 62 | holder.req = req 63 | holder.res = res 64 | holder.url = sanitizeUrl(req.url) 65 | holder.context = ctx 66 | holder.done() 67 | } 68 | 69 | function Holder() { 70 | this.next = null 71 | this.req = null 72 | this.res = null 73 | this.url = null 74 | this.context = null 75 | this.i = 0 76 | 77 | // eslint-disable-next-line @typescript-eslint/no-this-alias 78 | const that = this 79 | this.done = function (err) { 80 | const req = that.req 81 | const res = that.res 82 | const url = that.url 83 | const context = that.context 84 | const i = that.i++ 85 | 86 | req.url = req.originalUrl 87 | 88 | if (res.finished === true || res.writableEnded === true) { 89 | that.req = null 90 | that.res = null 91 | that.context = null 92 | that.i = 0 93 | pool.release(that) 94 | return 95 | } 96 | 97 | if (err || middlewares.length === i) { 98 | complete(err, req, res, context) 99 | that.req = null 100 | that.res = null 101 | that.context = null 102 | that.i = 0 103 | pool.release(that) 104 | } 105 | else { 106 | const middleware = middlewares[i] 107 | const fn = middleware.fn 108 | const regexp = middleware.regexp 109 | if (regexp) { 110 | const result = regexp.exec(url) 111 | if (result) { 112 | req.url = req.url.replace(result[0], '') 113 | if (req.url[0] !== '/') 114 | req.url = `/${req.url}` 115 | 116 | fn(req, res, that.done) 117 | } 118 | else { 119 | that.done() 120 | } 121 | } 122 | else { 123 | fn(req, res, that.done) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | function sanitizeUrl(url) { 131 | for (let i = 0, len = url.length; i < len; i++) { 132 | const charCode = url.charCodeAt(i) 133 | if (charCode === 63 || charCode === 35) 134 | return url.slice(0, i) 135 | } 136 | return url 137 | } 138 | 139 | function sanitizePrefixUrl(url) { 140 | if (url === '') 141 | return url 142 | if (url === '/') 143 | return '' 144 | if (url[url.length - 1] === '/') 145 | return url.slice(0, -1) 146 | return url 147 | } 148 | 149 | export default middie 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "skipLibCheck": true, 10 | "noUnusedLocals": true, 11 | "noImplicitAny": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: [ 5 | 'src/index.ts', 6 | 'src/client/index.ts', 7 | ], 8 | dts: true, 9 | clean: false, 10 | format: ['cjs', 'esm'], 11 | splitting: false, 12 | }) 13 | --------------------------------------------------------------------------------