Authentifizierungs-Flow in React Native
Einleitung
Profildaten speichern, Highscores auswerten, Warenkörbe auslesen oder Änderungen am Kundenkonto vornehmen – immer dann, wenn man kundenspezifische Informationen serverseitig ablegen möchte, kommt man um einen Authentifizierungsmechanismus nicht herum.
Ziel dieses Artikels ist es, Best Practices für die clientseitige Implementierung der verschiedenen Authentifizierungsmechanismen in React Native vorzustellen. In den Code-Beispielen verwenden wir mit Firebase Auth einen der populärsten Authentication Provider, die vorgestellten Patterns lassen sich aber ebenso für proprietäre Systeme oder andere Authentifizierungsanbieter anwenden, die JSON Webtoken basierte Authentifizierung unterstützen.
Für das State Management verwenden wir das Bordmittel React Context, als Navigationskomponente React Navigation. Auch hier gilt, dass sich die vorgestellten Methoden auf alternative State Management Systeme (z.B. Redux, MobX oder Recoil) und Navigations-Bibliotheken (z.B. React Router Native) übertragen lassen.
Authentifizierung mit JSON Web Token
Ein JSON Web Token ist ein nach RFC 7519 genormtes Access-Token, das den sicheren Informationsaustausch zwischen Systemen ermöglicht (Quelle: https://jwt.io/introduction) . Das Token wird vom Ersteller digital signiert, der Empfänger kann die enthaltene Information auslesen und sich ohne Rückversicherung beim Ersteller auf die Echtheit der Information verlassen.
Die Authentifizierung mittels JSON Web Token hat den Vorteil, dass serverseitig keinerlei Session-Information vorgehalten werden muss und Authentifizierung und Anwendungslogik vollständig voneinander getrennt werden können. Das folgende Diagramm veranschaulicht den Prozess:
JSON Web Tokens haben eine begrenzte Gültigkeit und müssen daher von Zeit zu Zeit vom Authentication Server erneuert werden. Zu diesem Zweck erzeugt der Authentication Server immer parallel ein sogenanntes Refresh Token, mit dessen Hilfe der Client ein neues Access Token anfordern kann. So besteht keine Notwendigkeit mehr, Benutzername und/oder Passwort des Users clientseitig zu speichern, nach einmaliger Anmeldung durch den Benutzer erfolgen alle weiteren Anfragen nur mithilfe des Access Tokens und ggf. des Refresh Tokens.
Der vom Client auslesbare Payload eines JSON Web Tokens enthält üblicherweise neben dem Ablaufdatum des Tokens eine eindeutige Nutzerkennung und zusätzliche sogenannte Custom Claims, die Auskunft über die Zugriffsrechte des Users geben – also z.B. Nutzerrollen o.Ä.
Einrichtung von Firebase Auth
Google Firebase ist eine Entwicklungsplattform, die diverse cloudbasierte Features für die App- und Webentwicklung zur Verfügung stellt, neben der Authentifizierung z.B. Cloud Messaging, Datenbanken mit lokaler Synchronisierung, Analytics, A/B Testing und Crash Reports.
Firebase Authentication bietet neben der „klassischen“ Authentifizierung per E-Mail und Passwort auch die Authentifizierung via Mobilnummer und über diverse OAuth Provider (u.a. Facebook, Google, Apple, Microsoft und GitHub) an. Außerdem wird auch anonyme Authentifizierung unterstützt, mit deren Hilfe es möglich ist, userspezifische Daten schon vor der Registrierung eines Users zu sammeln und diese dann zu einem späteren Zeitpunkt mit dem Profil des registrierten Users zusammenzuführen.
Vergleichbare Plattformen sind u.a. AWS Cognito, Auth0 oder Okta – wir nutzen in diesem Beispiel Firebase Authentication, da es aufgrund der Integration in die gesamte Firebase Entwicklungsplattform die populärste Plattform für die App-Entwicklung ist.
Um das Firebase JavaScript SDK verwenden zu können, muss unter https://firebase.google.com zunächst ein Projekt hinzugefügt werden:
Bei der Projekterstellung wird gefragt, ob Google Analytics für das Projekt verwendet werden soll. Dies ist für unsere Zwecke nicht erforderlich und kann deaktiviert werden.
Nach Erstellung des Projekts muss dem Projekt nun eine App hinzugefügt werden, Firebase bietet hier iOS-App, Android-App und Web-App als Optionen an:
Je nachdem, ob wir unser Projekt per Expo CLI (Quelle: https://expo.dev/) oder als pures React Native Projekt angelegt haben, unterscheiden sich hier nun die Vorgehensweisen. Wir werden im Folgenden den Expo-Workflow beschreiben, hierfür wird entgegen der intuitiven Annahme eine Web App erstellt. Expo verwendet diese dann sowohl für Android als auch für iOS. Für den nativen Workflow sei hier auf das npm Package react-native-firebase verwiesen (Quelle: https://rnfirebase.io/) . Hier wird das native Setup jeweils separat für Android und iOS angewandt.
Wir erstellen jedoch für den Expo-Flow eine Web-App und erhalten nach Angabe des App-Nicknames folgende Informationen:
Schließlich aktivieren wir in der Firebase Console unter Authentifizierung noch die Authentifizierungs-Methode „E-Mail" und sind für die clientseitige Integration startbereit.
Die serverseitige Einbindung betrachten wir weiter unten.
Unserem React Native Projekt fügen wir nun nur noch das Firebase JavaScript SDK hinzu:
expo install firebase
Den Aufruf von initializeApp(…) platzieren wir z.B. in der App.js, wobei darauf zu achten ist, dass die Konfiguration z.B. per ENV-Parameter übergeben wird und nicht im Klartext in unserem Repo landet.
Der Auth Context
Der Login-Status und die Berechtigungen eines Benutzers sind Informationen, die wir an vielen Stellen in der App benötigen, wir werden sie daher über den Application State verfügbar machen. Je nach eingesetzter State Management Lösung kann dies z.B. innerhalb eines Redux- / MobX-Stores oder per Recoil Atom geschehen, wir zeigen es hier beispielhaft mittels React Context (Quelle: https://reactjs.org/docs/context.html), da dies keine zusätzlichen Abhängigkeiten erfordert.
Die Basisversion unseres Auth Context macht zunächst nichts anderes als auf Änderungen im Login-Status des Users zu lauschen:
Das Firebase SDK stellt uns hierfür den Listener onAuthStateChanged zur Verfügung, wir legen den aktuellen User bei jeder Änderung in der State-Variable user ab, die unser Provider wiederum per value verfügbar macht.
Den Provider machen wir für alle Komponenten der App verfügbar, indem wir ihn in der App.js als oberste Ebene der Komponentenhierarchie einbinden:
Sinnvoll ist es außerdem, die Firebase Methoden, die wir in der App verwenden möchten, im AuthContext zu kapseln, so können wir z.B. die Fehlerbehandlung zentral durchführen oder einen Loading State mitführen. Die drei Methoden, die wir verwenden, sind signInWithEmailAndPassword, createUserWithEmailAndPassword und signOut.
Hier eine Beispiel-Implementierung der signIn-Funktion, die beiden anderen können analog implementiert werden:
Damit ist der Auth Context abgeschlossen, von jeder Stelle in der App kann nun auf den aktuellen User zugegriffen werden und die Methoden zum Registrieren, Anmelden und Abmelden stehen zur Verfügung.
Konfiguration des Navigation Stacks
Ein oft vorkommendes Szenario ist es, dass nicht registrierte Nutzer eine andere Navigation sehen als registrierte Nutzer. Ein nicht registrierter Nutzer könnte z.B. nur einen Home-Screen, einen Teaser-Screen und den Register/Login-Screen angeboten bekommen, während ein registrierter Nutzer seine Profildaten einsehen und ändern und auf andere geschützte Bereiche der App zugreifen kann.
Dies können wir nun leicht über den AuthContext steuern, indem wir je nach Login-Status einen anderen Navigationsbaum zurückgeben. Bei Verwendung von React Navigation (Quelle: https://reactnavigation.org/) als Navigations-Bibliothek sähe das z.B. so aus:
Formulare für Login und Registrierung
Nach all diesen Vorarbeiten sind die Formulare für Login und Registrierung nun mehr oder weniger trivial: Zwei einfache Textfelder für E-Mail und Passwort und ein Button, der die gewünschte Methode aus dem AuthContext (signIn oder signUp) aufruft.
Wenn der User erfolgreich angemeldet werden kann, wird die entsprechende State-Variable im AuthContext auf einen gültigen Wert gesetzt und die App-Navigation schaltet sich automatisch um.
Gleiches gilt für den Aufruf von signOut für einen angemeldeten User: Die State-Variable wird auf null gesetzt und die Navigation schaltet zurück auf den Modus für nicht angemeldete User. Nach erfolgreicher Registrierung sollte der neu registrierte Nutzer in der Firebase Console unter dem Menüpunkt Authentication/Users erscheinen:
Es sei darauf hingewiesen, dass Firebase in der Standardkonfiguration Registrierungen ohne Verifizierung der E-Mail-Adresse durchführt, was zumindest in Europa eher unüblich ist.
E-Mail-Verifizierung kann aber selbstverständlich konfiguriert werden, das Firebase SDK bietet hierzu die Funktion sendEmailVerification an, weitere Details hierzu finden sich unter https://firebase.google.com/docs/auth/web/manage-users.
Persistenz
Standardmäßig verwendet das Firebase SDK das Persistenzmodell LOCAL, was bedeutet, dass Access Token und Refresh Token im App Storage abgelegt werden und bei einem Neustart der App automatisch wieder daraus geladen werden. Der Nutzer meldet sich also nur einmalig in der App an, bei zukünftigen App Starts erfolgt die Anmeldung per Token ohne Nutzerinteraktion.
Für den Zugriff auf besonders sensible Informationen kann das Persistenzmodell jedoch auch geändert werden, hierfür stellt das Firebase SDK die Funktion setPersistence bereit, die neben LOCAL die Persistenzmodelle SESSION und NONE unterstützt (Quelle: https://firebase.google.com/docs/auth/web/auth-state-persistence).
Konfiguration der HTTP-Client Library
Unsere App verfügt nun über einen Registrierungs- und Anmeldemechanismus, den wir beim Aufruf von API-Methoden verwenden können, um bestimmte Funktionalitäten nur eingeloggten Nutzern zugänglich zu machen. Hierzu müssen wir das JSON Web Token des eingeloggten Nutzers im Authorization Header des API-Aufrufs übergeben.
Das Firebase SDK stellt uns die Funktion getIdToken zur Verfügung, um das aktuelle Token eines eingeloggten Nutzers zu erhalten. Zu beachten ist, dass diese Funktion asynchron ist, da sie im Falle eines abgelaufenen Access Tokens mithilfe des Refresh Tokens ein frisches Access Token erzeugt und hierzu intern ein API-Call beim Firebase-Server erforderlich ist.
getIdToken muss also immer unmittelbar vor einem API-Call aufgerufen werden, um sicherzustellen, dass wir immer ein aktuelles Access Token im Authorization Header verwenden.
Einige HTTP-Client-Libraries wie z.B. axios (Quelle: https://axios-http.com/) erlauben die Verwendung von Interceptors (Quelle: https://axios-http.com/docs/interceptors), mit deren Hilfe HTTP-Requests an einer zentralen Stelle unterbrochen und manipuliert werden können. Ist dies nicht möglich, kann man alternativ mit Wrapperfunktionen arbeiten.
Serverseitige Implementierung
Serverseitig kann das im Authorization Header mitgelieferte Token nun mithilfe des Firebase Admin SDK überprüft werden. Dies kann ohne Kommunikation mit dem Firebase Server geschehen, da die Gültigkeit des Tokens durch die digitale Signatur gewährleistet ist.
Das Firebase Admin SDK ist verfügbar für Node.js, Java, Python, Go und C#.
Mithilfe der Methode verifyIdToken kann das im Header mitgelieferte Token überprüft werden und die angefragte Route geöffnet/gesperrt werden (Quelle: https://firebase.google.com/docs/auth/admin/verify-id-tokens).
Das Admin SDK bietet außerdem eine Reihe zusätzlicher Funktionen zum User Management. Hingewiesen sei auf die Möglichkeit, Tokens sogenannte Custom Claims zuzuweisen, mit denen z.B. Nutzerrollen im Token selbst übermittelt werden können, sodass keine zusätzliche Rollenverwaltung mit Datenbankzugriff o.Ä. mehr erforderlich ist. Da die Claims direkt im Token gespeichert werden, stehen sie auch clientseitig im User-Objekt ohne zusätzliche Abfrage zur Verfügung (Quelle: https://firebase.google.com/docs/auth/admin/custom-claims).
Schlusswort
Registrierung und Login von Nutzern ist für die meisten Apps ein unverzichtbares Feature.
Wir denken, dass der oben am Beispiel von Firebase Authentication vorgestellte Implementierungsansatz einen guten Startpunkt für die Erstellung von Apps mit integriertem User-Management darstellt. Alternative auf JSON Webtoken basierende Systeme bieten vergleichbare SDKs und können analog eingebunden werden, die Kapselung des Authentifizierungsstatus in einem separaten Context-Modul erlaubt einen einfachen Austausch der Anbieter.
Wir wünschen viel Erfolg beim Ausprobieren: Happy Coding!
Über den Autor
Ralf hat seine Karriere in den 1990er Jahren als C++ Entwickler begonnen und 1997 die IT-Agentur pixell gegründet und als Geschäftsführer geleitet.
Nach dem Verkauf von pixell an den Reisetechnologie-Konzern Amadeus 2010 war er als Leiter des Amadeus Mobile Competence Center für die Mobile-Entwicklung des Konzerns im europäischen Markt verantwortlich.
2019 gründete er gemeinsam mit Thomas Görldt die actyvyst GmbH als Full Service Agentur für Apps und Mobile Websites.
actyvyst setzt auf den Technologie-Stack React / React Native / node.js / MongoDB.
Erfahre mehr über den Taktsoft Campus auf: https://www.taktsoft.com/campus