@auth/express
Version:
Authentication for Express.
211 lines (199 loc) • 6.94 kB
text/typescript
/**
*
* :::warning
* `@auth/express` is currently experimental. The API _will_ change in the future.
* :::
*
* Express Auth is the official Express integration for Auth.js.
* It provides a simple way to add authentication to your Express app in a few lines of code.
*
* ## Installation
* ```bash npm2yarn
* npm install @auth/express
* ```
*
* ## Usage
*
* ```ts title="src/routes/auth.route.ts"
* import { ExpressAuth } from "@auth/express"
* import GitHub from "@auth/express/providers/github"
* import express from "express"
*
* const app = express()
*
* // If app is served through a proxy, trust the proxy to allow HTTPS protocol to be detected
* // https://expressjs.com/en/guide/behind-proxies.html
* app.set('trust proxy', true)
* app.use("/auth/*", ExpressAuth({ providers: [ GitHub ] }))
* ```
*
* Don't forget to set the `AUTH_SECRET` environment variable. This should be a minimum of 32 characters, random string. On UNIX systems you can use `openssl rand -hex 32` or check out `https://generate-secret.vercel.app/32`.
*
* You will also need to load the environment variables into your runtime environment. For example in Node.js with a package like [`dotenv`](https://www.npmjs.com/package/dotenv) or `Deno.env` in Deno.
*
* ### Provider Configuration
* The callback URL used by the [providers](https://authjs.dev/reference/core/providers) must be set to the following, unless you mount the `ExpressAuth` handler on a different path:
*
* ```
* [origin]/auth/callback/[provider]
* ```
*
* ## Signing in and signing out
* Once your application is mounted you can sign in or out by making requests to the following [REST API endpoints](https://authjs.dev/reference/core/types#authaction) from your client-side code.
* NB: Make sure to include the `csrfToken` in the request body for all sign-in and sign-out requests.
*
* ## Managing the session
* If you are using Express with a template engine (e.g EJS, Pug), you can make the session data available to all routes via middleware as follows
*
* ```ts title="app.ts"
* import { getSession } from "@auth/express"
*
* export function authSession(req: Request, res: Response, next: NextFunction) {
* res.locals.session = await getSession(req)
* next()
* }
*
* app.use(authSession)
*
* // Now in your route
* app.get("/", (req, res) => {
* const { session } = res.locals
* res.render("index", { user: session?.user })
* })
* ```
*
* ## Authorization
* You can protect routes by checking for the presence of a session and then redirect to a login page if the session is not present.
* This can either be done per route, or for a group of routes using a middleware such as the following:
*
* ```ts
* export async function authenticatedUser(
* req: Request,
* res: Response,
* next: NextFunction
* ) {
* const session = res.locals.session ?? (await getSession(req, authConfig))
* if (!session?.user) {
* res.redirect("/login")
* } else {
* next()
* }
* }
* ```
*
* ### Per Route
* To protect a single route, simply add the middleware to the route as follows:
* ```ts title="app.ts"
* // This route is protected
* app.get("/profile", authenticatedUser, (req, res) => {
* const { session } = res.locals
* res.render("profile", { user: session?.user })
* })
*
* // This route is not protected
* app.get("/", (req, res) => {
* res.render("index")
* })
*
* app.use("/", root)
* ```
* ### Per Group of Routes
* To protect a group of routes, define a router and add the middleware to the router as follows:
*
* ```ts title="routes/protected.route.ts"
* import { Router } from "express"
*
* const router = Router()
*
* router.use(authenticatedUser) // All routes defined after this will be protected
*
* router.get("/", (req, res) => {
* res.render("protected")
* })
*
* export default router
* ```
*
* Then we mount the router as follows:
* ```ts title="app.ts"
* import protected from "./routes/protected.route"
*
* app.use("/protected", protected)
* ```
*
* ## Notes on ESM
* @auth/express is ESM only. This means your package.json must contain `"type": "module"` and tsconfig.json should contain `"module": "NodeNext"` or `ESNext`.
* File imports must use the `.js` extension, e.g. `import { MyRouter } from "./my-router.js"`.
*
* Your dev server should either be run with [tsx](https://www.npmjs.com/package/tsx) with `tsx index.ts` (fast startup, with no type checking), or ts-node with 'node --loader ts-node/esm index.ts' (slower startup, but has type checking).
*
* While it is NOT recommended, if you wish to use @auth/express within a CommonJS project without migrating and making the above changes, you can run the dev server with tsx and may be able to compile with [pkgroll](https://tsx.is/compilation).
* Add '"name": "./dist/index.js"' or '"name": "./dist/index.mjs"' to your package.json and run 'pkgroll' to compile with both ESM and CommonJS support. For new projects it is recommended to just use ESM.
*
* @module @auth/express
*/
import {
Auth,
type AuthConfig,
setEnvDefaults,
createActionURL,
customFetch,
} from "@auth/core"
import type { Session } from "@auth/core/types"
import * as e from "express"
import { toWebRequest, toExpressResponse } from "./lib/index.js"
export { customFetch }
export { AuthError, CredentialsSignin } from "@auth/core/errors"
export type {
Account,
DefaultSession,
Profile,
Session,
User,
} from "@auth/core/types"
export type ExpressAuthConfig = Omit<AuthConfig, "raw">
export function ExpressAuth(config: ExpressAuthConfig) {
return async (req: e.Request, res: e.Response, next: e.NextFunction) => {
e.json()(req, res, async (err) => {
if (err) return next(err)
e.urlencoded({ extended: true })(req, res, async (err) => {
if (err) return next(err)
try {
config.basePath = getBasePath(req)
setEnvDefaults(process.env, config)
await toExpressResponse(await Auth(toWebRequest(req), config), res)
if (!res.headersSent) next()
} catch (error) {
next(error)
}
})
})
}
}
export type GetSessionResult = Promise<Session | null>
export async function getSession(
req: e.Request,
config: ExpressAuthConfig
): GetSessionResult {
setEnvDefaults(process.env, config)
const url = createActionURL(
"session",
req.protocol,
// @ts-expect-error
new Headers(req.headers),
process.env,
config
)
const response = await Auth(
new Request(url, { headers: { cookie: req.headers.cookie ?? "" } }),
config
)
const { status = 200 } = response
const data = await response.json()
if (!data || !Object.keys(data).length) return null
if (status === 200) return data
throw new Error(data.message)
}
function getBasePath(req: e.Request) {
return req.baseUrl.split(req.params[0])[0].replace(/\/$/, "")
}