Laravel Sanctum Authentifizierung in Expo Go App

Laravel Sanctum ist eine einfach zu verwendende Authentifizierungsbibliothek von Laravel, die die Token-Authentifizierung mit Leichtigkeit implementiert. Wir werden Axios verwenden, um Requests aus der Expo Go-App an den Authentifizierungsserver zu senden und uns zu authentifizieren. Dir sollte klar sein, was Laravel Sanctum ist und es nicht mit Laravel Passport oder Laravel Socialite misszuverstehen ist.
Laravel Passport ist ein vollständiger OAuth2-Server, der hauptsächlich für die Erstellung von APIs verwendet wird, während Laravel Sanctum eine einfachere Alternative zur API-Authentifizierung ohne OAuth bietet.\

Mehr dazu

Noch leichter würde es mit einer Library von mir funktionieren. Diese kannst du hier finden: React Native Laravel Sanctum

Laravel Sanctum installieren

Ich gehe davon aus, dass du bereits eine Expo-App und Laravel-App laufen hast. Wenn nicht, kannst du in deren Dokumentation nachlesen wie man es installiert.
Ich nutze Laravel 10, aber es sollte auch mit älteren Versionen funktionieren (8-10 auf jeden Fall).

Um Laravel Sanctum zu installieren, folgen wir einfach der Dokumentation und führen folgenden Befehl aus:

composer require laravel/sanctum && php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" && php artisan migrate

Ist das getan, können wir direkt die benötigten Routen zu unserer API hinzufügen. Meine routes/api.php sieht dann so aus. Selbstverständlich kannst du die Funktionen auch in eigene Controller auslagern.

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/sanctum/token', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return $user->createToken($request->device_name)->plainTextToken;
});

Wir (Gut, eigentlich ist es schon alles in der Dokumentation beschrieben) haben eine Route /sanctum/token erstellt, die wir später in unserer Expo-App verwenden werden. Diese Route nimmt die E-Mail-Adresse, das Passwort und den Gerätenamen entgegen und prüft, ob die E-Mail-Adresse und das Passwort korrekt sind. Wenn ja, wird ein Token für das Gerät erstellt und zurückgegeben.
Die Route /user werden wir später verwenden, um den angemeldeten Benutzer abzurufen. Sie gibt uns alle sichtbaren Informationen des angemeldeten Benutzers zurück.

Das war es auch schon mit den Laravel-Konfigurationen. Deine Routes, die du schützen willst, musst du jetzt nurmehr hinter die auth:sanctum Middleware packen und fertig. Du kannst bei den ausgestellten Token auch Abilities festlegen wie man es bei APIs kennt.\

Expo Go App konfigurieren

Ich werde in diesem Beispiel einfach davon ausgehen, dass eine Login-View und eine Home-View gewollt ist.
Die Login-View wird die E-Mail-Adresse, das Passwort und den Gerätenamen entgegennehmen und an die Route /sanctum/token senden. Wenn die Anmeldung erfolgreich war, wird der Token in den SecureStorage von Expo gespeichert und der Benutzer wird auf die Home-View weitergeleitet.
Ich verwende React Navigation um zwischen den Views zu navigieren. Du kannst aber natürlich auch ohne dem Beispiel folgen.\

Meine App.js sieht dann so aus:

import React, { useEffect, useState } from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import Login from './components/Login'
import Home from './components/Home'
import * as SecureStore from 'expo-secure-store'
import axios from 'axios'

const Stack = createStackNavigator()

const App = () => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    async function getUser() {
      SecureStore.getItemAsync('access_token').then((token) => {
        axios
          .get('http://127.0.0.1:8000/api/user', {
            headers: {
              Authorization: `Bearer ${token}`,
            },
          })
          .then((response) => {
            setUser(response.data)
          })
          .catch((error) => console.log(error))
      })
    }
    getUser()
  }, [])

  return (
    <NavigationContainer>
      <Stack.Navigator>
        {!user ? (
          <Stack.Screen name="Login" component={Login} />
        ) : (
          <Stack.Screen name="Home" component={Home} user={user} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  )
}

export default App

Sobald die App geladen wird, wird versucht den Token aus dem SecureStorage zu laden und an die Route /user zu senden. Ihr müsst hier auf jeden Fall noch etwas Error-Handling betreiben und den User darüber informieren was passiert. Das sollte eigentlich alles "hinter" dem SplashScreen ablaufen. Expo hat hierfür eine super Dokumentation bzgl. dessen.
Wenn der Token gültig ist, wird der Benutzer auf die Home-View weitergeleitet, weil user dann gesetzt ist. Ansonsten wird die Login-View angezeigt.

In der Login-View fragen wir nach E-Mail-Adresse und dem Passwort. Anschließend senden wir diese an die Route /sanctum/token. Wenn die Anmeldung erfolgreich war, wird der Token im SecureStorage gespeichert und der Benutzer wird auf die Home-View weitergeleitet. Den Content für device_name können wir uns dank Expo bspw. mit dem Device-Modul von Expo abrufen.

Die Login.js sieht dann bei mir so aus:

import React, { useState } from 'react'
import {
  StyleSheet,
  Text,
  TextInput,
  View,
  Button,
  Platform,
} from 'react-native'
import axios from 'axios'
import * as SecureStore from 'expo-secure-store'

const Login = ({ navigation }) => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleLogin = async () => {
    await axios
      .post('http://127.0.0.1:8000/api/sanctum/token', {
        email: email,
        password: password,
        device_name: Platform.OS + ' ' + Platform.Version,
      })
      .then(async (response) => {
        await SecureStore.setItemAsync('access_token', response.data).then(
          () => {
            navigation.navigate('Home')
          },
        )
      })
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Login</Text>
      <TextInput
        style={styles.input}
        placeholder="Email"
        onChangeText={setEmail}
        value={email}
      />
      <TextInput
        style={styles.input}
        placeholder="Password"
        onChangeText={setPassword}
        value={password}
        secureTextEntry={true}
      />
      <Button title="Login" onPress={handleLogin} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  input: {
    width: '80%',
    padding: 10,
    borderWidth: 1,
    borderRadius: 5,
    marginBottom: 20,
  },
})

export default Login

Jetzt fehlt uns nur noch die Home-View, in der wir den Benutzer begrüßen. Den Nutzer sollte man sich aber global speichern und nicht erst in den Views abrufen.
Hier wäre jetzt ein Logout-Button sinnvoll, der den Token aus dem SecureStorage löscht, den Sanctum-Token widerruft (Revoking Tokens) und den Benutzer wieder auf die Login-View weiterleitet.

Die Home.js sieht dann so aus:

import React, { useEffect, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import axios from 'axios'
import * as SecureStore from 'expo-secure-store'

const Home = () => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    async function getUser() {
      SecureStore.getItemAsync('access_token').then((token) => {
        axios
          .get('http://127.0.0.1:8000/api/user', {
            headers: {
              Authorization: `Bearer ${token}`,
            },
          })
          .then((response) => {
            setUser(response.data)
          })
          .catch((error) => console.log(error))
      })
    }
    getUser()
  }, [])

  return (
    <View style={styles.container}>
      {user ? (
        <Text style={styles.title}>Servus, {user.name}</Text>
      ) : (
        <Text style={styles.title}>Loading...</Text>
      )}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
  },
})

export default Home

Endergebnis

ExpoSanctum LoginView Login.js ExpoSanctum HomeView Home.js