首页
/ NextAuth.js 与 Azure AD B2C 集成中的常见问题及解决方案

NextAuth.js 与 Azure AD B2C 集成中的常见问题及解决方案

2025-05-06 03:43:59作者:庞眉杨Will

问题背景

在使用 NextAuth.js 与 Azure AD B2C 进行集成时,开发者经常会遇到一个典型错误:"Cannot read properties of undefined (reading 'substring')"。这个错误通常发生在中间件处理过程中,特别是在 oidc-token-hash 模块尝试处理令牌时。

错误分析

该错误的核心在于 NextAuth.js 的 OIDC 令牌验证流程中,某个关键变量未被正确初始化。具体来说,当 NextAuth.js 尝试使用 oidc-token-hash 库验证令牌时,传入的参数可能为 undefined,导致 substring 方法调用失败。

解决方案

1. 正确的 SessionProvider 配置

首先需要确保 SessionProvider 组件被正确设置。这个组件是 NextAuth.js 的核心,负责在整个应用中提供会话状态。

'use client';
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";

export function SessionProvider({ children }: { children: React.ReactNode }) {
    return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}

2. 路由处理器配置

在 Next.js 13+ 的应用路由结构中,需要正确设置 auth 路由处理器:

import NextAuth from "next-auth"
import { authOptions } from "@/lib/auth/auth-options"

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

3. 完整的 authOptions 配置

完整的 authOptions 配置是解决此问题的关键。以下是一个完整的 Azure AD B2C 集成示例:

import { jwtDecode } from "jwt-decode";
import { DefaultSession } from "next-auth";
import AzureADB2C from "next-auth/providers/azure-ad-b2c"
import CredentialsProvider from "next-auth/providers/credentials"

const B2C_TENANT = process.env.AZURE_AD_B2C_TENANT!;
const CLIENT_ID = process.env.AZURE_AD_B2C_CLIENT_ID!;
const POLICY_NAME = process.env.AZURE_AD_B2C_POLICY_NAME!;

declare module "next-auth" {
    interface Session {
        accessToken?: string;
        idToken?: string;
        user: {
            id: string;
            name?: string | null;
            email?: string | null;
            image?: string | null;
            roles?: string[];
        } & DefaultSession["user"];
    }

    interface User {
        accessToken?: string;
        idToken?: string;
        roles?: string[];
    }
}

export const authOptions = {
    providers: [
        CredentialsProvider({
            id: "azure-ad-b2c-ropc",
            name: "Credentials",
            credentials: {
                email: { label: "Email", type: "text" },
                password: { label: "Password", type: "password" }
            },
            async authorize(credentials) {
                try {
                    const tokenEndpoint = `https://${B2C_TENANT}.b2clogin.com/${B2C_TENANT}.onmicrosoft.com/${POLICY_NAME}/oauth2/v2.0/token`;

                    const bodyParams = {
                        grant_type: 'password',
                        client_id: CLIENT_ID,
                        scope: `${CLIENT_ID} offline_access openid`,
                        username: credentials?.email || '',
                        password: credentials?.password || '',
                        response_type: 'token id_token',
                    };

                    const response = await fetch(tokenEndpoint, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                        },
                        body: new URLSearchParams(bodyParams).toString(),
                    });

                    const responseText = await response.text();
                    let data;
                    try {
                        data = JSON.parse(responseText);
                    } catch (e) {
                        throw new Error(`Authentication failed: ${responseText}`);
                    }

                    if (!response.ok) {
                        throw new Error(data.error_description || 'Authentication failed');
                    }
                    
                    const decodedIDToken = jwtDecode(data.id_token) as any;
                    const decodedAccessToken = jwtDecode(data.access_token) as any;
                
                    return {
                        id: decodedIDToken.oid || decodedAccessToken.oid,
                        email: credentials?.email,
                        name: decodedIDToken.name,
                        accessToken: data.access_token,
                        idToken: data.id_token,
                        roles: decodedIDToken.roles || [],
                    };
                } catch (error) {
                    return null;
                }
            }
        }),
        AzureADB2C({
            clientId: process.env.AZURE_AD_B2C_CLIENT_ID!,
            clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET!,
            tenantId: process.env.AZURE_AD_B2C_TENANT!,
        })
    ],
    callbacks: {
        async jwt({ token, user }) {
            if (user) {
                token.accessToken = user.accessToken;
                token.idToken = user.idToken;
                token.roles = user.roles;
                token.name = user.name;
                token.id = user.id;
            }
            return token;
        },
        async session({ session, token }) {
            session.accessToken = token.accessToken;
            session.idToken = token.idToken;
            session.user.roles = token.roles as string[]
            if (session.user && token.id) {
                session.user.id = token.id as string;
            }
            return session;
        }
    }
}

