Я пишу приложение Firebase в ванильном JavaScript. Я использую Firebase Authentication и FirebaseUI для Интернета. Я использую Firebase Cloud Functions для реализации сервера, который получает запросы на мои маршруты страниц и возвращает отображаемый HTML. Я изо всех сил стараюсь найти оптимальную практику использования моих токенов с идентификационными данными на стороне клиента для доступа к защищенным маршрутам, которые обслуживаются моей облачной функцией Firebase.
Я считаю, что я понимаю основной поток: пользователь входит в систему, что означает, что токен идентификатора отправляется клиенту, где он получен в onAuthStateChanged
а затем вставлен в поле Authorization
любого нового HTTP-запроса с соответствующим префиксом и затем проверяется сервером, когда пользователь пытается получить доступ к защищенному маршруту.
Я не понимаю, что я должен делать с токеном ID внутри обратного вызова onAuthStateChanged
или как мне следует модифицировать JavaScript на стороне клиента, чтобы изменить заголовки запросов, когда это необходимо.
Я использую Firebase Cloud Functions для обработки запросов на маршрутизацию. Вот мои functions/index.js
, которые экспортируют метод app
которому перенаправлены все запросы, и где отмечены маркеры идентификаторов:
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="#" onclick="location.href='https://cdn.firebase.com/libs/firebaseui/2.6.0/firebaseui.css'; return false;" />
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
, в котором описывается конфигурация сервера, обрабатывающего HTTP-запросы, реализованные в виде облачной функции Firebase:
{
"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.')
}
})
Что делать с аутентифицированными маркерами идентификаторов на стороне клиента?
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 для веб-приложения, которая не является одностраничным приложением? Мне не нужен комплексный поток аутентификации. Я готов пожертвовать гибкостью для системы безопасности, которой я могу доверять и реализовать просто.