You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Cypress+TypeScript中实现Azure AD无交互式2FA登录自动化测试

非交互式实现Azure AD带2FA的Cypress+TypeScript登录方案

我之前刚好在项目里解决过类似的问题——用Cypress+TypeScript实现Azure AD的自动化登录,还要处理2FA的非交互式流程,之前交互式拿OTP确实很麻烦,给你分享几个经过验证的可行方案:

方案一:用TOTP算法程序化生成OTP(最适合测试环境)

如果你的测试用户是用Microsoft Authenticator App设置的2FA,那可以直接通过TOTP算法生成实时OTP,完全不需要人工干预。

步骤:

  1. 获取TOTP密钥:在用户设置2FA的时候,不要直接扫二维码,而是选择“显示二维码和密钥”,把那个base32格式的密钥保存下来(建议存在Cypress环境变量里,不要硬编码)。
  2. 安装TOTP库:在项目里安装speakeasy(常用的TOTP生成库):
    npm install speakeasy --save-dev
    
  3. 编写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();
    });
    
  4. 配置环境变量:在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中。

步骤:

  1. 配置Azure AD应用:
    • 在Azure Portal注册一个应用,进入「身份验证」>「高级设置」,开启「设备代码流」
    • 给应用添加目标资源的权限(比如User.Read、offline_access等),并授予管理员同意
  2. 编写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;
    }
    
  3. 在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,适合多测试用户的场景。

步骤:

  1. 注册管理员应用:在Azure Portal注册一个应用,添加UserAuthenticationMethod.ReadWrite.All的应用权限,并授予管理员同意。
  2. 编写获取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;
    }
    
  3. 在登录流程中使用:调用这个函数获取OTP,然后输入到登录界面即可。

优缺点:

  • ✅ 动态生成OTP,不需要提前保存密钥
  • ✅ 支持多测试用户场景
  • ❌ 需要管理员权限,配置复杂度较高
  • ❌ 存在Graph API调用次数限制

通用注意事项:

  • 所有敏感信息(密码、密钥、client secret)务必用Cypress环境变量或CI/CD秘密变量存储,绝对不要硬编码
  • 使用专门的测试账号,不要用生产环境账号
  • 在CI/CD环境运行时,确保依赖库(如speakeasy、axios)已正确安装

内容的提问来源于stack exchange,提问作者yael berman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 14:32:47