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

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

2025-05-06 06:29:55作者:庞眉杨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 集成中的常见问题,并构建一个稳定可靠的身份验证系统。

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

热门内容推荐

最新内容推荐

项目优选

收起
ohos_react_nativeohos_react_native
React Native鸿蒙化仓库
C++
176
260
RuoYi-Vue3RuoYi-Vue3
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
854
505
openGauss-serveropenGauss-server
openGauss kernel ~ openGauss is an open source relational database management system
C++
129
182
openHiTLSopenHiTLS
旨在打造算法先进、性能卓越、高效敏捷、安全可靠的密码套件,通过轻量级、可剪裁的软件技术架构满足各行业不同场景的多样化要求,让密码技术应用更简单,同时探索后量子等先进算法创新实践,构建密码前沿技术底座!
C
254
295
ShopXO开源商城ShopXO开源商城
🔥🔥🔥ShopXO企业级免费开源商城系统,可视化DIY拖拽装修、包含PC、H5、多端小程序(微信+支付宝+百度+头条&抖音+QQ+快手)、APP、多仓库、多商户、多门店、IM客服、进销存,遵循MIT开源协议发布、基于ThinkPHP8框架研发
JavaScript
93
15
Cangjie-ExamplesCangjie-Examples
本仓将收集和展示高质量的仓颉示例代码,欢迎大家投稿,让全世界看到您的妙趣设计,也让更多人通过您的编码理解和喜爱仓颉语言。
Cangjie
331
1.08 K
HarmonyOS-ExamplesHarmonyOS-Examples
本仓将收集和展示仓颉鸿蒙应用示例代码,欢迎大家投稿,在仓颉鸿蒙社区展现你的妙趣设计!
Cangjie
397
370
note-gennote-gen
一款跨平台的 Markdown AI 笔记软件,致力于使用 AI 建立记录和写作的桥梁。
TSX
83
4
CangjieCommunityCangjieCommunity
为仓颉编程语言开发者打造活跃、开放、高质量的社区环境
Markdown
1.07 K
0
kernelkernel
deepin linux kernel
C
21
5