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

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,保存设置
  • 配置插件:
    • 设置标题为“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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.02 06:37:28