使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌 [英] Handling Firebase ID tokens on the client side with vanilla JavaScript

查看:70
本文介绍了使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我相信我理解基本流程:用户登录,这意味着一个 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('检查请求是否使用 Firebase ID 令牌授权')if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&!req.cookies.__session) {console.error('没有 Firebase ID 令牌作为授权标头中的不记名令牌传递.','确保通过提供以下 HTTP 标头来授权您的请求:','授权:Bearer ','或通过传递一个__session"cookie.')res.status(403).send('未授权')返回}让 idTokenif (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {console.log('找到授权"标题')//从 Authorization 头中读取 ID Token.idToken = req.headers.authorization.split('Bearer')[1]} 别的 {console.log('找到__session"cookie')//从 cookie 中读取 ID Token.idToken = req.cookies.__session}admin.auth().verifyIdToken(idToken).then(decodedIdToken => {console.log('ID Token 正确解码', decodedIdToken)console.log('token details:', JSON.stringify(decodedIdToken))console.log('用户邮箱:', decodedIdToken.firebase.identities['google.com'][0])req.user = decodedIdToken返回下一个()}).catch(错误=>{console.error('验证 Firebase ID 令牌时出错:', 错误)res.status(403).send('未授权')})}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 逻辑 = `<script src="https://www.gstatic.com/firebasejs/4.10.0/firebase.js"></script><script src="/init.js"></script><!-- 身份验证--><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(`<头><title>索引</title>${元}<身体><h1>索引</h1><a href="/user/fake">假用户</a><div id="firebaseui-auth-container"></div>${逻辑}</html>`)})app.get('/user/:name', firebaseAuthenticate, (request, response) => {response.send(`<头><title>用户 - ${request.params.name}</title>${元}<身体><h1>用户 ${request.params.name}</h1>${逻辑}</html>`)})export.app = functions.https.onRequest(app)

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

<代码>{"name": "函数","description": "Firebase 云函数",脚本":{"lint": "./node_modules/.bin/eslint .","serve": "firebase serve --only 功能","shell": "firebase 实验:函数:shell","start": "npm run shell","deploy": "firebase deploy --only 函数",日志":firebase 函数:日志"},依赖关系":{"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 函数":^0.8.1"},开发依赖":{"eslint": "^4.12.0","eslint-plugin-promise": "^3.6.0"},私人":真的}

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

<代码>{职能": {预部署":[npm --prefix $RESOURCE_DIR 运行 lint"]},主持":{"公开": "公开",忽略": ["firebase.json","**/.*",**/node_modules/**"],重写":[{来源": "**",功能":应用程序"}]}}

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

/* 全局 firebase, firebaseui */const uiConfig = {//signInSuccessUrl: '',登录选项:[//保留要为用户提供的提供者的行.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],回调:{signInSuccess () { 返回假 }}//服务条款网址.//tosUrl: ''}const ui = new firebaseui.auth.AuthUI(firebase.auth())ui.start('#firebaseui-auth-container', uiConfig)firebase.auth().onAuthStateChanged(function (user) {如果(用户){firebase.auth().currentUser.getIdToken().then(token => {console.log('您是授权用户.')//这是不安全的.我应该怎么做?//document.cookie = '__session=' + token})} 别的 {console.warn('你是一个未经授权的用户.')}})

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

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

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

var xhr = new XMLHttpRequest()xhr.open('GET', URL)xmlhttp.setRequestHeader("授权", 'Bearer' + token)xhr.onload = 函数 () {如果(xhr.status === 200){警报('成功:' + xhr.responseText)}别的 {alert('请求失败.返回'+xhr.status的状态)}}xhr.send()

但是我不想做单页应用,所以不能使用AJAX.我不知道如何将令牌插入普通路由请求的标头中,例如通过单击带有有效 href 的锚标记触发的请求.我应该拦截这些请求并以某种方式修改它们吗?

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

解决方案

为什么 cookie 不安全?

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

<块引用>

你需要担心吗?

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

<块引用>

我们该怎么办?

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

    var token = false;/* 你将在授权时设置它 */$.ajaxSetup({发送前:函数(xhr){/* 检查令牌是否设置或检索它 */如果(令牌){xhr.setRequestHeader('Authorization', 'Bearer' + token);}}});

<块引用>

如果我们想使用 Cookies

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

  1. 非持久性(或会话)cookie:非持久性 cookie 没有最长寿命/到期日期,当用户关闭浏览器窗口时会被删除,因此在以下情况下非常可取安全问题.
  2. 持久性 cookie:持久性 cookie 是具有最长寿命/到期日期的 cookie.这些 cookie 会一直存在,直到时间段结束.如果您希望 cookie 仍然存在,即使用户关闭浏览器并在第二天返回,则首选持久性 cookie,从而防止每次都进行身份验证并改善用户体验.

document.cookie = '__session=' + token/* 非持久化 */document.cookie = '__session=' + token + ';max-age=' + (3600*24*7)/* 持续 1 周 */

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

I am writing a Firebase application in vanilla JavaScript. I am using Firebase Authentication and FirebaseUI for Web. I am using Firebase Cloud Functions to implement a server that receives requests for my page routes and returns rendered HTML. I am struggling to find the best practice for utilizing my authenticated ID tokens on the client side to access protected routes served by my Firebase Cloud Function.

I believe I understand the basic flow: the user logs in, which means an ID token is sent to the client, where it is received in the onAuthStateChanged callback and then inserted into the Authorization field of any new HTTP request with the proper prefix, and then checked by the server when the user attempts to access a protected route.

I do not understand what I should do with the ID token inside the onAuthStateChanged callback, or how I should modify my client side JavaScript to modify the request headers when necessary.

I am using Firebase Cloud Functions to handle routing requests. Here is my functions/index.js, which exports the app method that all requests are redirected to and where ID tokens are checked:

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)

Her is my functions/package.json, which describes the configuration of the server handling HTTP requests implemented as a 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
}

Here is my firebase.json, which redirects all page requests to my exported app function:

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

Here is my public/auth.js, where the token is requested and received on the client. This is where I get stuck:

/* 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.')
  }
})

What should I do with authenticated ID tokens on the client side?

Cookies/localStorage/webStorage do not seem to be fully securable, at least not in any relatively simple and scalable way that I can find. There may be a simple cookie-based process which is as secure as directly including the token in a request header, but I have not been able to find code I could easily apply to Firebase for doing so.

I know how to include tokens in AJAX requests, like:

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()

However, I don't want to make a single page application, so I cannot use AJAX. I cannot figure out how to insert the token into the header of normal routing requests, like the ones triggered by clicking on an anchor tag with a valid href. Should I intercept these requests and modify them somehow?

What is the best practice for scalable client side security in a Firebase for Web application that is not a single page application? I do not need a complex authentication flow. I am willing to sacrifice flexibility for a security system I can trust and implement simply.

解决方案

Why cookies are not secured?

  1. Cookie data can be easily tempered with, if a developer is stupid enough to store logged in user's role in cookie, user can easily alter his cookie data, document.cookie = "role=admin". (voila!)
  2. ‎Cookie data can be easily picked up by a hacker by XSS attack and he can login to your account.
  3. ‎Cookie data can be easily collected from your browser, and your roommate can steal your cookie and login as you from his computer.
  4. ‎Anyone who is monitoring your network traffic can collect your cookie if you are not using SSL.

Do you need to be concerned?

  1. We are not storing anything stupid in the cookie the user can modify to gain any unauthorized access.
  2. ‎If a hacker can pick-up cookie data by XSS attack, he can also pickup the Auth token if we don't use single page application (because we will be storing the token somewhere eg localstorage).
  3. ‎Your roommate can also pickup your localstorage data.
  4. ‎Anyone monitoring your network can also pickup your Authorization header unless you use SSL. Cookie and Authorization are both sent as plain text in http header.

What should we do?

  1. If we are storing the token somewhere, there is no security advantage over cookies, Auth token are best suited for single page applications adding additional security or where cookies are not an available option.
  2. ‎If we are concerned of someone monitoring network traffic, we should host our site with SSL. Cookies and http-headers cannot be intercepted if SSL is used.
  3. ‎If we are using single page application, we should not store the token anywhere, just keep it in a JS variable and create ajax request with Authorization header. If you are using jQuery you can add a beforeSend handler to the global ajaxSetup that sends the Auth token header whenever you make any ajax request.

    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

If we don't want to implement a single page application and stick to cookies, then there are two options to choose from.

  1. Non-Persistent (or session) cookies: Non-persistent cookies has no max-life/expiration date and gets deleted when the user closes browser window, thus making it so much preferable in situations where security is concerned.
  2. Persistent cookies: Persistent cookies are those with a max-life/expiration date. These cookies persist until the time period is over. Persistent cookies are preferred when you want the cookie to exist even if the user closes the browser and comes back next day, thus preventing authentication every time and improving user's experience.

document.cookie = '__session=' + token  /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */

Persistent or Non-Persistent which one to use, the choice is completely the project dependent. And in case of Persistent cookies the max-age should be balanced, it should not be a month, or an hour. 1 or 2 weeks look better option to me.

这篇关于使用 vanilla JavaScript 在客户端处理 Firebase ID 令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