如何在Cypress+TypeScript中实现Azure AD无交互式2FA登录自动化测试
非交互式实现Azure AD带2FA的Cypress+TypeScript登录方案
我之前刚好在项目里解决过类似的问题——用Cypress+TypeScript实现Azure AD的自动化登录,还要处理2FA的非交互式流程,之前交互式拿OTP确实很麻烦,给你分享几个经过验证的可行方案:
方案一:用TOTP算法程序化生成OTP(最适合测试环境)
如果你的测试用户是用Microsoft Authenticator App设置的2FA,那可以直接通过TOTP算法生成实时OTP,完全不需要人工干预。
步骤:
- 获取TOTP密钥:在用户设置2FA的时候,不要直接扫二维码,而是选择“显示二维码和密钥”,把那个base32格式的密钥保存下来(建议存在Cypress环境变量里,不要硬编码)。
- 安装TOTP库:在项目里安装
speakeasy(常用的TOTP生成库):npm install speakeasy --save-dev - 编写Cypress自定义命令:在
cypress/support/commands.ts里添加生成OTP和登录的命令:import * as speakeasy from 'speakeasy'; Cypress.Commands.add('azureAdLogin', () => { const username = Cypress.env('AZURE_AD_USERNAME'); const password = Cypress.env('AZURE_AD_PASSWORD'); const totpSecret = Cypress.env('AZURE_AD_TOTP_SECRET'); // 生成当前有效OTP(Azure AD的TOTP步长固定为30秒) const otp = speakeasy.totp({ secret: totpSecret, encoding: 'base32', step: 30, }); // 执行登录流程 cy.visit('/'); // 输入用户名 cy.get('#i0116').type(username); cy.get('#idSIButton9').click(); // 输入密码 cy.get('#i0118').type(password); cy.get('#idSIButton9').click(); // 输入生成的OTP cy.get('#idTxtBx_SAOTCC_OTC').type(otp); cy.get('#idSubmit_SAOTCC_Continue').click(); // 跳过"保持登录"提示 cy.get('#idBtn_Back').click(); }); - 配置环境变量:在
cypress.config.ts或者项目根目录的.env文件里添加敏感信息:AZURE_AD_USERNAME=your-test-user@domain.com AZURE_AD_PASSWORD=your-test-password AZURE_AD_TOTP_SECRET=your-base32-totp-secret
优缺点:
- ✅ 完全非交互式,适配CI/CD流水线
- ✅ 实现简单,不需要额外的Azure配置
- ❌ 需要提前获取并妥善存储TOTP密钥,注意不要提交到代码仓库
方案二:使用Azure AD Device Code Flow(适合无界面环境)
如果你的测试环境是无界面的CI/CD(比如GitHub Actions、Azure DevOps),Device Code Flow是更好的选择——它不需要在浏览器里输入任何内容,通过后台获取token后直接注入到Cypress中。
步骤:
- 配置Azure AD应用:
- 在Azure Portal注册一个应用,进入「身份验证」>「高级设置」,开启「设备代码流」
- 给应用添加目标资源的权限(比如
User.Read、offline_access等),并授予管理员同意
- 编写token获取脚本:在项目里创建
scripts/getAzureToken.ts脚本,用于获取access token:import axios from 'axios'; export async function getAzureToken() { const clientId = Cypress.env('AZURE_AD_CLIENT_ID'); const tenantId = Cypress.env('AZURE_AD_TENANT_ID'); const resource = Cypress.env('AZURE_AD_RESOURCE'); // 获取设备授权码 const deviceCodeRes = await axios.post( `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/devicecode`, new URLSearchParams({ client_id: clientId, scope: `${resource}/user.read offline_access`, }) ); const { device_code, expires_in } = deviceCodeRes.data; let tokenRes; const startTime = Date.now(); // 轮询等待token(测试环境可提前完成设备授权,避免每次等待) while (Date.now() - startTime < expires_in * 1000) { try { tokenRes = await axios.post( `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, new URLSearchParams({ client_id: clientId, device_code: device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', }) ); break; } catch (error) { if (error.response?.data.error === 'authorization_pending') { await new Promise(resolve => setTimeout(resolve, 5000)); } else { throw error; } } } return tokenRes?.data.access_token; } - 在Cypress中注入token:在测试用例里调用脚本获取token,然后注入到应用的存储中(取决于你的应用如何存储身份凭证):
import { getAzureToken } from '../scripts/getAzureToken'; describe('Azure AD Protected App', () => { before(async () => { const accessToken = await getAzureToken(); // 假设应用将token存在localStorage的'access_token'键中 cy.window().then(win => { win.localStorage.setItem('access_token', accessToken); }); }); it('should access protected page successfully', () => { cy.visit('/protected-page'); cy.get('#welcome-message').should('contain', 'Welcome'); }); });
优缺点:
- ✅ 完全无界面,适配各类CI/CD环境
- ✅ 不需要处理OTP输入,流程更稳定
- ❌ 需要额外配置Azure AD应用,首次授权需要手动完成(之后可复用refresh token)
方案三:使用Azure AD Graph API获取OTP(需要管理员权限)
如果你有Azure AD的管理员权限,可以通过Graph API给测试用户动态生成一次性OTP,适合多测试用户的场景。
步骤:
- 注册管理员应用:在Azure Portal注册一个应用,添加
UserAuthenticationMethod.ReadWrite.All的应用权限,并授予管理员同意。 - 编写获取OTP的函数:
import axios from 'axios'; export async function getAzureAdOtp(userId: string) { const clientId = Cypress.env('AZURE_AD_ADMIN_CLIENT_ID'); const clientSecret = Cypress.env('AZURE_AD_ADMIN_CLIENT_SECRET'); const tenantId = Cypress.env('AZURE_AD_TENANT_ID'); // 获取管理员身份token const adminTokenRes = await axios.post( `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: 'client_credentials', scope: 'https://graph.microsoft.com/.default', }) ); // 生成并获取OTP const otpRes = await axios.post( `https://graph.microsoft.com/v1.0/users/${userId}/authentication/phoneMethods/{phoneMethodId}/generateVerificationCode`, {}, { headers: { Authorization: `Bearer ${adminTokenRes.data.access_token}` }, } ); return otpRes.data.verificationCode; } - 在登录流程中使用:调用这个函数获取OTP,然后输入到登录界面即可。
优缺点:
- ✅ 动态生成OTP,不需要提前保存密钥
- ✅ 支持多测试用户场景
- ❌ 需要管理员权限,配置复杂度较高
- ❌ 存在Graph API调用次数限制
通用注意事项:
- 所有敏感信息(密码、密钥、client secret)务必用Cypress环境变量或CI/CD秘密变量存储,绝对不要硬编码
- 使用专门的测试账号,不要用生产环境账号
- 在CI/CD环境运行时,确保依赖库(如speakeasy、axios)已正确安装
内容的提问来源于stack exchange,提问作者yael berman
相关产品推荐
相关产品推荐

