shuding / nextra

Simple, powerful and flexible site generation framework with everything you love from Next.js.
https://nextra.site
MIT License
11.28k stars 1.23k forks source link

Basic Auth using middleware and /api/auth route #1477

Open timsun28 opened 1 year ago

timsun28 commented 1 year ago

Hi,

I've tried to implement a basic auth functionality using middleware as described here: https://github.com/vercel/examples/tree/main/edge-middleware/basic-auth-password

I got the password protection to work correctly, but I ran into an issue where I would still get the website displayed when I would cancel the password function.

I've recreated my problem on the following page: https://nextra-docs-basic-auth-demo.vercel.app/ The source code can be found here: https://github.com/timsun28/nextra-docs-basic-auth-demo

To recreate the problem, go to the site url and click cancel instead of entering the ursername and password (4dmin & testpwd123). If you enter the correct credentials, it will redirect you to the correct page and allows you to use the app without any problems.

In the basic-auth-password example from vercel, they use the following method: res.end(Auth Required.); This displays the "Auth Required" message on the screen if the user cancels the popup.

How could I recreate that effect with nextra docs?

Thank you in advance! Timo

[Update]:

I've managed to get around this problem by implementing the following in my middleware file:

import { withLocales } from 'nextra/locales'
import { NextResponse } from 'next/server'

export const middleware = withLocales((request) => {
  const basicAuth = request.headers.get('authorization')
  const url = request.nextUrl
  if (basicAuth) {
    const authValue = basicAuth.split(' ')[1]
    const [user, pwd] = atob(authValue).split(':')
    if (user === '4dmin' && pwd === 'testpwd123') {
      return NextResponse.next()
    } else {
      return NextResponse.error()
    }
  }
  url.pathname = '/api/auth'
  return NextResponse.rewrite(url)
})

In this case adding the else { return NextResponse.error() } solved my issue. But I'm now running into some issues where images and other files in the public folder are missing.

I found on the nextjs docs about middleware that you could use a matcher to only match on regular urls, but by adding this config to my middleware file, the entire app doesn't run anymore and I keep getting 404 error pages.

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

Would it be possible, to somehow get this to work as intended? 

[Update 2] I managed to add the following to my middleware code;

const contentRoute = /\.(jpe?g|svg|png|webmanifest|xml|ico|txt)$/.test(request.nextUrl.pathname)
if (contentRoute) return NextResponse.next()

This at least made it possible to get pictures and other files to work properly, but I am now running into the same issue where after the app is deployed, I still get to see the page without markup but with all the data. Looking at the network tab in devtools it seems like the initial page load is still done and not canceled. Could this be an issue with how the middleware is written with Nextra and could this maybe be fixed in some way?

fyfirman commented 11 months ago

Hello @timsun28, I've just put the basic auth in my nextra here. I only configure for /private and let the rest for public. I also experienced with your issue before, then I changed the configuration of middleware to be like these:

export const config = {
  matcher: [
    '/private/(.*)',
    '/_next/static/chunks/pages/private/(.*)'
  ],
}
...

Hope it helps you!

More details: links

timsun28 commented 9 months ago

Thanks to @fyfirman comments, I got this to work with locales as well.

I initially tried with the matcher as well, but I couldn't get the root page to work and it returned a 404. Seeing how in my project every page should be inaccessible when the user is not logged in, I removed the matcher all together.

For those who are looking to do the same, this is my final code I ended up with:

// middleware.ts
import { withLocales } from 'nextra/locales'
import { NextRequest, NextResponse } from 'next/server'

export const middleware = withLocales((request: NextRequest) => {
  const basicAuth = request.headers.get('authorization')
  const url = request.nextUrl

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

    if (user === 'preventech' && pwd === 'Y2XPt7cNvTHz99') {
      return NextResponse.next()
    }
  }
  url.pathname = '/api/auth'
  return NextResponse.rewrite(url)
})
// api/auth.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.setHeader("WWW-authenticate", 'Basic realm="Secure Area"');
  res.statusCode = 401;
  res.end(`Auth Required.`);
}

In case you need to have only certain pages to be authorized, make sure to add a matcher to your middleware to only match certain pages.

timsun28 commented 9 months ago

I have to unfortunately reopen this ticket, it seems like the problem where the images aren't loading correctly has returned. I found this post online where it seems like this is just not really possible.

@fyfirman , Do you have this same issue or are images working correctly for you? With my initial code, the images worked correctly, but I had the problem where if the user canceled the authentication popup. It would still show text and images, without any style which is also not the wanted result.

If anyone else has come up with a way to add some simple way of authentication with Nextra, feel free to describe your method here or link to a project.