使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌

Posted

技术标签:

【中文标题】使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌【英文标题】:Handling Firebase ID tokens on the client side with vanilla JavaScript 【发布时间】:2018-07-30 18:05:15 【问题描述】:

我正在用原生 javascript 编写 Firebase 应用程序。我正在为 Web 使用 Firebase 身份验证和 FirebaseUI。我正在使用 Firebase Cloud Functions 来实现一个服务器,该服务器接收对我的页面路由的请求并返回呈现的 html。我正在努力寻找在客户端使用经过身份验证的 ID 令牌访问 Firebase Cloud Function 提供的受保护路由的最佳做法。

我相信我理解了基本流程:用户登录,这意味着一个ID令牌被发送到客户端,在onAuthStateChanged回调中接收它,然后插入到任何新HTTP的Authorization字段中使用正确的前缀请求,然后在用户尝试访问受保护的路由时由服务器检查。

我不明白应该如何处理 onAuthStateChanged 回调中的 ID 令牌,或者我应该如何修改客户端 JavaScript 以在必要时修改请求标头。

我正在使用 Firebase Cloud Functions 来处理路由请求。这是我的functions/index.js,它导出了所有请求都重定向到的app 方法以及检查ID 令牌的位置:

const functions = require('firebase-functions')
const admin = require('firebase-admin')
const express = require('express')
const cookieParser = require('cookie-parser')
const cors = require('cors')

const app = express()
app.use(cors( origin: true ))
app.use(cookieParser())

admin.initializeApp(functions.config().firebase)

const firebaseAuthenticate = (req, res, next) => 
  console.log('Check if request is authorized with Firebase ID token')

  if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
    !req.cookies.__session) 
    console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
      'Make sure you authorize your request by providing the following HTTP header:',
      'Authorization: Bearer <Firebase ID Token>',
      'or by passing a "__session" cookie.')
    res.status(403).send('Unauthorized')
    return
  

  let idToken
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) 
    console.log('Found "Authorization" header')
    // Read the ID Token from the Authorization header.
    idToken = req.headers.authorization.split('Bearer ')[1]
   else 
    console.log('Found "__session" cookie')
    // Read the ID Token from cookie.
    idToken = req.cookies.__session
  

  admin.auth().verifyIdToken(idToken).then(decodedIdToken => 
    console.log('ID Token correctly decoded', decodedIdToken)
    console.log('token details:', JSON.stringify(decodedIdToken))

    console.log('User email:', decodedIdToken.firebase.identities['google.com'][0])

    req.user = decodedIdToken
    return next()
  ).catch(error => 
    console.error('Error while verifying Firebase ID token:', error)
    res.status(403).send('Unauthorized')
  )


const meta = `<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.css" />

const logic = `<!-- Intialization -->
<script src="https://www.gstatic.com/firebasejs/4.10.0/firebase.js"></script>
<script src="/init.js"></script>

<!-- Authentication -->
<script src="https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.js"></script>
<script src="/auth.js"></script>`

app.get('/', (request, response) => 
  response.send(`<html>
  <head>
    <title>Index</title>

    $meta
  </head>
  <body>
    <h1>Index</h1>

    <a href="/user/fake">Fake User</a>

    <div id="firebaseui-auth-container"></div>

    $logic
  </body>
</html>`)
)

app.get('/user/:name', firebaseAuthenticate, (request, response) => 
  response.send(`<html>
  <head>
    <title>User - $request.params.name</title>

    $meta
  </head>
  <body>
    <h1>User $request.params.name</h1>

    $logic
  </body>
