0

I want to be able to authenticate users with Oauth and protect the API endpoints with Basic authentication. How can I achieve this? My currently configuration doesn't work, unless I remove the Basic auth part and exclude api routes from authentication/authorization. In that case, Oauth authentication alone works perfectly.

auth.config.ts

import type { NextAuthConfig, Session } from 'next-auth';
import Google from "next-auth/providers/google"
import { SupabaseAdapter } from "@auth/supabase-adapter"
import { NextRequest } from 'next/server';

export const authConfig = {
  pages: {
    signIn: '/login'
  },
  callbacks: {
    authorized({ auth, request: { nextUrl }} : {auth: null | Session, request: NextRequest}) {
        const isLoggedIn = !!auth?.user;
        const isOnHomePage = nextUrl.pathname === '/';
        if (isOnHomePage) {
            if (isLoggedIn) {
                return true;
            }
            return false; // Redirect unauthenticated users to login page
        } else if (isLoggedIn) {
            return Response.redirect(new URL('/', nextUrl))
        }
        return true;
    }
  },
  providers: [Google], 
  adapter: SupabaseAdapter({
    url: process.env.SUPABASE_URL!,
    secret: process.env.SUPABASE_SERVICE_ROLE_KEY!,
  })
} satisfies NextAuthConfig;

auth.ts

import NextAuth from "next-auth";
import { authConfig } from './auth.config';

export const { handlers, signIn, signOut, auth } = NextAuth(authConfig)

middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import { NextResponse } from 'next/server';
 
const { auth } = NextAuth(authConfig);

export default auth((req) => {
  const url = new URL(req.url);

  // Explicitly skip NextAuth API routes and session endpoint
  if (url.pathname.startsWith('/api/auth/') || url.pathname === '/api/session') {
    return NextResponse.next();
  }

  if (url.pathname.startsWith('/api/')) {
    const authHeader = req.headers.get('authorization') ?? '';
    if (!authHeader.startsWith('Basic ')) {
      return new NextResponse('Unauthorized', {
        status: 401,
        headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
      });
    }

    const base64 = authHeader.slice(6);
    let decoded = '';
    try {
      if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
        decoded = Buffer.from(base64, 'base64').toString('utf-8');
      } else if (typeof atob === 'function') {
        decoded = atob(base64);
      } else {
        return new NextResponse('Unauthorized', {
          status: 401,
          headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
        });
      }
    } catch (err) {
      return new NextResponse('Unauthorized', {
        status: 401,
        headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
      });
    }

    const [username, password] = decoded.split(':');

    const validUsername = process.env.API_BASIC_AUTH_USER;
    const validPassword = process.env.API_BASIC_AUTH_PASS;

    if (!validUsername || !validPassword || username !== validUsername || password !== validPassword) {
      return new NextResponse('Unauthorized', {
        status: 401,
        headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
      });
    }
    // Basic auth passed — continue to API handler
    return NextResponse.next();
  }
  // Non-API routes: proceed (NextAuth will handle redirects/auth pages)
  return NextResponse.next();
});

export const config = {
  // apply middleware to API routes and app pages; do NOT use negated matcher entries
  matcher: [
    '/api/:path*',
    '/((?!_next/static|_next/image|.*\\.png$).*)'
  ],
};

I get the followin error in the browser console:

providers.tsx:36 ClientFetchError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON. Read more at https://errors.authjs.dev#autherror
    at fetchData (client.js:39:22)
    at async getSession (react.js:97:21)
    at async SessionProvider.useEffect [as _getSession] (react.js:253:43)

1 Answer 1

0

OK, I figured it out

middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export const { auth: middleware } = NextAuth(authConfig);

export const config = {
  // apply middleware to API routes and app pages; do NOT use negated matcher entries
  matcher: [
    '/api/:path*',
    '/((?!_next/static|_next/image|.*\\.png$).*)'
  ],
};

auth.config.ts

import type { NextAuthConfig, Session } from 'next-auth';
import Google from "next-auth/providers/google"
import { SupabaseAdapter } from "@auth/supabase-adapter"
import { NextRequest, NextResponse } from 'next/server';

export const authConfig = {
  pages: {
    signIn: '/login'
  },
  callbacks: {
    authorized({ auth, request} : {auth: null | Session, request: NextRequest}) {

      // Allow Next.js assets
      if (request.nextUrl.pathname.startsWith('/_next') || request.nextUrl.pathname === '/favicon.ico') return true;

      // Public routes
      if (request.nextUrl.pathname === '/login' || request.nextUrl.pathname.startsWith('/public')) return true;

      if (request.nextUrl.pathname.startsWith('/api/auth/') || request.nextUrl.pathname === '/api/session' || request.nextUrl.pathname === '/api/connect-bank') {
        return true;
      }

      // Protect API routes with Basic Auth
      const url = new URL(request.url);
      if (url.pathname.startsWith('/api/')) {
        const authHeader = request.headers.get('authorization') ?? '';
        if (!authHeader.startsWith('Basic ')) {
          return new NextResponse('Unauthorized', {
            status: 401,
            headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
          });
        }

        const base64 = authHeader.slice(6);
        let decoded = '';
        try {
          if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
            decoded = Buffer.from(base64, 'base64').toString('utf-8');
          } else if (typeof atob === 'function') {
            decoded = atob(base64);
          } else {
            return new NextResponse('Unauthorized', {
              status: 401,
              headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
            });
          }
        } catch (err) {
          return new NextResponse('Unauthorized', {
            status: 401,
            headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
          });
        }

        const [username, password = ''] = decoded.split(':', 2);

        const validUsername = process.env.API_BASIC_AUTH_USER;
        const validPassword = process.env.API_BASIC_AUTH_PASS;

        if (!validUsername || !validPassword || username !== validUsername || password !== validPassword) {
          return new NextResponse('Unauthorized', {
            status: 401,
            headers: { 'WWW-Authenticate': 'Basic realm="Restricted"' },
          });
        }

        // Basic auth passed — continue to API handler
        return true;
      }

      // Redirect users to login page if unauthenticated
      console.debug('NextAuth authorized callback:', {
        isLoggedIn: !!auth?.user,
        pathname: request.nextUrl.pathname,
      });
      const isLoggedIn = !!auth?.user;
      const isOnHomePage = request.nextUrl.pathname === '/';
      if (isOnHomePage) {
          if (isLoggedIn) {
              return true;
          }
          console.debug('Redirecting to login page from home');
          return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
          return Response.redirect(new URL('/', request.nextUrl))
      }
      return true;
    }
  },
  providers: [Google], 
  adapter: SupabaseAdapter({
    url: process.env.SUPABASE_URL!,
    secret: process.env.SUPABASE_SERVICE_ROLE_KEY!,
  })
} satisfies NextAuthConfig;
Sign up to request clarification or add additional context in comments.

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.