Ich schreibe eine Firebase-Anwendung in Vanilla JavaScript. Ich verwende Firebase Authentication und FirebaseUI for Web. Ich verwende Firebase Cloud Functions, um einen Server zu implementieren, der Anforderungen für meine Seitenrouten empfängt und gerenderten HTML-Code zurückgibt. Ich bemühe mich, die beste Methode zu finden, um meine authentifizierten ID-Token auf der Clientseite für den Zugriff auf geschützte Routen zu verwenden, die von meiner Firebase-Cloud-Funktion bedient werden.
Ich glaube, ich verstehe den grundlegenden Ablauf: Der Benutzer meldet sich an, dh ein ID-Token wird an den Client gesendet, wo er im onAuthStateChanged
-Callback empfangen und dann in das Authorization
-Feld einer neuen HTTP-Anforderung mit dem entsprechenden Präfix eingefügt wird dann vom Server geprüft, wenn der Benutzer versucht, auf eine geschützte Route zuzugreifen.
Ich verstehe nicht, was ich mit dem ID-Token im onAuthStateChanged
-Callback machen soll, oder wie ich mein clientseitiges JavaScript ändern sollte, um bei Bedarf die Anforderungsheader zu ändern.
Ich verwende Firebase Cloud-Funktionen, um Routing-Anfragen zu bearbeiten. Hier ist mein functions/index.js
, der die app
-Methode exportiert, zu der alle Anforderungen umgeleitet werden und bei der ID-Token überprüft werden:
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)
Ihr ist mein functions/package.json
, der die Konfiguration des Servers beschreibt, der HTTP-Anforderungen verarbeitet, die als Firebase-Cloud-Funktion implementiert sind:
{
"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
}
Hier ist mein firebase.json
, der alle Seitenanforderungen an meine exportierte app
-Funktion umleitet:
{
"functions": {
"predeploy": [
"npm --prefix $RESOURCE_DIR run lint"
]
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "app"
}
]
}
}
Hier ist mein public/auth.js
, wo das Token vom Client angefordert und empfangen wird. Hier stecke ich fest:
/* 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.')
}
})
Was muss ich mit authentifizierten ID-Token auf der Clientseite tun?
Cookies/localStorage/webStorage scheinen nicht vollständig sicherbar zu sein, zumindest nicht auf relativ einfache und skalierbare Weise, die ich finden kann. Es kann einen einfachen Cookie-basierten Prozess geben, der so sicher ist wie das direkte Einfügen des Tokens in einen Anforderungsheader, aber ich konnte keinen Code finden, den ich problemlos bei Firebase anwenden könnte.
Ich weiß, wie man Token in AJAX -Anfragen einfügt, wie zum Beispiel:
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()
Ich möchte jedoch keine Einzelseitenanwendung erstellen, daher kann ich AJAX nicht verwenden. Ich kann nicht herausfinden, wie das Token in den Header normaler Routing-Anforderungen eingefügt wird, wie z. B. solche, die durch Klicken auf einen Ankertag mit einer gültigen href
ausgelöst werden. Soll ich diese Anfragen abfangen und irgendwie ändern?
Was ist die bewährte Methode für skalierbare clientseitige Sicherheit in einer Firebase für Webanwendung, die keine Einzelseitenanwendung ist? Ich brauche keinen komplexen Authentifizierungsfluss. Ich bin bereit, auf Flexibilität für ein Sicherheitssystem zu verzichten, dem ich einfach vertrauen und implementieren kann.
Warum sind Cookies nicht gesichert?
document.cookie = "role=admin"
. (voila!) Müssen Sie besorgt sein?
Was sollen wir machen?
Wenn wir eine Einzelseitenanwendung verwenden, sollten wir das Token nirgendwo speichern. Behalten Sie es einfach in einer JS-Variablen und erstellen Sie eine Ajax-Anforderung mit dem Autorisierungskopf. Wenn Sie jQuery verwenden, können Sie dem globalen beforeSend
einen ajaxSetup
-Handler hinzufügen, der bei jeder Ajax-Anforderung den Auth-Token-Header sendet.
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);
}
}
});
Wenn wir Cookies verwenden möchten
Wenn wir keine Einzelseitenanwendung implementieren und an Cookies festhalten möchten, stehen zwei Optionen zur Auswahl.
document.cookie = '__session=' + token /* Non-Persistent */
document.cookie = '__session=' + token + ';max-age=' + (3600*24*7) /* Persistent 1 week */
Persistent oder Nicht-Persistent welche verwendet werden soll, ist die Wahl vollständig vom Projekt abhängig. Bei hartnäckigen Cookies sollte das Höchstalter ausgeglichen sein, es sollte nicht ein Monat oder eine Stunde sein. 1 oder 2 Wochen sehen für mich die bessere Option aus.
Sie sind zu skeptisch, wenn Sie den Firebase-ID-Token in einem Cookie speichern. Wenn Sie es in einem Cookie speichern, wird es mit jeder Anfrage an Ihre Firebase Cloud-Funktion gesendet.
Firebase-ID-Token:
Wird von Firebase erstellt, wenn sich ein Benutzer bei einer Firebase-App anmeldet. Diese Token sind signierte JWTs, die einen Benutzer in einem Firebase-Projekt sicher identifizieren. Diese Token enthalten grundlegende Profilinformationen für einen Benutzer, einschließlich der ID-Zeichenfolge des Benutzers, die für das Firebase-Projekt eindeutig ist. Da die Integrität der ID-Token überprüft werden kann, können Sie sie an einen Back-End-Server senden, um den aktuell angemeldeten Benutzer zu identifizieren.
Wie in der Definition eines Firebase-ID-Tokens angegeben, kann die Integrität des Tokens überprüft werden. Es sollte also sicher gespeichert und an den Server gesendet werden. Das Problem entsteht dadurch, dass Sie dieses Token nicht für jede Anforderung an Ihre Firebase Cloud-Funktion im Authentication-Header angeben müssen, da Sie die Verwendung von AJAX -Anforderungen für das Routing vermeiden möchten.
Dadurch werden Cookies zurückgesetzt, da Cookies automatisch mit Serveranforderungen gesendet werden. Sie sind nicht so gefährlich, wie Sie denken. Firebase verfügt sogar über eine Beispielanwendung mit dem Namen " Serverseitig erzeugte Seiten mit Vorlagen für Lenker und Benutzersitzungen ", die Sitzungscookies zum Senden des Firebase-ID-Token verwendet.
Ihr Beispiel sehen Sie hier hier :
// Express middleware that checks if a Firebase ID Tokens is passed in the `Authorization` HTTP
// header or the `__session` cookie and decodes it.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// When decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token');
return getIdTokenFromRequest(req, res).then(idToken => {
if (idToken) {
return addDecodedIdTokenToRequest(idToken, req);
}
return next();
}).then(() => {
return next();
});
};
/**
* Returns a Promise with the Firebase ID Token if found in the Authorization or the __session cookie.
*/
function getIdTokenFromRequest(req, res) {
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
return Promise.resolve(req.headers.authorization.split('Bearer ')[1]);
}
return new Promise((resolve, reject) => {
cookieParser(req, res, () => {
if (req.cookies && req.cookies.__session) {
console.log('Found "__session" cookie');
// Read the ID Token from cookie.
resolve(req.cookies.__session);
} else {
resolve();
}
});
});
}
Auf diese Weise können Sie AJAX nicht benötigen und Routen können von Ihrer Firebase-Cloud-Funktion verarbeitet werden. Schauen Sie sich unbedingt die Firebase-Vorlage an, in der die Kopfzeile auf jeder Seite geprüft wird.
<script>
function checkCookie() {
// Checks if it's likely that there is a signed-in Firebase user and the session cookie expired.
// In that case we'll hide the body of the page until it will be reloaded after the cookie has been set.
var hasSessionCookie = document.cookie.indexOf('__session=') !== -1;
var isProbablySignedInFirebase = typeof Object.keys(localStorage).find(function (key) {
return key.startsWith('firebase:authUser')
}) !== 'undefined';
if (!hasSessionCookie && isProbablySignedInFirebase) {
var style = document.createElement('style');
style.id = '__bodyHider';
style.appendChild(document.createTextNode('body{display: none}'));
document.head.appendChild(style);
}
}
checkCookie();
document.addEventListener('DOMContentLoaded', function() {
// Make sure the Firebase ID Token is always passed as a cookie.
firebase.auth().addAuthTokenListener(function (idToken) {
var hadSessionCookie = document.cookie.indexOf('__session=') !== -1;
document.cookie = '__session=' + idToken + ';max-age=' + (idToken ? 3600 : 0);
// If there is a change in the auth state compared to what's in the session cookie we'll reload after setting the cookie.
if ((!hadSessionCookie && idToken) || (hadSessionCookie && !idToken)) {
window.location.reload(true);
} else {
// In the rare case where there was a user but it could not be signed in (for instance the account has been deleted).
// We un-hide the page body.
var style = document.getElementById('__bodyHider');
if (style) {
document.head.removeChild(style);
}
}
});
});
</script>
Verwenden Sie Erstellen einer Secure Token-Bibliothek und fügen Sie das Token direkt hinzu ( Custom auth payload ):
var token = tokenGenerator.createToken({ "uid": "1234", "isModerator": true });
Ihre Token-Daten sind uid
(oder app_user_id) und isModerator
innerhalb des Regelausdrucks. Beispiel:
{
"rules": {
".read": true,
"$comment": {
".write": "(!data.exists() && newData.child('user_id').val() == auth.uid) || auth.isModerator == true"
}
}
}