webentwicklung-frage-antwort-db.com.de

Umgang mit Firebase-ID-Token auf der Clientseite mit Vanilla JavaScript

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.

14

Warum sind Cookies nicht gesichert?

  1. Cookie-Daten können leicht gemildert werden. Wenn ein Entwickler dumm genug ist, die Rolle eines angemeldeten Benutzers in einem Cookie zu speichern, kann der Benutzer seine Cookie-Daten problemlos ändern, document.cookie = "role=admin". (voila!) 
  2. Cookie-Daten können von einem Hacker durch einen XSS-Angriff leicht abgerufen werden und er kann sich in Ihrem Konto anmelden.
  3. Cookie-Daten können einfach über Ihren Browser erfasst werden. Ihr Mitbewohner kann Ihr Cookie stehlen und sich von Ihrem Computer aus anmelden.
  4. Jeder, der Ihren Netzwerkverkehr überwacht, kann Ihr Cookie abholen, wenn Sie kein SSL verwenden.

Müssen Sie besorgt sein?

  1. Wir speichern nichts Dummes in dem Cookie, das der Benutzer ändern kann, um unberechtigten Zugriff zu erhalten.
  2. Wenn ein Hacker durch XSS-Angriff Cookie-Daten abholen kann, kann er auch das Auth-Token abholen, wenn keine Einzelseitenanwendung verwendet wird (da das Token irgendwo gespeichert wird, z. B. localstorage).
  3. Ihr Mitbewohner kann auch Ihre lokalen Speicherdaten abholen.
  4. Jeder, der Ihr Netzwerk überwacht, kann Ihren Autorisierungsheader auch abholen, wenn Sie nicht SSL verwenden. Cookie und Autorisierung werden als einfacher Text im http-Header gesendet.

Was sollen wir machen? 

  1. Wenn wir das Token irgendwo speichern, gibt es keinen Sicherheitsvorteil gegenüber Cookies. Auth-Token eignen sich am besten für Einzelseitenanwendungen, die zusätzliche Sicherheit hinzufügen oder wenn Cookies nicht verfügbar sind.
  2. Wenn wir jemanden befürchten, der den Netzwerkverkehr überwacht, sollten wir unsere Website mit SSL hosten. Cookies und http-Header können nicht abgefangen werden, wenn SSL verwendet wird.
  3. 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.

  1. Nicht persistente Cookies (oder Sitzungscookies): Nicht persistente Cookies haben kein Gültigkeitsdatum und werden gelöscht, wenn der Benutzer das Browserfenster schließt. Dies macht es in Situationen, in denen es um die Sicherheit geht, vorzuziehen.
  2. Persistent Cookies: Persistent Cookies sind Cookies mit einem Gültigkeitsdatum. Diese Cookies bleiben bis zum Ablauf der Zeit erhalten. Dauerhafte Cookies werden bevorzugt, wenn das Cookie vorhanden sein soll, auch wenn der Benutzer den Browser schließt und am nächsten Tag zurückkehrt. Dadurch wird die Authentifizierung jedes Mal verhindert und die Benutzererfahrung verbessert.
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.

2
Munim Munna

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>
1
mootrichard

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"
    }
  }
}
0
user5377037