</html>`)
)

exports.app = functions.https.onRequest(app)

她是我的functions/package.json,它描述了处理 HTTP 请求的服务器的配置,实现为 Firebase Cloud Function:


  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": 
    "lint": "./node_modules/.bin/eslint .",
    "serve": "firebase serve --only functions",
    "shell": "firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  ,
  "dependencies": 
    "cookie-parser": "^1.4.3",
    "cors": "^2.8.4",
    "eslint-config-standard": "^11.0.0-beta.0",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-node": "^6.0.0",
    "eslint-plugin-standard": "^3.0.1",
    "firebase-admin": "~5.8.1",
    "firebase-functions": "^0.8.1"
  ,
  "devDependencies": 
    "eslint": "^4.12.0",
    "eslint-plugin-promise": "^3.6.0"
  ,
  "private": true

这是我的firebase.json,它将所有页面请求重定向到我导出的app 函数:


  "functions": 
    "predeploy": [
      "npm --prefix $RESOURCE_DIR run lint"
    ]
  ,
  "hosting": 
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      
        "source": "**",
        "function": "app"
      
    ]
  

这是我的public/auth.js,在客户端请求和接收令牌。这就是我卡住的地方:

/* global firebase, firebaseui */

const uiConfig = 
  // signInSuccessUrl: '<url-to-redirect-to-on-success>',
  signInOptions: [
    // Leave the lines as is for the providers you want to offer your users.
    firebase.auth.GoogleAuthProvider.PROVIDER_ID,
    // firebase.auth.FacebookAuthProvider.PROVIDER_ID,
    // firebase.auth.TwitterAuthProvider.PROVIDER_ID,
    // firebase.auth.GithubAuthProvider.PROVIDER_ID,
    firebase.auth.EmailAuthProvider.PROVIDER_ID
    // firebase.auth.PhoneAuthProvider.PROVIDER_ID
  ],
  callbacks: 
    signInSuccess ()  return false 
  
  // Terms of service url.
  // tosUrl: '<your-tos-url>'

const ui = new firebaseui.auth.AuthUI(firebase.auth())
ui.start('#firebaseui-auth-container', uiConfig)

firebase.auth().onAuthStateChanged(function (user) 
  if (user) 
    firebase.auth().currentUser.getIdToken().then(token => 
      console.log('You are an authorized user.')

      // This is insecure. What should I do instead?
      // document.cookie = '__session=' + token
    )
   else 
    console.warn('You are an unauthorized user.')
  
)

在客户端我应该如何处理经过身份验证的 ID 令牌?

Cookies/localStorage/webStorage 似乎不是完全安全的,至少不是我能找到的任何相对简单和可扩展的方式。可能有一个简单的基于 cookie 的过程,它与直接在请求标头中包含令牌一样安全,但我无法找到可以轻松应用于 Firebase 的代码。

我知道如何在 AJAX 请求中包含令牌,例如:

var xhr = new XMLHttpRequest()
xhr.open('GET', URL)
xmlhttp.setRequestHeader("Authorization", 'Bearer ' + token)
xhr.onload = function () 
    if (xhr.status === 200) 
        alert('Success: ' + xhr.responseText)
    
    else 
        alert('Request failed.  Returned status of ' + xhr.status)
    

xhr.send()

但是,我不想制作单页应用程序,所以我不能使用 AJAX。我无法弄清楚如何将令牌插入到正常路由请求的标头中,例如通过单击具有有效href 的锚标记触发的请求。我应该拦截这些请求并以某种方式修改它们吗?

在 Firebase for Web 应用程序(不是单页应用程序)中实现可扩展客户端安全性的最佳做法是什么?我不需要复杂的身份验证流程。我愿意为一个我可以信任和简单实施的安全系统牺牲灵活性。

【问题讨论】:

这取决于您的应用架构(您使用的是其他 Firebase 服务、单页应用还是传统 Web 应用、托管您自己的服务器等)。一种选择是在登录后,将 ID 令牌发送到您的后端并进行验证。您可以检查auth_time 以获取最近的登录信息,然后使用 expressjs 会话发出会话 cookie。 我唯一的后端是托管在 firebase 云功能中的快速服务器。我不想做单页应用程序。快速服务器构建一个 html 字符串并将其直接发送给用户,如我的代码所示。有没有办法使用 cookie 的 auth_time 来完全保护路由?你能提供一个答案来说明如何做到这一点吗? 提供更多关于你的设置的信息,如果可能的话发布 package.json 我已经包含了我的package.jsonfirebase.json。我的帖子现在包括我的所有应用程序,而不是我的 public/init.js,它只运行从 Firebase 控制台复制的初始化代码。在服务器端,Firebase Cloud Function 接收所有请求并以呈现的 HTML 进行响应。我使用 FirebaseUI 文档中的代码来验证对受保护路由的请求。在客户端,用户使用 FirebaseUI 登录并接收 ID 令牌。他们需要在 HTTP 请求中将该令牌发送到服务器以访问受保护的路由。我怎样才能以完全安全的方式? 您为什么认为 cookie 不安全?你在担心什么? 【参考方案1】:

为什么 cookie 不受保护?

    Cookie 数据很容易修改,如果开发人员愚蠢到将登录用户的角色存储在 cookie 中,用户可以轻松更改他的 cookie 数据,document.cookie = "role=admin"。 (瞧!) “Cookie 数据很容易被黑客通过 XSS 攻击获取并登录到您的帐户。 “Cookie 数据可以很容易地从您的浏览器中收集,您的室友可以窃取您的 Cookie 并在他的计算机上以您的身份登录。 ‎如果您不使用 SSL,任何监控您的网络流量的人都可以收集您的 cookie。

你需要担心吗?

    我们不会在 cookie 中存储任何愚蠢的东西,用户可以修改这些内容以获得任何未经授权的访问。 ‎如果黑客可以通过 XSS 攻击获取 cookie 数据,如果我们不使用单页应用程序,他也可以获取 Auth 令牌(因为我们会将令牌存储在某个地方,例如本地存储)。 ‎您的室友还可以获取您的本地存储数据。 ‎除非您使用 SSL,否则监视您的网络的任何人也可以获取您的授权标头。 Cookie 和授权均以纯文本形式在 http 标头中发送。

我们应该怎么做?

    如果我们将令牌存储在某处,则与 cookie 相比没有安全优势,身份验证令牌最适合添加额外安全性或 cookie 不可用选项的单页应用程序。 ‎如果我们担心有人监控网络流量,我们应该使用 SSL 托管我们的网站。如果使用 SSL,则无法拦截 Cookie 和 http-headers。

    ‎如果我们使用单页应用程序,我们不应该将令牌存储在任何地方,只需将其保存在 JS 变量中并使用 Authorization 标头创建 ajax 请求。如果您使用的是 jQuery,则可以将 beforeSend 处理程序添加到全局 ajaxSetup,该处理程序会在您发出任何 ajax 请求时发送 Auth 令牌标头。

    var token = false; /* you will set it when authorized */
    $.ajaxSetup(
        beforeSend: function(xhr) 
            /* check if token is set or retrieve it */
            if(token)
                xhr.setRequestHeader('Authorization', 'Bearer ' + token);
            
        
    );
    

如果我们想使用 Cookies

如果我们不想实现单页应用程序并坚持使用 cookie,那么有两个选项可供选择。

    非持久性(或会话)cookies:非持久性cookies没有最长寿命/到期日期,并在用户关闭浏览器窗口时被删除,因此在涉及安全性的情况下更可取. 持久性 cookie:持久性 cookie 是具有最长寿命/到期日期的那些。这些 cookie 会一直持续到该时间段结束。当您希望 cookie 存在时,即使用户关闭浏览器并在第二天返回时也可以使用持久性 cookie,从而防止每次都进行身份验证并改善用户体验。
document.cookie = '__session=' + token  /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */

使用 Persistent 还是 Non-Persistent,选择完全取决于项目。如果是持久 cookie,max-age 应该是平衡的,它不应该是一个月或一个小时。 1 或 2 周对我来说是更好的选择。

【讨论】:

我没有在我的任何代码中使用 jQuery 或 AJAX。我正在服务器上构建我的所有内容。 你贴了一个jQuery代码,你不是在用吗? 这只是我如何知道客户端请求可以修改标头的一个示例-我正在尝试弄清楚如何使用发送到服务器的正常路由请求来执行此操作,例如发生的请求当您单击具有有效href 的锚标记时。 我已经修改了我的问题以删除多余的 jQuery 参考。对于任何混淆,我深表歉意。 所以你想在点击任何链接时进行 ajax 调用?【参考方案2】:

使用Generating a Secure Token libraries并直接添加令牌(Custom auth payload):

var token = tokenGenerator.createToken( "uid": "1234", "isModerator": true );

你的令牌数据是uid(或app_user_id)和isModerator里面的规则表达式,例如:


  "rules": 
    ".read": true,
    "$comment": 
      ".write": "(!data.exists() && newData.child('user_id').val() == auth.uid) || auth.isModerator == true"
    
  

【讨论】:

除非我误解了文档,否则这说明了一种连接到 Firebase 数据库参考的方法。我需要知道热才能使用我的 Firebase ID 令牌来验证 HTTP 请求,我的服务器使用 Firebase 托管在 Firebase 函数中实现。我错过了使用这些说明的方法吗?【参考方案3】:

您对将 Firebase ID 令牌存储在 cookie 中过于怀疑。通过将其存储在 cookie 中,它将随每个请求一起发送到您的 Firebase Cloud 函数。

Firebase ID 令牌:

由 Firebase 在用户登录 Firebase 应用时创建。这些令牌是经过签名的 JWT,可以安全地识别 Firebase 项目中的用户。这些令牌包含用户的基本个人资料信息,包括用户的 ID 字符串,这是 Firebase 项目独有的。因为可以验证 ID 令牌的完整性,您可以将它们发送到后端服务器以识别当前登录的用户。

正如 Firebase ID 令牌的定义中所述,令牌的完整性可以得到验证,因此它应该可以安全地存储并发送到您的服务器。问题出现在您不希望在向 Firebase Cloud Function 发出的每个请求的 Authentication 标头中提供此令牌,因为您希望避免使用 AJAX 请求进行路由。

这又回到了使用 cookie 上,因为 cookie 是随服务器请求自动发送的。它们并不像你想象的那么危险。 Firebase 甚至有一个名为“Server-side generated pages w/ Handlebars templating and user sessions”的示例应用程序,它利用会话 cookie 发送 Firebase ID 令牌。

你可以看到他们的这个here的例子:

// Express middleware that checks if a Firebase ID Tokens is passed in the `Authorization` HTTP
// header or the `__session` cookie and decodes it.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// When decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = (req, res, next) => 
    console.log('Check if request is authorized with Firebase ID token');

    return getIdTokenFromRequest(req, res).then(idToken => 
        if (idToken) 
            return addDecodedIdTokenToRequest(idToken, req);
        
        return next();
    ).then(() => 
        return next();
    );
;

/**
 * Returns a Promise with the Firebase ID Token if found in the Authorization or the __session cookie.
 */
function getIdTokenFromRequest(req, res) 
    if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) 
        console.log('Found "Authorization" header');
        // Read the ID Token from the Authorization header.
        return Promise.resolve(req.headers.authorization.split('Bearer ')[1]);
    
    return new Promise((resolve, reject) => 
        cookieParser(req, res, () => 
            if (req.cookies && req.cookies.__session) 
                console.log('Found "__session" cookie');
                // Read the ID Token from cookie.
                resolve(req.cookies.__session);
             else 
                resolve();
            
        );
    );

这将使您不需要 AJAX 并允许您的 Firebase Cloud Function 处理路由。请务必查看 Firebase 的模板,他们在其中检查每个 page 的标题。

<script>
    function checkCookie() 
    // Checks if it's likely that there is a signed-in Firebase user and the session cookie expired.
    // In that case we'll hide the body of the page until it will be reloaded after the cookie has been set.
    var hasSessionCookie = document.cookie.indexOf('__session=') !== -1;
    var isProbablySignedInFirebase = typeof Object.keys(localStorage).find(function (key) 
            return key.startsWith('firebase:authUser')
) !== 'undefined';
    if (!hasSessionCookie && isProbablySignedInFirebase) 
        var style = document.createElement('style');
    style.id = '__bodyHider';
        style.appendChild(document.createTextNode('bodydisplay: none'));
    document.head.appendChild(style);


checkCookie();
    document.addEventListener('DOMContentLoaded', function() 
        // Make sure the Firebase ID Token is always passed as a cookie.
        firebase.auth().addAuthTokenListener(function (idToken) 
            var hadSessionCookie = document.cookie.indexOf('__session=') !== -1;
            document.cookie = '__session=' + idToken + ';max-age=' + (idToken ? 3600 : 0);
            // If there is a change in the auth state compared to what's in the session cookie we'll reload after setting the cookie.
            if ((!hadSessionCookie && idToken) || (hadSessionCookie && !idToken)) 
                window.location.reload(true);
             else 
                // In the rare case where there was a user but it could not be signed in (for instance the account has been deleted).
                // We un-hide the page body.
                var style = document.getElementById('__bodyHider');
                if (style) 
                    document.head.removeChild(style);
                
            
        );
    );
</script>

【讨论】:

这是否意味着在您链接的示例中,用于在客户端从 functions/views/layouts/main.handlebars 文件设置 cookie 的方法对于当前的最佳实践是安全的?如果我使用该逻辑在我的应用程序中设置客户端 cookie,我对 ID 令牌的使用是否安全? 是的,ID Token 只是 Firebase 通常存储在 LocalStorage 中的 JSON Web Token。由您来验证服务器端的令牌并对其进行解码。 Firebase 在Verify ID Token 上的文章解释了如何处理。

以上是关于使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌的主要内容,如果未能解决你的问题,请参考以下文章

如何从node js文件中调用一个vanilla JavaScript函数?

javascript 使用vanilla JavaScript爬上DOM树

javascript 使用Vanilla JavaScript Shopify GraphQL Storefront API

javascript 使用Vanilla JavaScript Shopify GraphQL Storefront API

javascript 使用vanilla JS观察对象属性更改

使用 Vanilla Javascript 在表格中显示倒计时