leojacoby
leojacoby

Reputation: 85

CORS error from NextJS endpoint error even with "Access-Control-Allow-Origin" header

I have a React frontend making a call to a NextJS endpoint. I have what I believe are the correct headers set on the server using the next.config.js file...

module.exports = {
    reactStrictMode: true,
    webpack: (config, options) => {
        if (!options.isServer) {
            config.resolve.fallback.fs = false
        }

        return config
    },
    async headers() {
        return [
            {
                source: '/api/:path*',
                headers: [
                    { key: 'origins', value: '*' },
                    { key: 'Bypass-Tunnel-Reminder', value: '*' },
                    { key: 'Access-Control-Allow-Origin', value: '*' },
                    { key: 'Access-Control-Request-Methods', value: 'POST, GET, OPTIONS' },
                    { key: 'Access-Control-Allow-Headers', value: 'Authorization, Content-Type' },
                ],
            },
        ]
    },
}

Nevertheless, I keep getting a CORS error when I call the endpoint from my frontend.Access to XMLHttpRequest at 'http://localhost:3001/api/github/setCredentials' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

Here is the frontend code...

axios.post(
                    'http://localhost:3001/github/setCredentials',
                    {
                        accessCode,
                    },
                    {
                        headers: {
                            Authorization: localStorage.getItem('jwt_token'),
                            'Content-Type': 'application/x-www-form-urlencoded',
                        },
                    },
                )

The jwt_token looks like "Bearer <TOKEN>"

The endpoint looks like this...

const authenticate = (method, req, res) =>
    new Promise((resolve, reject) => {
        passport.authenticate(method, { session: false }, (error, token) => {
            if (error) {
                reject(error)
            } else {
                resolve(token)
            }
        })(req, res)
    })

passport.use(jwtStrategy)

const handler = nextConnect({
    onError: (err, req: NextApiRequest, res: NextApiResponse, next) => {
        console.error(err.stack)
        res.status(500).end('Something broke!')
    },
    onNoMatch: (req: NextApiRequest, res: NextApiResponse) => {
        res.status(404).end('Page is not found')
    },
})
    .use(passport.initialize())
    .post(async (req: NextApiRequest, res: NextApiResponse) => {
        try {
            const user = await authenticate('jwt', req, res)
            if (!user) return res.status(403).json({ error: 'Unauthenticated user' })
            
            return res.status(200).json({ success: true })
        } catch (err) {
            return res.status(500).json({ error: err.toString() })
        }
    })

export default handler

The passport.js authentication strategy looks like this...

const opts: any = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
opts.secretOrKey = JWT_SECRET

export const jwtStrategy = new JwtStrategy(opts, function (jwt_payload, done) {
    mongoose
        .getUserByEthAddress(jwt_payload.address)
        .then((user: User | null) => {
            return done(null, user)
        })
        .catch((err) => {
            return done(err, false)
        })
})

Interestingly (frustratingly?) enough, hitting my other endpoints do not cause CORS errors. Also, when I remove the "Authorization" header from the request, I get a typical 403 error thrown by the middleware being unable to find the user, but no CORS error.

Also, when I make this call on Postman with the Authorization token and everything, it works great, so the auth layer is not the issue.

Any ideas what could be causing this? Everything I read makes it seem like setting the "Access-Control..." headers in the next.config.js file would solve this, but that has not been the case.

Upvotes: 1

Views: 3140

Answers (2)

Martik Avagyan
Martik Avagyan

Reputation: 11

I configured my next.js middleware to handle accepted origins and return website or route only if origin type is accepted for production mode. For local development I set origin value as * because no need to handle CORS for localhost. You can copy paste this code to your middleware.ts file.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const isProduction = process.env.DATASET === 'production';
const corsOrigins = isProduction
    ? [
          'domain1',
          'domain2',
          'domain3',
          'domain4',
      ]
    : ['*'];

export function middleware(req: NextRequest) {
    const origin = req.headers.get('origin');
    const res = NextResponse.next();

    // Set CORS headers
    res.headers.set('Access-Control-Allow-Credentials', 'true');
    res.headers.set('Access-Control-Allow-Methods', 'GET,DELETE,PATCH,POST,PUT');
    res.headers.set(
        'Access-Control-Allow-Headers',
        'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
    );

    if (corsOrigins.includes('*') || corsOrigins.includes(origin || '')) {
        res.headers.set('Access-Control-Allow-Origin', origin || '*');
    }

    return res;
}

export const config = {
    matcher: '/api/:path*', // Match all API routes
};

Please modify the corsOrigins array value (add all your needed domains).

Upvotes: 0

leojacoby
leojacoby

Reputation: 85

@jub0bs led me to the fix. I needed to do a couple things.

  1. Set up nextjs-cors middlware like so...
    .use(async (req: NextApiRequest, res: NextApiResponse, next) => {
        await NextCors(req, res, {
            // Options
            methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
            origin: '*',
            optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
        })
        next()
    })
  1. Add an OPTIONS method to the handler...
    .options((req: NextApiRequest, res: NextApiResponse) => {
        return res.status(200).send('ok')
    })

Then, my passport middleware populated the request with a req.user object that I can use when I handle the POST method.

Upvotes: 1

Related Questions