19

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
        }
    }
}

Saul Montilla
  • 687
  • 6
  • 16

3 Answers3

25

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))
}
Saul Montilla
  • 687
  • 6
  • 16
  • 1
    Do you think I can do the same using fetch rather than axios? The example does not work with fetch. – user3174311 Nov 09 '21 at 17:53
  • You probably need to use a `.then` after your request and convert the response to json – Youzef Mar 22 '22 at 11:39
  • this works ok and we can get the cookie from the backend. but the problem is that the cookie stays in the header even after signout. how should we handle this? – amir yeganeh May 05 '22 at 06:12
  • 1
    @amiryeganeh httponly cookies cannot be removed on client side. This must be handled on the backend. To expire the cookie, when you logout the backend should respond the cookie that you want to remove with the same parameters (name, path, domain, ...) but with a expiration time lower than the current time, for example "Thu, 01 Jan 1970 00:00:01 GMT" – Saul Montilla May 06 '22 at 17:06
  • 1
    Im trying this but with typescript but im not getting it to work :- ( it complains when i try to access any other property on nextAuthOptions, Providers works fine... but like accessing session or the callbacks it gets upset with me. Anyone got it to work with an example with Typescript? – ScreamoIsDead Oct 09 '22 at 19:08
  • 2
    @ScreamoIsDead I updated the answer with an example with typescript – Saul Montilla Oct 13 '22 at 23:54
  • My API calls' Request Headers are still missing Cookies, could you please provide a link to a working Github repo ? – Sushant Rajbanshi Jan 28 '23 at 15:56
  • with this solution, how do you use `getServerSession(req,res, nextAuthOption(req,res))`? there's an type error – Mocha May 23 '23 at 04:53
2

To remove cookie in nextAuth after signing out, I used the following block of code - set the cookie parameters to match what you have for the cookie to be expired - Use the SignOut event in [...nextauth].js file

export default async function auth(req, res) {
    return await NextAuth(req, res, {
        ...    
        events: {
            async signOut({ token }) {
                res.setHeader("Set-Cookie", "cookieName=deleted;Max-Age=0;path=/;Domain=.example.com;");
            },
        },
        ...
     }
}
Doreen Chemweno
  • 303
  • 2
  • 6
-1

You need to configure clientAxios to include cookies that the server sends as part of its response in all requests back to the server. Setting api.defaults.withCredentials = true; should get you what you want. See the axios configuration for my vue application below:

import axios from "axios";

export default ({ Vue, store, router }) => {
  const api = axios.create({
    baseURL: process.env.VUE_APP_API_URL
  });
  api.defaults.withCredentials = true; ------> this line includes the cookies
  Vue.prototype.$axios = api;
  store.$axios = api;
};

Joe Berg
  • 774
  • 2
  • 8
  • 21
  • The login request is made server side by next-auth, i have the cookies available in any server side call, but client side calls fail – Saul Montilla May 20 '21 at 00:19