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

Firebase Auth用户登录Budibase方案咨询:简化自定义OIDC提供商可行吗?

问题背景

我们正在使用Budibase开发一款内部应用,该应用需与要求在Authorization头中携带Firebase Authentication令牌的REST API交互。Budibase支持为每个API调用设置该请求头,但目前没有内置的Firebase Auth登录方式。我们可选择的登录方式为Google或OpenID Connect,因此想咨询:

  1. 能否将Firebase Auth作为OIDC提供商使用(而非像Identity Platform那样作为OIDC客户端)?
  2. 已知答案是否定的,我们调研了使用Node.js的oidc-provider搭建自定义OIDC提供商以衔接Firebase与Budibase的方案,但该方案包含大量我们不需要的OIDC功能,过于复杂。请问在不影响安全性的前提下,若精简功能(例如仅支持单个可硬编码/通过环境变量配置的客户端),是否有更简便的搭建方式?

解决方案

首先明确:Firebase Auth无法直接作为OIDC身份提供商

Firebase Auth的定位是OIDC客户端(对接Google、Facebook等第三方身份源),本身并不暴露OIDC标准的授权、令牌等端点,因此必须搭建中间层来衔接Firebase Auth与Budibase的OIDC登录需求。

方案1:基于Express的极简OIDC代理(仅支持单个客户端)

只实现Budibase所需的OIDC核心流程,砍掉所有冗余功能,核心逻辑如下:

  • 仅允许预配置的Budibase客户端接入(通过环境变量硬编码客户端ID/密钥)
  • 复用Firebase Auth的登录流程,验证用户身份后生成符合OIDC标准的令牌返回给Budibase
  • 仅实现OIDC授权码流必需的3个端点:授权端点、令牌端点、用户信息端点

简化代码示例

const express = require('express');
const jwt = require('jsonwebtoken');
const admin = require('firebase-admin');
const app = express();
app.use(express.json());

// 环境变量配置(生产环境用环境变量,不要硬编码)
const CONFIG = {
  BUDIBASE_CLIENT_ID: process.env.BUDIBASE_CLIENT_ID,
  BUDIBASE_CLIENT_SECRET: process.env.BUDIBASE_CLIENT_SECRET,
  BUDIBASE_REDIRECT_URI: process.env.BUDIBASE_REDIRECT_URI,
  JWT_SIGNING_KEY: process.env.JWT_SIGNING_KEY, // 生产环境建议用RSA私钥
  FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID
};

// 初始化Firebase Admin SDK
admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  projectId: CONFIG.FIREBASE_PROJECT_ID
});

// 1. OIDC授权端点:跳转到自定义Firebase登录页
app.get('/auth', (req, res) => {
  const state = req.query.state || '';
  // 返回嵌入Firebase Web SDK的登录页面
  res.send(`
    <html>
      <body>
        <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app.js"></script>
        <script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-auth.js"></script>
        <script>
          const firebaseConfig = { /* 你的Firebase Web配置 */ };
          firebase.initializeApp(firebaseConfig);
          async function login() {
            const provider = new firebase.auth.GoogleAuthProvider();
            const result = await firebase.auth().signInWithPopup(provider);
            const idToken = await result.user.getIdToken();
            // 登录成功后回调到代理的回调端点
            fetch('/callback', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ firebaseIdToken: idToken, state: "${state}" })
            }).then(res => res.redirect(res.url));
          }
        </script>
        <button onclick="login()">登录</button>
      </body>
    </html>
  `);
});

// 2. 登录回调:验证Firebase令牌,生成OIDC授权码并重定向到Budibase
app.post('/callback', async (req, res) => {
  try {
    const { firebaseIdToken, state } = req.body;
    // 验证Firebase ID Token的合法性
    const decodedFirebaseToken = await admin.auth().verifyIdToken(firebaseIdToken);
    
    // 生成临时授权码(生产环境建议存在Redis/内存中,带过期时间)
    const authCode = Math.random().toString(36).substring(2, 15);
    
    // 重定向回Budibase的回调地址,携带授权码和state
    const redirectUrl = new URL(CONFIG.BUDIBASE_REDIRECT_URI);
    redirectUrl.searchParams.set('code', authCode);
    redirectUrl.searchParams.set('state', state);
    res.redirect(redirectUrl.toString());
  } catch (err) {
    res.status(401).send('Invalid Firebase Token');
  }
});

