集成OAuth2的Google Workspace日历会议插件重定向时CORS失败
Google Apps Script OAuth2 重定向到usercallback时CORS错误解决方案
问题背景
我用App Script和OAuth2库构建了Google Workspace日历会议插件,OAuth客户端配置如下:
return ( OAuth2.createService("OAuthApp") .setAuthorizationBaseUrl(`${FRONTEND_DOMAIN}/auth/login`) .setTokenUrl(`${BACKEND_DOMAIN}/v1.0/oauth/exchange-code-for-token`) .setClientId(CLIENT_ID) .setClientSecret(CLIENT_SECRET) .setCallbackFunction("authCallback") .setPropertyStore(PropertiesService.getUserProperties()) .setCache(CacheService.getUserCache()) .setLock(LockService.getUserLock()) );
用户在前端登录页面完成验证后,后端通过302重定向到插件的https://script.google.com/macros/d/{SCRIPT_ID}/usercallback地址并携带授权码,但此时浏览器控制台报CORS错误:
Access to XMLHttpRequest at 'https://script.google.com/macros/d/{SCRIPT_ID}/usercallback?code=xxx' (redirected from 'https://{BACKEND_ENDPOINT}/oauth/verify-otp') from origin 'https://{FRONTEND_ENDPOINT}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
插件清单已配置必要的OAuth权限和Web App设置,尝试修改Content-Type避免预检请求无效,期望完成授权码与令牌的交换。
原因分析
Google Script的usercallback端点不支持CORS跨域AJAX请求,它的设计目标是接收浏览器直接导航的请求,而非AJAX响应的重定向。当前流程中,前端通过AJAX请求后端,后端返回302后浏览器会以AJAX方式发起重定向请求,触发预检OPTIONS请求,但Google未在该端点设置允许跨域的响应头,导致CORS拦截。
解决方案
1. 改用浏览器弹窗完成授权流程
放弃AJAX请求,在插件中通过新弹窗/标签页打开前端登录页面,让浏览器直接导航完成授权:
function initAuthFlow() { const authService = getOAuthService(); // 你的OAuth服务获取函数 const authUrl = authService.getAuthorizationUrl(); window.open(authUrl, 'AuthPopup', 'width=600,height=700,toolbar=no'); }
前端登录页面需要保留OAuth2库自动生成的state参数(会包含在authUrl中),验证成功后传递给后端。
2. 后端重定向时携带完整参数
后端验证用户身份后,必须将code和state两个参数一起拼接到重定向URL中,确保OAuth2库能验证请求合法性:
# 示例:后端(Flask)重定向逻辑 @app.route("/oauth/verify-otp") def verify_otp(): # 执行OTP验证逻辑... auth_code = generate_authorization_code() state = request.args.get("state") # 从前端登录页面URL获取state callback_url = f"https://script.google.com/macros/d/{SCRIPT_ID}/usercallback?code={auth_code}&state={state}" return redirect(callback_url, code=302)
3. 完善authCallback函数
确保插件的回调函数正确处理授权响应,完成令牌交换:
function authCallback(request) { const service = getOAuthService(); const isAuthorized = service.handleCallback(request); if (isAuthorized) { // 授权成功,自动关闭弹窗返回插件 return HtmlService.createHtmlOutput(` <html> <body> <p>授权成功!请关闭此窗口返回日历插件。</p> <script>window.close();</script> </body> </html> `); } else { return HtmlService.createHtmlOutput("<p>授权失败,请重试。</p>"); } }
4. 验证Web App配置
确认插件的appsscript.json中Web App设置正确:
"webapp": { "access": "ANYONE", "executeAs": "USER_ACCESSING" }
同时确保SCRIPT_ID与插件的实际ID一致。
注意事项
- 禁止用AJAX发起授权流程,必须通过浏览器直接导航(弹窗/新标签页);
state参数必须完整传递,OAuth2库依赖它防止CSRF攻击,缺失会导致授权失败;- 后端重定向必须是302状态码的浏览器跳转,不能是AJAX响应中的重定向指令。
内容的提问来源于stack exchange,提问作者Gurbaj Singh
相关产品推荐
相关产品推荐

