Search code examples
next.jsnext-auth

How to send httponly cookies client side when using next-auth credentials provider?


I'm creating a next js application, using next-auth to handle authentication.

I have an external backend api, so I'm using Credentials Provider.

The problem is that the backend sends httponly cookies, but those are not being attached to the browser when i make a request client side.

In /pages/api/[...auth].js

import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import clientAxios from '../../../config/configAxios'

export default NextAuth({
    providers: [
        Providers.Credentials({
            async authorize(credentials) {
                try {
                    const login = await clientAxios.post('/api/login', {
                        username: credentials.username,
                        password: credentials.password,
                        is_master: credentials.is_master
                    })


                    const info = login.data.data.user
                    const token = {
                        accessToken: login.data.data.access_token,
                        expiresIn: login.data.data.expires_in,
                        refreshToken: login.data.data.refresh_token
                    }
                    // I can see cookies here
                    const cookies = login.headers['set-cookie']

                    return { info, token, cookies }
                } catch (error) {
                    console.log(error)
                    throw (Error(error.response.data.M))
                }
            }
        })
    ],
    callbacks: {
        async jwt(token, user, account, profile, isNewUser) {
            if (token) {
               // Here cookies are set but only in server side
               clientAxios.defaults.headers.common['Cookie'] = token.cookies
            }
            if (user) {
                token = {
                    user: user.info,
                    ...user.token,
                }
            }

            return token
        },
        async session(session, token) {
            // Add property to session, like an access_token from a provider.
            session.user = token.user
            session.accessToken = token.accessToken
            session.refreshToken = token.refreshToken

            return session
        }
    },
    session: {
        jwt: true
    }
})

my axios config file

import axios from 'axios';

const clientAxios = axios.create({

    baseURL: process.env.backendURL,
    withCredentials: true,
    headers:{
        'Accept' : 'application/json',
        'Content-Type' : 'application/json'
    }

});

export default clientAxios;

a page component

import { getSession } from "next-auth/client";
import clientAxios from "../../../config/configAxios";
import { useEffect } from "react"

export default function PageOne (props) {
    useEffect(async () => {
      // This request fails, cookies are not sent
      const response = await clientAxios.get('/api/info');
    }, [])

    return (
        <div>
           <h1>Hello World!</h1>
        </div>
    )
}

export async function getServerSideProps (context) {
    const session = await getSession(context)

    if (!session) {
        return {
            redirect: {
                destination: '/login',
                permanent: false
            }
        }
    }

    // This request works
    const response = await clientAxios.get('/api/info');
    
    return {
        props: {
            session,
            info: response.data
        }
    }
}


Solution

  • After time of researching I have figured it out.

    I had to make a change in /pages/api/auth in the way I'm exporting NextAuth.

    Instead of

    export default NextAuth({
        providers: [
           ...
        ]
    
    })
    
    

    Export it like this, so we can have access to request and response object

    export default (req, res) => {
        return NextAuth(req, res, options)
    }
    

    But to access them in the options object, we can make it a callback

    const nextAuthOptions = (req, res) => {
        return {
            providers: [
               ...
            ]
        }
    }
    
    export default (req, res) => {
        return NextAuth(req, res, nextAuthOptions(req, res))
    }
    

    To send a cookie back to the frontend from the backed we must add a 'Set-Cookie' header in the respond

    res.setHeader('Set-Cookie', ['cookie_name=cookie_value'])
    

    The complete code would be

    import NextAuth from 'next-auth';
    import CredentialsProvider from 'next-auth/providers/credentials';
    
    const nextAuthOptions = (req, res) => {
        return {
            providers: [
               CredentialsProvider({
                    async authorize(credentials) {
                       try {                      
                            const response = await axios.post('/api/login', {
                                username: credentials.username,
                                password: credentials.password
                            })
    
                            const cookies = response.headers['set-cookie']
    
                            res.setHeader('Set-Cookie', cookies)
                            
                            return response.data
                        } catch (error) {
                            console.log(error)
                            throw (Error(error.response))
                        } 
                    }
               })
            ]
        }
    }
    
    export default (req, res) => {
        return NextAuth(req, res, nextAuthOptions(req, res))
    }
    

    Update - Typescript example

    Create a type for the callback nextAuthOptions

    import { NextApiRequest, NextApiResponse } from 'next';
    import { NextAuthOptions } from 'next-auth';
    
    type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
    

    Combining everything

    import { NextApiRequest, NextApiResponse } from 'next';
    import NextAuth, { NextAuthOptions } from 'next-auth';
    import CredentialsProvider from 'next-auth/providers/credentials';
    import axios from 'axios'
    
    type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
    
    const nextAuthOptions: NextAuthOptionsCallback = (req, res) => {
         return {
            providers: [
               CredentialsProvider({
                    credentials: {
                    },
                    async authorize(credentials) {
                       try {                      
                            const response = await axios.post('/api/login', {
                                username: credentials.username,
                                password: credentials.password
                            })
    
                            const cookies = response.headers['set-cookie']
    
                            res.setHeader('Set-Cookie', cookies)
    
                            return response.data
                        } catch (error) {
                            console.log(error)
                            throw (Error(error.response))
                        } 
                    }
               })
            ],
            callbacks: {
                ...
            },
            session: {
                ...
            }
        }
    }
    
    export default (req: NextApiRequest, res: NextApiResponse) => {
        return NextAuth(req, res, nextAuthOptions(req, res))
    }