使用passport-azure-ad实现Azure AD B2C自定义SSO及Graph API用户管理问题
Azure AD B2C 自定义SSO登录/注册(Node.js + passport-azure-ad + Graph API)
需求背景
我要在Node.js中实现Azure AD B2C的SSO登录/注册功能,必须通过Graph API创建用户。因为需要根据用户信息生成二维码,所以流程要自定义,只需要获取用户对象和token即可。
当前配置
1. Passport Azure AD策略配置
ssoOptions = { identityMetadata: `https://login.microsoftonline.com/${this.TENANT_ID}/${this.VERSION}/.well-known/openid-configuration`, clientID: this.TENANT_ID, clientSecret: this.TENANT_SECRET, redirectUrl: this.TENANT_REDIRECT_URL, validateIssuer: true, isB2C: true, loggingLevel: 'warn', responseMode: 'form_post', responseType: 'code id_token', passReqToCallback: true, scope: ['openid', 'profile', 'email'] } passport.use('oidc-strategy', new OIDCStrategy(ssoOptions, (req: any, iss: any, profile: any, accessToken: any, refreshToken: any, done: any) => { try { const userProfile = { oid: profile.oid, displayName: profile.displayName, email: profile._json.preferred_username, accessToken, refreshToken } return done(null, userProfile) } catch (error) { console.error('ERROR', error); return done(error); } }))
2. 路由配置(返回401未授权)
api.get('/ssoSignUp/authenticate', passport.authenticate('oidc-strategy', { session: false }), (req, res) => { res.json(req.user); });
3. Azure AD B2C应用清单配置
"signInAudience": "AzureADandPersonalMicrosoftAccount",
4. Postman配置调整
- 初始Auth URL(报错):
https://login.microsoftonline.com/AppName.onmicrosoft.com/oauth2/v2.0/authorize - 调整后可获取token的Auth URL:
https://appName.b2clogin.com/appName.onmicrosoft.com/B2C_policy_name/oauth2/v2.0/authorize - Access Token URL:
https://login.microsoftonline.com/AppName.onmicrosoft.com/oauth2/v2.0/token - Callback URL:
https://localhost:3000/ - ClientID/ClientSecret:自定义值
- Scope:
openid profile email
5. React端MSAL配置
export const msalConfig = { auth: { clientId: '<clientID>', authority: `https://<tentantName>.b2clogin.com/<tentantName>.onmicrosoft.com/<policyName>/v2.0/`, redirectUri: 'http://localhost:3000', knownAuthorities: ['<tentantName>.b2clogin.com'], }, }; export const loginRequest = { scopes: ['openid', 'profile', 'User.Read'], };
遇到的问题
- Postman测试报错:输入Microsoft账号凭证后返回
AADSTS50020: User account 'xyz@abc.com' from identity provider 'live.com' does not exist in tenant 'AppName' and cannot access the application '123123'(AppName) in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account
- SSO界面无注册选项:当前仅显示登录入口,无法触发注册流程
核心需求
- 用户不存在时,通过Graph API在Azure AD B2C中创建用户
- 用户已存在时,更新其Azure ID等信息
解决方案步骤
一、修复B2C策略与Auth端点问题
- 确保使用注册+登录组合策略:确认你调整后的Auth URL中指定的B2C策略是「注册和登录」组合策略(而非仅登录策略),这样SSO界面才会显示注册选项。
- 修正Passport的identityMetadata:B2C环境需使用专属端点,替换为以下格式:
identityMetadata: `https://${this.TENANT_NAME}.b2clogin.com/${this.TENANT_NAME}.onmicrosoft.com/${this.B2C_POLICY_NAME}/v2.0/.well-known/openid-configuration`
原配置的login.microsoftonline.com是通用AD端点,不适合B2C场景,会导致验证逻辑错误。
二、解决401未授权问题
- 校验重定向URI一致性:确保Passport配置中的
redirectUrl与Azure B2C应用注册里的「重定向URI」完全匹配(包括协议、端口、路径)。 - 临时关闭Issuer验证:若使用自定义策略或多租户场景,可先设置
validateIssuer: false测试,后续再根据策略配置精准验证逻辑。
三、实现Graph API的用户创建/更新逻辑
1. 配置Graph API权限
在Azure B2C应用注册中添加应用权限:
User.ReadWrite.All(用于创建/更新用户)- 完成管理员同意授权,确保后端服务能调用Graph API。
2. 获取Graph API调用凭证
使用客户端凭证流获取Graph API的访问令牌:
import axios from 'axios'; async function getGraphAccessToken() { const tokenResponse = await axios.post(`https://${TENANT_NAME}.b2clogin.com/${TENANT_NAME}.onmicrosoft.com/oauth2/v2.0/token`, new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: 'https://graph.microsoft.com/.default', grant_type: 'client_credentials' })); return tokenResponse.data.access_token; }
3. 在Passport回调中实现用户逻辑
修改Passport策略的回调函数,加入用户检查、创建/更新逻辑:
passport.use('oidc-strategy', new OIDCStrategy(ssoOptions, async (req: any, iss: any, profile: any, accessToken: any, refreshToken: any, done: any) => { try { const graphToken = await getGraphAccessToken(); const userEmail = profile._json.preferred_username; // 1. 检查用户是否存在于B2C租户 const existingUserResponse = await axios.get(`https://graph.microsoft.com/v1.0/users?$filter=mail eq '${userEmail}'`, { headers: { Authorization: `Bearer ${graphToken}` } }); let b2cUser; if (existingUserResponse.data.value.length === 0) { // 2. 不存在则创建用户 b2cUser = await axios.post('https://graph.microsoft.com/v1.0/users', { accountEnabled: true, displayName: profile.displayName, mailNickname: profile.displayName.replace(/\s+/g, ''), userPrincipalName: `${userEmail.split('@')[0]}@${TENANT_NAME}.onmicrosoft.com`, mail: userEmail, passwordProfile: { forceChangePasswordNextSignIn: false, password: generateRandomPassword() // 生成临时密码,或按业务逻辑处理 }, identities: [ { signInType: 'emailAddress', issuer: `${TENANT_NAME}.onmicrosoft.com`, issuerAssignedId: userEmail } ] }, { headers: { Authorization: `Bearer ${graphToken}` } }); } else { // 3. 存在则更新用户信息 b2cUser = await axios.patch(`https://graph.microsoft.com/v1.0/users/${existingUserResponse.data.value[0].id}`, { displayName: profile.displayName, // 其他需要更新的字段 }, { headers: { Authorization: `Bearer ${graphToken}` } }); } // 返回自定义用户对象 const userProfile = { oid: profile.oid, b2cUserId: b2cUser.data.id, displayName: profile.displayName, email: userEmail, accessToken, refreshToken }; return done(null, userProfile); } catch (error) { console.error('ERROR', error); return done(error); } }));
四、调整React端MSAL配置
确保loginRequest的scope与后端Passport配置一致,同时确认authority中的策略为注册登录组合策略:
export const loginRequest = { scopes: ['openid', 'profile', 'email'], };
内容的提问来源于stack exchange,提问作者Scripter9700
相关产品推荐
相关产品推荐

