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

Dav*_*son 16 javascript security firebase firebase-authentication firebaseui

我正在使用vanilla JavaScript编写Firebase应用程序.我正在使用Firebase身份验证和FirebaseUI for Web.我正在使用Firebase Cloud Functions来实现一个服务器,该服务器接收我的页面路由请求并返回呈现的HTML.我正在努力寻找在客户端使用经过身份验证的ID令牌来访问我的Firebase云功能所服务的受保护路由的最佳做法.

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

我不明白我应该对onAuthStateChanged回调中的ID令牌做什么,或者我应该如何修改我的客户端JavaScript以在必要时修改请求头.

我正在使用Firebase云功能来处理路由请求.这是我的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)
Run Code Online (Sandbox Code Playgroud)

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

{
  "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
}
Run Code Online (Sandbox Code Playgroud)

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

{
  "functions": {
    "predeploy": [
      "npm --prefix $RESOURCE_DIR run lint"
    ]
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "app"
      }
    ]
  }
}
Run Code Online (Sandbox Code Playgroud)

这是我的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.')
  }
})
Run Code Online (Sandbox Code Playgroud)

我应该如何处理客户端上经过身份验证的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()
Run Code Online (Sandbox Code Playgroud)

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

在不是单页面应用程序的Firebase for Web应用程序中,可扩展客户端安全性的最佳实践是什么?我不需要复杂的身份验证流程.我愿意为我可以信任并简单实施的安全系统牺牲灵活性.

Mun*_*nna 5

为什么 cookie 不安全?

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

你需要担心吗?

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

我们应该做什么?

  1. 如果我们将令牌存储在某个地方,则没有 cookie 的安全优势,Auth 令牌最适合添加额外安全性的单页应用程序或 cookie 不可用的选项。
  2. ?如果我们担心有人监控网络流量,我们应该使用 SSL 托管我们的站点。如果使用 SSL,则无法拦截 Cookie 和 http-header。
  3. ?如果我们使用单页应用程序,我们不应该将令牌存储在任何地方,只需将它保存在一个 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);
            }
        }
    });
    
    Run Code Online (Sandbox Code Playgroud)

如果我们想使用 Cookies

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

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

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