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

时间:2018-02-20 11:24:20

标签: 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)

她是我的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
}

以下是我的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应用程序中不是单页面应用程序的可扩展客户端安全性的最佳实践是什么?我不需要复杂的身份验证流程。我愿意牺牲我可以信任的安全系统的灵活性并简单地实施。

2 个答案:

答案 0 :(得分:2)

  

为什么不保证cookie?

  1. Cookie数据可以轻松调整,如果开发人员足够愚蠢地存储用户在cookie中的角色,用户可以轻松更改其Cookie数据document.cookie = "role=admin"。 (瞧!)
  2. 黑客可以通过XSS攻击轻松获取Cookie数据,他可以登录您的帐户。
  3. 可以从您的浏览器轻松收集Cookie数据,您的室友可以窃取您的cookie并从您的计算机上登录。
  4. 如果您不使用SSL,任何监控网络流量的人都可以收集您的Cookie。
  5.   

    你需要担心吗?

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

      我们该怎么办?

      1. 如果我们将令牌存储在某处,则没有cookie的安全优势,Auth令牌最适合添加额外安全性的单页应用程序,或者cookie不是可用选项。
      2. 如果我们担心有人监控网络流量,我们应该使用SSL托管我们的网站。如果使用SSL,则无法拦截Cookie和http-header。
      3. 如果我们使用单页面应用程序,我们不应该将令牌存储在任何地方,只需将其保存在JS变量中并使用Authorization标头创建ajax请求。如果您使用 jQuery ,则可以向全局ajaxSetup添加beforeSend处理程序,以便在您发出任何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);
                }
            }
        });
        
      4.   

        如果我们想使用Cookies

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

        1. 非持久性(或会话)Cookie:非持久性Cookie没有最大生命周期/到期日期,并在用户关闭浏览器窗口时被删除,从而使其在以下情况下更受欢迎安全问题。
        2. 持久性Cookie:持久式Cookie是指具有最长生命周期/到期日期的Cookie。这些cookie一直存在,直到时间段结束。如果您希望cookie存在,即使用户关闭浏览器并在第二天返回,也会优先使用持久性cookie,从而阻止每次身份验证并改善用户体验。
        3. document.cookie = '__session=' + token  /* Non-Persistent */
          document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */
          

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

答案 1 :(得分:0)

使用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"
    }
  }
}