当使用next-auth credentials提供程序时,如何在客户端发送httponly cookies?

19

我正在创建一个Next.js应用程序,使用next-auth来处理身份验证。

我有一个外部后端API,所以我正在使用凭据提供程序。

问题在于后端发送httponly cookie,但是当我在客户端发出请求时,这些cookie未被附加到浏览器上。

在/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
    }
})

我的axios配置文件

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;
一个页面组件
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
        }
    }
}

3个回答

27

经过一段时间的研究,我解决了这个问题。

我需要在/pages/api/auth中更改导出NextAuth的方式。

改为:

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

})

像这样导出它,这样我们就可以访问请求和响应对象

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

但是要在选项对象中访问它们,我们可以将其作为回调函数处理

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

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

为了从后端向前端发送Cookie,我们必须在响应中添加'Set-Cookie'头部。

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

完整的代码如下所示

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

更新 - TypeScript 示例

为回调函数nextAuthOptions创建一个类型

import { NextApiRequest, NextApiResponse } from 'next';
import { NextAuthOptions } from 'next-auth';

type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions

将所有东西结合在一起

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

1
你认为我可以使用fetch而不是axios来完成同样的事情吗?这个例子在fetch中无法工作。 - user3174311
1
@amiryeganeh httponly cookie 无法在客户端上删除,必须在后端处理。为了使 cookie 过期,在您注销时,后端应该响应要删除的 cookie,使用相同的参数(名称、路径、域等),但过期时间比当前时间更短,例如“Thu, 01 Jan 1970 00:00:01 GMT”。 - Saul Montilla
1
我正在尝试使用TypeScript,但是我无法让它工作:-(当我尝试访问nextAuthOptions上的任何其他属性时,它会抱怨,Providers可以正常工作...但是像访问session或回调函数时,它会对我感到不满。有人用TypeScript的示例使其工作了吗? - ScreamoIsDead
2
@ScreamoIsDead 我更新了答案,并提供了一个 TypeScript 示例。 - Saul Montilla
使用这个解决方案,你如何使用 getServerSession(req,res, nextAuthOption(req,res))?出现了类型错误。 - Mocha
显示剩余4条评论

2
为了在登出后删除 nextAuth 的 cookie,我使用了以下代码块 - 将 cookie 参数设置为与要过期的 cookie 匹配的值 - 在 [...nextauth].js 文件中使用 SignOut 事件。
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;");
            },
        },
        ...
     }
}

-2
你需要配置clientAxios,以便在所有请求返回服务器时,它都包含服务器作为响应的cookie。设置api.defaults.withCredentials = true;应该可以满足你的需求。请参考下面我Vue应用程序的axios配置:
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;
};


登录请求由next-auth在服务器端进行,我可以在任何服务器端调用中获得可用的cookies,但客户端调用失败。 - Saul Montilla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接