我正在使用原生 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?
- Cookie 数据很容易被篡改,如果开发人员愚蠢到将登录用户的 Angular 色存储在 cookie 中,用户可以轻松更改他的 cookie 数据,
document.cookie = "role=admin"
. (瞧!) - 黑客可以通过 XSS 攻击轻松获取 Cookie 数据,然后他可以登录到您的帐户。
- Cookie 数据可以很容易地从您的浏览器中收集,您的室友可以窃取您的 cookie 并从他的计算机上以您的身份登录。
- 如果您未使用 SSL,任何监控您网络流量的人都可以收集您的 cookie。
Do you need to be concerned?
- 我们不会在 cookie 中存储任何愚蠢的东西,用户可以修改这些内容以获得任何未经授权的访问。
- 如果黑客可以通过 XSS 攻击获取 cookie 数据,那么如果我们不使用单页应用程序,他也可以获取 Auth token (因为我们会将 token 存储在某个地方,例如本地存储)。
- 你的室友也可以获取你的本地存储数据。
- 除非您使用 SSL,否则监视您网络的任何人也可以获取您的授权 header 。 Cookie 和 Authorization 都以纯文本形式在 http header 中发送。
What should we do?
- 如果我们将 token 存储在某个地方,则与 cookie 相比没有安全优势,Auth token 最适合用于增加额外安全性的单页应用程序或 cookie 不可用的地方。
- 如果我们担心有人监控网络流量,我们应该使用 SSL 托管我们的网站。如果使用 SSL,则无法拦截 Cookie 和 http header 。
如果我们使用单页应用程序,我们不应该将 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,那么有两个选项可供选择。
- 非持久性(或 session )cookie:非持久性 cookie 没有最长生命周期/过期日期,并且会在用户关闭浏览器窗口时被删除,因此在以下情况下更受欢迎安全问题。
- 持久性 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/