// 3. OIDC令牌端点:用授权码换取OIDC ID Token
app.post('/token', async (req, res) => {
  try {
    const { code, client_id, client_secret } = req.body;
    // 验证客户端身份
    if (client_id !== CONFIG.BUDIBASE_CLIENT_ID || client_secret !== CONFIG.BUDIBASE_CLIENT_SECRET) {
      return res.status(401).json({ error: 'invalid_client' });
    }
    
    // 验证授权码(生产环境需校验存储的授权码有效性)
    // 简化处理,直接生成OIDC ID Token
    const oidcIdToken = jwt.sign({
      sub: decodedFirebaseToken.uid,
      name: decodedFirebaseToken.name,
      email: decodedFirebaseToken.email,
      email_verified: decodedFirebaseToken.email_verified,
      iss: 'https://your-proxy-domain.com', // 你的代理域名
      aud: CONFIG.BUDIBASE_CLIENT_ID,
      exp: Math.floor(Date.now() / 1000) + 3600, // 1小时过期
      iat: Math.floor(Date.now() / 1000)
    }, CONFIG.JWT_SIGNING_KEY, { algorithm: 'RS256' });
    
    res.json({
      id_token: oidcIdToken,
      token_type: 'Bearer',
      expires_in: 3600
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 4. OIDC用户信息端点(Budibase可能需要)
app.get('/userinfo', async (req, res) => {
  try {
    const authHeader = req.headers.authorization;
    if (!authHeader) return res.status(401).json({ error: 'unauthorized' });
    
    const token = authHeader.split(' ')[1];
    const decodedOidcToken = jwt.verify(token, CONFIG.JWT_SIGNING_KEY);
    const firebaseUser = await admin.auth().getUser(decodedOidcToken.sub);
    
    res.json({
      sub: firebaseUser.uid,
      name: firebaseUser.displayName,
      email: firebaseUser.email,
      email_verified: firebaseUser.emailVerified
    });
  } catch (err) {
    res.status(401).json({ error: 'invalid_token' });
  }
});

app.listen(3000, () => console.log('OIDC Proxy running on port 3000'));

优势

  • 仅保留必要功能,代码量少,易于维护
  • 所有敏感配置通过环境变量管理,避免硬编码
  • 安全层面:验证Firebase令牌合法性,用非对称密钥签名OIDC令牌,防止篡改

方案2:无服务器代理(Firebase Cloud Functions/Vercel Edge Functions)

如果不想维护服务器,可以把上述代理逻辑部署为无服务器函数,比如Firebase Cloud Functions:

  • 将代码适配为云函数的格式
  • 环境变量配置在云函数的控制台中
  • 优势:自动扩缩容,无需服务器运维成本

方案3:直接修改Budibase登录流程(跳过OIDC中间层)

如果你的技术团队能修改Budibase的前端代码,这是最直接的方案:

  • 在Budibase的登录页面嵌入Firebase Web SDK,实现Firebase登录
  • 将用户的Firebase ID Token存储到Budibase的用户会话元数据中
  • 配置Budibase的API集成时,从用户元数据中读取令牌,自动添加到Authorization头

优势

  • 无需搭建额外的OIDC代理层
  • 流程更短,减少潜在故障点

安全注意事项

  1. 令牌验证:必须用Firebase Admin SDK验证Firebase ID Token的合法性,防止伪造
  2. 密钥管理:OIDC令牌的签名密钥要用RSA非对称密钥,私钥妥善保管,公钥暴露给Budibase用于验证令牌
  3. CSRF防护:授权流程中必须携带state参数,防止跨站请求伪造
  4. 令牌过期:OIDC令牌的过期时间设置为1小时以内,避免长期有效
  5. 客户端校验:仅允许预配置的Budibase客户端接入,拒绝未知客户端的请求

内容的提问来源于stack exchange,提问作者Hannes Hultergård

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 00:44:50