关键点说明

  1. 类型扩展:通过声明模块扩展了 NextAuth.js 的 Session 和 User 类型,以支持 Azure AD B2C 的特定字段。

  2. 双重认证提供者:同时配置了 CredentialsProvider 和 AzureADB2C 提供者,前者用于资源所有者密码凭证流(ROPC),后者用于标准的 OAuth2 流程。

  3. 令牌处理:在 authorize 回调中正确处理了 Azure AD B2C 返回的令牌,包括解码和提取关键信息。

  4. 会话管理:通过 jwt 和 session 回调确保令牌和用户信息正确传递到会话中。

最佳实践

  1. 环境变量验证:确保所有必要的环境变量都已正确设置,特别是 Azure AD B2C 相关的配置。

  2. 错误处理:在关键操作如令牌获取和解析时添加充分的错误处理逻辑。

  3. 日志记录:在生产环境中添加适当的日志记录,便于排查问题。

  4. 类型安全:充分利用 TypeScript 的类型系统,确保自定义字段的类型安全。

通过以上配置和最佳实践,可以有效地解决 NextAuth.js 与 Azure AD B2C 集成中的常见问题,并构建一个稳定可靠的身份验证系统。

登录后查看全文
热门项目推荐

最新内容推荐

项目优选

收起
docsdocs
OpenHarmony documentation | OpenHarmony开发者文档
Dockerfile
139
1.91 K
kernelkernel
deepin linux kernel
C
22
6
nop-entropynop-entropy
Nop Platform 2.0是基于可逆计算理论实现的采用面向语言编程范式的新一代低代码开发平台,包含基于全新原理从零开始研发的GraphQL引擎、ORM引擎、工作流引擎、报表引擎、规则引擎、批处理引引擎等完整设计。nop-entropy是它的后端部分,采用java语言实现,可选择集成Spring框架或者Quarkus框架。中小企业可以免费商用
Java
8
0
ohos_react_nativeohos_react_native
React Native鸿蒙化仓库
C++
192
273
RuoYi-Vue3RuoYi-Vue3
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
923
551
openHiTLSopenHiTLS
旨在打造算法先进、性能卓越、高效敏捷、安全可靠的密码套件,通过轻量级、可剪裁的软件技术架构满足各行业不同场景的多样化要求,让密码技术应用更简单,同时探索后量子等先进算法创新实践,构建密码前沿技术底座!
C
421
392
openGauss-serveropenGauss-server
openGauss kernel ~ openGauss is an open source relational database management system
C++
145
189
金融AI编程实战金融AI编程实战
为非计算机科班出身 (例如财经类高校金融学院) 同学量身定制,新手友好,让学生以亲身实践开源开发的方式,学会使用计算机自动化自己的科研/创新工作。案例以量化投资为主线,涉及 Bash、Python、SQL、BI、AI 等全技术栈,培养面向未来的数智化人才 (如数据工程师、数据分析师、数据科学家、数据决策者、量化投资人)。
Jupyter Notebook
74
64
Cangjie-ExamplesCangjie-Examples
本仓将收集和展示高质量的仓颉示例代码,欢迎大家投稿,让全世界看到您的妙趣设计,也让更多人通过您的编码理解和喜爱仓颉语言。
Cangjie
344
1.3 K
easy-eseasy-es
Elasticsearch 国内Top1 elasticsearch搜索引擎框架es ORM框架,索引全自动智能托管,如丝般顺滑,与Mybatis-plus一致的API,屏蔽语言差异,开发者只需要会MySQL语法即可完成对Es的相关操作,零额外学习成本.底层采用RestHighLevelClient,兼具低码,易用,易拓展等特性,支持es独有的高亮,权重,分词,Geo,嵌套,父子类型等功能...
Java
36
8