Microsoft Teams应用OAuth用户授权场景下获取refresh_token问题
问题解决:Teams应用手动授权时获取refresh_token的方案
你当前手动授权流程使用的是隐式授权流(Implicit Grant Flow),该流程设计上不返回refresh_token(纯前端交互,无法安全存储敏感令牌)。要实现离线访问获取refresh_token,必须切换到授权码流+PKCE(Authorization Code Flow with PKCE),这是适合公共客户端(如Teams前端应用)的安全方案。
一、核心修改思路
- 前端发起授权请求时,切换
response_type为code,并生成PKCE所需的code_verifier和code_challenge - 用户授权后,前端将授权码
code和code_verifier传给后端 - 后端通过授权码调用Azure AD的
/token接口,兑换access_token和refresh_token - 后端存储
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应用注册配置检查
- 认证设置:启用授权码流,SPA应用需勾选"允许公共客户端流"
- 重定向URI:添加前端
/auth-end地址,类型选择"单页应用(SPA)" - API权限:确保已添加
User.Read、TeamsActivity.Send,且offline_access权限已包含(用户授权时自动生效)
内容的提问来源于stack exchange,提问作者Soumya Dey
相关产品推荐
相关产品推荐

