如何通过NextAuth与Boxyhq SAML-Jackson实现Azure AD SAML令牌转OAuth令牌
实现SAML令牌兑换为Azure AD OAuth令牌的方案
核心思路
利用Azure AD的On-Behalf-Of (OBO) 授权流程,将Boxyhq返回的SAML断言(即NextAuth中的access_token)兑换为可用于第三方API的OAuth令牌,全程无需用户额外操作。
步骤1:配置Azure AD权限与应用
- 在Azure AD中,确保你的SAML企业应用已启用OAuth2.0支持:
- 进入该应用的**“API权限”**页面,添加目标第三方API的委托权限(例如Microsoft Graph的
User.Read),并完成管理员同意。 - 注册一个单独的OAuth客户端应用(或复用现有SAML应用),记录其
client_id和client_secret,确保该应用被允许使用OBO流程。
- 进入该应用的**“API权限”**页面,添加目标第三方API的委托权限(例如Microsoft Graph的
- 确认Azure AD信任SAML令牌的签发者:在Azure AD的**“企业应用”**中,你的SAML应用的
Issuer值需与Boxyhq配置的issuer一致。
步骤2:从NextAuth/Boxyhq获取SAML断言
在NextAuth的认证流程中,Boxyhq会返回包含用户SAML断言的access_token(格式为JWT)。你可以在NextAuth的回调函数中获取该令牌,用于后续兑换。
步骤3:实现OBO令牌兑换逻辑
在NextAuth的服务器端回调中(如jwt或signIn回调),调用Azure AD的令牌端点完成兑换:
示例代码([...nextauth].js)
import NextAuth from "next-auth"; import BoxyhqSAML from "@boxyhq/saml-jackson"; export default NextAuth({ providers: [ BoxyhqSAML({ issuer: process.env.BOXYHQ_ISSUER, clientId: process.env.BOXYHQ_CLIENT_ID, clientSecret: process.env.BOXYHQ_CLIENT_SECRET, }), ], callbacks: { async jwt({ token, account }) { // 仅在用户首次登录或令牌更新时执行兑换 if (account?.access_token && !token.oauthAccessToken) { const tokenEndpoint = `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`; const formData = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", client_id: process.env.AZURE_OAUTH_CLIENT_ID, client_secret: process.env.AZURE_OAUTH_CLIENT_SECRET, assertion: account.access_token, scope: "https://graph.microsoft.com/User.Read", // 替换为目标API的权限范围 requested_token_use: "on_behalf_of", }); const response = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formData, }); const tokenData = await response.json(); // 将兑换得到的OAuth令牌存入JWT token.oauthAccessToken = tokenData.access_token; token.oauthRefreshToken = tokenData.refresh_token; token.oauthExpiresAt = Date.now() + tokenData.expires_in * 1000; } // 自动刷新过期的OAuth令牌 if (token.oauthRefreshToken && Date.now() > token.oauthExpiresAt - 60000) { const tokenEndpoint = `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`; const formData = new URLSearchParams({ grant_type: "refresh_token", client_id: process.env.AZURE_OAUTH_CLIENT_ID, client_secret: process.env.AZURE_OAUTH_CLIENT_SECRET, refresh_token: token.oauthRefreshToken, scope: "https://graph.microsoft.com/User.Read", }); const response = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formData, }); const tokenData = await response.json(); token.oauthAccessToken = tokenData.access_token; token.oauthRefreshToken = tokenData.refresh_token ?? token.oauthRefreshToken; token.oauthExpiresAt = Date.now() + tokenData.expires_in * 1000; } return token; }, async session({ session, token }) { // 将OAuth令牌注入session,供前端或API路由使用 session.user.oauthAccessToken = token.oauthAccessToken; return session; }, }, session: { strategy: "jwt" }, // 必须使用JWT策略存储令牌 });
关键注意事项
- 安全要求:
client_secret必须存储在环境变量中,绝对不能暴露给前端,兑换逻辑必须在服务器端执行(NextAuth的回调默认在服务器端运行)。 - 令牌有效性:确保Boxyhq返回的
access_token是有效的JWT,包含sub(用户唯一标识)、iss(签发者)等Azure AD认可的声明。 - 权限范围:
scope参数必须与目标第三方API要求的权限完全匹配,且已获得管理员同意。
内容的提问来源于stack exchange,提问作者jacktim
相关产品推荐
相关产品推荐

