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

Microsoft Teams应用OAuth用户授权场景下获取refresh_token问题

问题解决:Teams应用手动授权时获取refresh_token的方案

你当前手动授权流程使用的是隐式授权流(Implicit Grant Flow),该流程设计上不返回refresh_token(纯前端交互,无法安全存储敏感令牌)。要实现离线访问获取refresh_token,必须切换到授权码流+PKCE(Authorization Code Flow with PKCE),这是适合公共客户端(如Teams前端应用)的安全方案。


一、核心修改思路

  1. 前端发起授权请求时,切换response_type为code,并生成PKCE所需的code_verifier和code_challenge
  2. 用户授权后,前端将授权码code和code_verifier传给后端
  3. 后端通过授权码调用Azure AD的/token接口,兑换access_token和refresh_token
  4. 后端存储refresh_token,用于后续静默刷新令牌

二、代码修改

1. 前端授权流程调整(ConsentPopup.js)

生成PKCE参数,切换到授权码流:

class ConsentPopup extends React.Component {
  componentDidMount() {
    console.log("consentPopUp initialized");
    microsoftTeams.initialize();

    // 生成PKCE code_verifier并存储
    const codeVerifier = crypto.randomBytes(32).toString('base64url');
    sessionStorage.setItem('code_verifier', codeVerifier);

    // 生成code_challenge(SHA-256哈希)
    async function generateCodeChallenge(verifier) {
      const encoder = new TextEncoder();
      const data = encoder.encode(verifier);
      const digest = await crypto.subtle.digest('SHA-256', data);
      return btoa(String.fromCharCode(...new Uint8Array(digest)))
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
    }

    microsoftTeams.getContext(async (context, error) => {
      let tenant = context["tid"];
      let client_id = env.REACT_APP_AZURE_APP_REGISTRATION_ID;
      const codeChallenge = await generateCodeChallenge(codeVerifier);

      let queryParams = {
        tenant: `${tenant}`,
        client_id: `${client_id}`,
        response_type: "code", // 切换为授权码流
        response_mode: "query", // 用query参数返回code
        scope: "https://graph.microsoft.com/User.Read https://graph.microsoft.com/TeamsActivity.Send offline_access",
        redirect_uri: window.location.origin + "/auth-end",
        nonce: crypto.randomBytes(16).toString("base64"),
        code_challenge: codeChallenge,
        code_challenge_method: "S256"
      };

      let url = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?`;
      queryParams = new URLSearchParams(queryParams).toString();
      window.location.assign(url + queryParams);
    });
  }

  render() {
    return <div><h1>Please wait...</h1></div>;
  }
}

2. 前端授权结果处理(ClosePopup.js)

将授权码传给后端兑换令牌:

class ClosePopup extends React.Component {
  componentDidMount(){
    microsoftTeams.initialize();
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');
    const codeVerifier = sessionStorage.getItem('code_verifier');
    
    if (code && codeVerifier) {
      // 调用后端接口兑换令牌
      fetch('/auth/consent-code', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
          code,
          codeVerifier,
          tenantId: urlParams.get('tid')
        })
      })
      .then(res => res.json())
      .then(data => {
        if (data.access_token && data.refresh_token) {
          microsoftTeams.authentication.notifySuccess({
            accessToken: data.access_token,
            refreshToken: data.refresh_token
          });
        } else {
          microsoftTeams.authentication.notifyFailure("令牌兑换失败");
        }
      })
      .catch(err => {
        microsoftTeams.authentication.notifyFailure(`兑换错误: ${err.message}`);
      });
    } else {
      microsoftTeams.authentication.notifyFailure("未获取到授权码");
    }
  }

  render() {
    return <div><h1>授权流程完成</h1></div>;
  }
}

export default ClosePopup;

3. 后端新增授权码兑换接口

接收前端传来的code,调用Azure AD获取令牌:

router.post('/auth/consent-code', async (req, res) => {
  try {
    const { code, codeVerifier, tenantId } = req.body;
    if (!code || !codeVerifier || !tenantId) {
      return res.status(400).json({ errors: [{ msg: '缺少必要参数' }] });
    }

    const tokenQueryParams = new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: MS_CLIENT_ID,
      client_secret: MS_CLIENT_SECRET, // SPA应用可省略此参数,依赖PKCE保证安全
      code: code,
      redirect_uri: `${process.env.FRONTEND_URL}/auth-end`, // 必须与前端授权请求的redirect_uri一致
      code_verifier: codeVerifier,
      scope: graphScopes // 与前端scope保持一致
    }).toString();

    const { data, status } = await axios({
      method: 'POST',
      url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      data: tokenQueryParams,
    });

    if (status !== 200) throw new Error('授权码兑换失败');

    // 此处可将refresh_token存储到数据库,用于后续刷新access_token
    console.log({ accessToken: data.access_token, refreshToken: data.refresh_token });

    return res.status(200).json(data);
  } catch (error) {
    console.error({
      function: 'consentCodeExchange',
      error: error.response?.data || error.message
    });
    return res.status(403).json({ errors: [{ msg: '授权流程完成失败' }] });
  }
});

三、Azure AD应用注册配置检查

  1. 认证设置:启用授权码流,SPA应用需勾选"允许公共客户端流"
  2. 重定向URI:添加前端/auth-end地址,类型选择"单页应用(SPA)"
  3. API权限:确保已添加User.Read、TeamsActivity.Send,且offline_access权限已包含(用户授权时自动生效)

内容的提问来源于stack exchange,提问作者Soumya Dey

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:44:52