javascript - 使用普通 JavaScript 在客户端处理 Firebase ID token

标签 javascript security firebase firebase-authentication firebaseui

我正在使用原生 JavaScript 编写一个 Firebase 应用程序。我正在为 Web 使用 Firebase 身份验证和 FirebaseUI。我正在使用 Firebase Cloud Functions 来实现一个服务器,该服务器接收对我的页面路由的请求并返回呈现的 HTML。我正在努力寻找在客户端使用经过身份验证的 ID token 访问由我的 Firebase 云函数提供的 protected 路由的最佳实践。

我相信我理解基本流程:用户登录,这意味着一个 ID token 被发送到客户端,它在 onAuthStateChanged 回调中接收,然后插入到 具有适当前缀的任何新 HTTP 请求的授权 字段,然后在用户尝试访问 protected 路由时由服务器检查。

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

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

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,它描述了处理作为 Firebase Cloud Function 实现的 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
}

这是我的 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,客户端在其中请求和接收 token 。这是我卡住的地方:

/* 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 token ?

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

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

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。我无法弄清楚如何将 token 插入到正常路由请求的 header 中,例如通过单击具有有效 href 的 anchor 标记触发的请求。我应该拦截这些请求并以某种方式修改它们吗?

对于非单页应用程序的 Firebase for Web 应用程序,可扩展客户端安全性的最佳实践是什么?我不需要复杂的身份验证流程。我愿意为我可以信任和简单实现的安全系统牺牲灵 active 。

最佳答案

Why cookies are not secured?

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

Do you need to be concerned?

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

What should we do?

  1. 如果我们将 token 存储在某个地方,则与 cookie 相比没有安全优势,Auth token 最适合用于增加额外安全性的单页应用程序或 cookie 不可用的地方。
  2. 如果我们担心有人监控网络流量,我们应该使用 SSL 托管我们的网站。如果使用 SSL,则无法拦截 Cookie 和 http header 。
  3. 如果我们使用单页应用程序,我们不应该将 token 存储在任何地方,只需将其保存在一个 JS 变量中并创建带有 Authorization header 的 ajax 请求。如果您使用的是 jQuery,则可以将 beforeSend 处理程序添加到全局 ajaxSetup每当您发出任何 ajax 请求时,它都会发送 Auth token header 。

    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);
            }
        }
    });
    

If we want to use Cookies

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

  1. 非持久性(或 session )cookie:非持久性 cookie 没有最长生命周期/过期日期,并且会在用户关闭浏览器窗口时被删除,因此在以下情况下更受欢迎安全问题。
  2. 持久性 cookies:持久性 cookies 是那些有最长生命周期/过期日期的。这些 cookie 会一直保留到时间段结束。如果您希望即使用户关闭浏览器并在第二天返回时 cookie 仍然存在,则首选持久性 cookie,从而防止每次都进行身份验证并改善用户体验。
document.cookie = '__session=' + token  /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */

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

关于javascript - 使用普通 JavaScript 在客户端处理 Firebase ID token ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48884217/

相关文章:

javascript - 如何在 jquery 中检测一次单击的双击

javascript - 没有显示 Ajax 附件的文件数据

javascript - 新的 Twitter API : using it for a custom twitter feed

javascript - LESS Css 计算 js 变量失败?

security - RESTful API 中的 API key 、HTTP 身份验证与 OAuth

ios - 使用 Firebase 时使用未解析的标识符 'FIRDatabase'

php - 随机字符串是不是很好的验证码

linux - Python 子进程可以在没有 shell = true 的情况下创建 conda 环境吗?

javascript - 范围问题 : storing account credentials in a new class object within a promise, 但现在需要在类的其余部分使用此对象

android - 是否可以为不同的应用程序设置不同的 Firebase 数据库规则?