Safari桌面端插件卡片出现invalid_request错误排查求助
Safari桌面端OIDC流程失败导致仪表盘插件卡片加载异常(invalid_request)
桌面端Safari中,自研及第三方(如Zogo)的仪表盘插件卡片均无法正常加载,仅显示invalid_request错误。
Chrome与Safari的核心差异在于OIDC流程的执行结果:插件卡片iframe通过src请求后端(外部应用的首个重定向URI)后,后端会重定向至带必要参数的Digital Garden OIDC授权地址,随后浏览器跳转至登录地址。Chrome可正常完成整个OIDC流程,但Safari中iframe无法访问Digital Garden的Cookie(如ConsumerJWT),导致系统判定用户需重新登录。
该功能近期曾正常运行,推测是Banno或Safari的更新导致集成故障,且问题在客户的多个Banno实例中均存在。已尝试以下方案但均无效:
- 关闭Safari的阻止跨站跟踪设置
- 启用分区Cookie(CHIPS)
- 禁用完整第三方Cookie拦截(ITP)
- 开启Intelligent Tracking Prevention调试模式,未发现拦截日志
最小复现代码
mre.js
/** * 最小复现示例 - 基于Express的OAuth 2.0 PKCE流程 * 此示例在单个文件中演示完整的OAuth流程 */ const express = require('express'); const https = require('https'); const fs = require('fs'); const crypto = require('crypto'); const fetch = require('node-fetch').default; const { jwtDecode } = require('jwt-decode'); const app = express(); // 配置项 const CONFIG = { app_port: 8080, api: { environment: 'https://digital.garden-fi.com', client_id: '', // TODO: 在此处填写你的Client ID client_secret: '' // TODO: 在此处填写你的Client Secret } }; // 内存存储state与code verifier的对应关系 const stateStore = new Map(); // PKCE辅助函数 function createPkcePair() { const codeVerifier = crypto.randomBytes(60).toString('hex').slice(0, 128); const codeChallenge = crypto.createHash('sha256') .update(Buffer.from(codeVerifier)) .digest('base64') .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); return { codeVerifier, codeChallenge }; } function createState() { return crypto.randomBytes(60).toString('hex').slice(0, 128); } // 令牌交换 async function exchangeAuthCode(baseUrl, clientId, clientSecret, authCode, redirectUri, codeVerifier) { const tokenUrl = `${baseUrl}/a/consumer/api/v0/oidc/token`; const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=authorization_code&code=${authCode}&redirect_uri=${redirectUri}&code_verifier=${codeVerifier}` }); const tokenResponse = await response.json(); return { accessToken: tokenResponse.access_token, idToken: jwtDecode(tokenResponse.id_token) }; } // 路由 app.get('/auth', async (req, res) => { const redirectUri = `https://localhost:${CONFIG.app_port}/auth/callback`; if (req.query.state) { stateStore.delete(req.query.state); } const { codeVerifier, codeChallenge } = createPkcePair(); const state = createState(); stateStore.set(state, codeVerifier); const authBaseURL = `${CONFIG.api.environment}/a/consumer/api/v0/oidc/auth`; const params = new URLSearchParams({ scope: 'openid profile https://api.banno.com/consumer/auth/accounts.readonly', response_type: 'code', client_id: CONFIG.api.client_id, redirect_uri: redirectUri, state: state, code_challenge: codeChallenge, code_challenge_method: 'S256', claims: JSON.stringify({ id_token: { 'https://api.banno.com/consumer/claim/institution_id': null }, userinfo: { 'https://api.banno.com/consumer/claim/institution_id': null } }) }); const authorizationURL = `${authBaseURL}?${params}`; console.log(`授权URL: ${authorizationURL}`); res.redirect(authorizationURL); }); app.get('/auth/callback', async (req, res) => { const redirectUri = `https://localhost:${CONFIG.app_port}/auth/callback`; if (!req.query.code || !req.query.state) { return res.status(400).send('授权错误:缺少code或state参数'); } const state = req.query.state; const codeVerifier = stateStore.get(state); stateStore.delete(state); if (!codeVerifier) { return res.status(400).send('授权错误:无效的state参数'); } try { const tokens = await exchangeAuthCode( CONFIG.api.environment, CONFIG.api.client_id, CONFIG.api.client_secret, req.query.code, redirectUri, codeVerifier ); console.log('身份令牌:', tokens.idToken); res.send('Hello World!'); } catch (error) { res.status(500).send(`错误:${error.message}`); } }); // 启动服务器 const options = { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }; https.createServer(options, app).listen(CONFIG.app_port, () => { console.log(`OAuth服务器运行在 https://localhost:${CONFIG.app_port}`); console.log(`启动授权流程:https://localhost:${CONFIG.app_port}/auth`); });
package.json
{ "scripts": { "start": "node mre.js" }, "dependencies": { "crypto": "^1.0.1", "express": "^5.2.1", "fs": "^0.0.1-security", "https": "^1.0.0", "jwt-decode": "^4.0.0", "node-fetch": "^3.3.2" } }
复现步骤
- 登录开发者门户:
- 在“Build external applications”板块复制Client ID和Client Secret,粘贴到
mre.js的对应配置项中 - 设置首个Primary重定向URI为
https://localhost:8080/auth,第二个为https://localhost:8080/auth/callback,保存设置
- 在“Build external applications”板块复制Client ID和Client Secret,粘贴到
- 配置插件:
- 设置标题为“My Test Plugin”
- 设置描述为“Test Description”
- 设置初始高度为300
- 点击保存
- 若未安装Node.js,先完成安装
- 将
mre.js和package.json放在同一目录下 - 打开终端进入该目录,执行以下命令:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" npm install npm run start
- 配置系统信任生成的
cert.pem证书,然后重启Safari - 通过Safari登录Digital Garden平台
- 观察插件卡片:Safari中显示invalid_request错误,而Chrome中正常显示“Hello World!”
内容的提问来源于stack exchange,提问作者Mark Coburn
相关产品推荐
相关产品推荐

