我正在使用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应用程序中不是单页面应用程序的可扩展客户端安全性的最佳实践是什么?我不需要复杂的身份验证流程。我愿意牺牲我可以信任的安全系统的灵活性并简单地实施。
答案 0 :(得分:2)
为什么不保证cookie?
document.cookie = "role=admin"
。 (瞧!)你需要担心吗?
我们该怎么办?
如果我们使用单页面应用程序,我们不应该将令牌存储在任何地方,只需将其保存在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);
}
}
});
如果我们想使用Cookies
如果我们不想实现单页应用程序并坚持使用cookie,那么有两种选择可供选择。
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"
}
}
}