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

4

23

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

Directoire answered 18/5, 2021 at 22:59 Comment(0)
D
30

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))
}
Directoire answered 2/10, 2021 at 17:5 Comment(10)
Do you think I can do the same using fetch rather than axios? The example does not work with fetch.Timehonored
You probably need to use a .then after your request and convert the response to jsonRhinoscopy
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?Atilt
@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"Directoire
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?Aficionado
@Aficionado I updated the answer with an example with typescriptDirectoire
My API calls' Request Headers are still missing Cookies, could you please provide a link to a working Github repo ?Millionaire
with this solution, how do you use getServerSession(req,res, nextAuthOption(req,res))? there's an type errorAccoutre
@SaulMontilla Looks like it doesn't work like that anymore. I get error 'res.setHeader is not a function'Afterword
This looks like exactly what I want, but I can't get it working. res.setHeader('Set-Cookie', cookies) seems to have no effect. I log res.getHeaders() immediately afterwards, and they don't contain what I added. I wonder if this answer from years ago doesn't apply to "next": "^14.2.4", "next-auth": "^4.24.7". Thanks for sharing, though.Cashew
R
3

If you're using NextJS 14 app router with next-auth V5 (beta version at the time of writing this), this will likely be your export from the ./auth.ts file:

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

In that case, after getting the response from the server that includes the set-cookie header, you can use the cookies() function from next/headers to set the cookie on the browser.

First step is to parse the cookie, which you can accomplish with either the cookie module or the built in querystring module, then extracting the name of the cookie and its value, and lastly setting it using the cookies().set() method, with the same details as was received from the backend.

// ./auth.ts

import { cookies } from "next/headers";
import qs from "querystring";
...
export const authConfig = {
...
    providers: [
        Credentials({
            async authorize(credentials) {
            ...
                const res = await fetch(`api/auth/endpoint`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify(credentials),
                    credentials: "include",
                });
                // Check if the response is ok here
                // Parse the cookie string with querystring module
                const setCookie = qs.decode(
                    res.headers.get("set-cookie") as string,
                    "; ",
                    "="
                ); // cast this to a type with the structure of the cookie received from your backend

                // Extract the cookie name and the value from the first entry in the 
                // setCookie object
                const [cookieName, cookieValue] = Object.entries(setCookie)[0] as [
                    string,
                    string
                ];

                // set the values that you need for your cookie
                cookies().set({
                    name: cookieName,
                    value: cookieValue,
                    httpOnly: true, // the parsing of httpOnly returns an empty string, so either have some logic to set it to boolean, or set it manually
                    maxAge: parseInt(setCookie["Max-Age"]),
                    path: setCookie.Path,
                    sameSite: "strict",
                    expires: new Date(setCookie.Expires),
                    secure: true,
                });

                // return the user object if the response was okay, or null if it wasn't
                ...
            }
        })
    ]
}

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

This discussion can help shed more light if you're still stuck.

Rondelet answered 30/12, 2023 at 16:54 Comment(1)
res.setHeader('Set-Cookie', cookies) was not working for me but it did! Thank you.Poindexter
L
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;");
            },
        },
        ...
     }
}
Linson answered 6/10, 2022 at 10:46 Comment(0)
E
-2

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

Enfold answered 18/5, 2021 at 23:37 Comment(1)
The login request is made server side by next-auth, i have the cookies available in any server side call, but client side calls failDirectoire

© 2022 - 2024 — McMap. All rights reserved.