How do I add Basic Authentication to Nextjs node server?

Viewed 3722
2 Answers

--- UPDATED for NextJS 12.2 ---

The current version of Nextjs allows you to run have add custom middleware by just having a middleware.js/ts file in your root directory (next to the pages folder). There you can simply check the Auth-headers and decide to either call next to allow it or return a 401 status code to deny it. Here is an example copied from the Nextjs example (typescript)

import { NextRequest, NextResponse } from 'next/server'

export const config = {
  matcher: '/',
}

export function middleware(req: NextRequest) {
  const basicAuth = req.headers.get('authorization')
  const url = req.nextUrl

  if (basicAuth) {
    const authValue = basicAuth.split(' ')[1]
    const [user, pwd] = atob(authValue).split(':')

    if (user === '4dmin' && pwd === 'testpwd123') {
      return NextResponse.next()
    }
  }
  url.pathname = '/api/auth'

  return NextResponse.rewrite(url)
}

here is the link to the full example project: https://github.com/vercel/examples/tree/main/edge-functions/basic-auth-password

--- FOR OLDER VERSIONS (pre-12.2)---

The current version of Nextjs allows you to run have add custom middleware by just having a _middleware.js/ts file in your pages directory. There you can simply check the Auth-headers and decide to either call next to allow it or return a 401 status code to deny it. Here is an example copied from the Nextjs example (typescript)

import { NextRequest, NextResponse } from 'next/server'

export function middleware(req: NextRequest) {
  const basicAuth = req.headers.get('authorization')

  if (basicAuth) {
    const auth = basicAuth.split(' ')[1]
    const [user, pwd] = Buffer.from(auth, 'base64').toString().split(':')

    if (user === '4dmin' && pwd === 'testpwd123') {
      return NextResponse.next()
    }
  }

  return new Response('Auth required', {
    status: 401,
    headers: {
      'WWW-Authenticate': 'Basic realm="Secure Area"',
    },
  })
}

here is the link to the full example project: https://github.com/vercel/examples/tree/main/edge-functions/basic-auth-password

Edit: The ExpressJs solution below won't work if you want to host it on Vercel for example which costs a bit but offers the best Next.js hosting with built-in Edge Caching and image scaling and optimisations. So if you want to host it on Vercel than add authentication to every single route using: https://nextjs.org/docs/authentication (might be no small feet unless done from the start of the project).

If you however intend to host it somewhere else that supports Node.js and Express.js hosting (and offers nothing special for Next.js) then the below is a great solution.

ExpressJs solution:

This post doesn't cover storing of Basic Auth credentials or using multiple credentials. It's more of a starting point because I couldn't find anything at all on google.

We'll be using Express.js to host NextJS (in production mode!) in both examples below.

To start the server in both examples: node index.mjs (currently using node -v 14.9.0)

First: create index.mjs file in the root.

(Option 1) Using latest NextJS:

  1. Use latest (9.5 as of writing) NextJS + Express.js node server (Latest nextjs has fixed problems (I haven't verified) with CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
  2. Inside index.mjs add the following code (install required modules):
    import next from 'next'
    import express from 'express'

    import auth from 'basic-auth'

    const dev = process.env.NODE_ENV === 'development'
    const app = next({ dev })
    const handle = app.getRequestHandler()

    app.prepare()
      .then(() => {
        const server = express()

        server.use(function (req, res, next) {
          const credentials = auth(req)

          if (!credentials || credentials.name !== '[change_me_username]' || credentials.pass !== '[change_me_password]') {
            res.status(401)
            res.header('WWW-Authenticate', 'Basic realm="example"')
            res.send('Access denied')
          } else {
            next()
          }
        });

        server.get('*', (req, res) => {
          return handle(req, res)
        })

        const port = 80;
        server.listen(port, (err) => {
          if (err) {
            throw err
          }
          console.log(`Ready on http://localhost:${port}`)
        })
      })
      .catch((ex) => {
        console.error(ex.stack)
        process.exit(1)
      })

(Option 2) Using NextJS 9.4 (if you can't upgrade)

This answer does not deal with potential ramifications from potential CSP security wholes, best and easiest way to deal with that is to upgrade to NextJs 9.5 period.

  1. Inside index.mjs add the following code (install required modules):
    import next from 'next'
    import express from 'express'

    import auth from 'basic-auth'

    const dev = process.env.NODE_ENV === 'development'
    const app = next({ dev })
    const handle = app.getRequestHandler()

    app.prepare()
      .then(() => {
        const server = express()

        server.use(function (req, res, next) {
          const credentials = auth(req)

          if (!credentials || credentials.name !== '[change_me_username]' || credentials.pass !== '[change_me_password]') {
            res.status(401)
            res.header('WWW-Authenticate', 'Basic realm="example"')
            res.send('Access denied')
          } else {
            // this is different from above NextJS 9.5, 9.4 requires inline js.
            res.setHeader(
              'Content-Security-Policy',
              "default-src * 'self' data: 'unsafe-inline' *"
            );
            next()
          }
        });

        server.get('*', (req, res) => {
          return handle(req, res)
        })

        const port = 80;
        server.listen(port, (err) => {
          if (err) {
            throw err
          }
          console.log(`Ready on http://localhost:${port}`)
        })
      })
      .catch((ex) => {
        console.error(ex.stack)
        process.exit(1)
      })
  1. Inside next.config.js if you use next-images, otherwise just put the object: module.exports = { // ... }
const withImages = require('next-images')
module.exports = withImages({
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            // // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
            key: 'Content-Security-Policy',
            value: "default-src * 'self' data: 'unsafe-inline' *"
          }
        ]
      }
    ]
  }
})
Related