JWT Auth – Wie man eine REST API mit dem Lumen Framework um eine JWT Authentifizierung erweitert

Zweiter Teil unserer Artikel-Serie über das Lumen-Framework. Dieses mal geht es darum, wie man die Endpunkte einer REST-API absichert.

Im letzten Artikel haben wir ausprobiert, wie schnell man mit Lumen eine einfache API bauen kann. Diese wollen wir nun jetzt absichern, d.h. nur authentifizierte Nutzer sollen Daten abrufen können.
Dazu Wollen wir eine Authentifizierung mittels JSON Web Token einbauen.

Dieses Beispiel bezieht sich auf folgende Versionen:
PHP: 7.3
Laravel / Lumen: 5.8
tymon/jwt-auth: >1

Vorbereitung

Lumen / Laravel macht es uns recht einfach, da bereits vieles für die Authentifizierung mittels JWT-Middleware vorbereitet ist.
Meine Wahl fiel auf eine Implementierung von Sean Tymon: jwr-auth – JSON Web Token Authentication for Laravel & Lumen (Dokumentation).

Zunächst installieren wir also das Modul „tymon/jwt-auth“ mittels Composer. Für Lumen 5.8 benötigen wir die aktuelle Version vom Release 1.x. Dazu fügen wir folgende Zeile in den „require“-Block der composer.json hinzu.


"tymon/jwt-auth": "^1"

Dann führen wir auf der Konsole ein composer install im Projektordner durch.

composer install

Bevor wir beginnen, die Authentifizierung einzubauen, benötigen wir eine Datenbankverbindung und eine Tabelle für die Benutzer, gegen die die Authentifizierung läuft.

Datenbankverbindung und User-Tabelle

Bei Laravel / Lumen ist die Authentifizierung unter der Haube eng mit der Datenbank gekoppelt. Haben wir also eine Datenbank mit einer User-Tabelle, müssen wir gar nicht mehr viel machen.

In der .env Datei legen wir die Datenbankverbindung fest.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

In der bootstrap/app.php kommentieren wir folgende Zeilen aus:

$app->withFacades();
$app->withEloquent();

Jetzt können wir eine Migration erstellen. Folgender Befehl erstellt eine Datei im Ordner „database“.

php artisan make:migration create_users_table

Diese Datei können wir nun um die benötigten Felder erweitern.

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Im Ordner „app“ findet man die Datei User.php, die das User-Model implementiert.
Dort erweitern wir die „fillable“-Felder und hidden-Felder.

protected $fillable = [
	'name', 'email', 'password'
];

protected $hidden = [
	'password', 'remember_token'
];

Jetzt können wir die Migration ausführen lassen. Dadurch wird die Users-Tabelle in der Datenbank angelegt.

php artisan migrate

So. Jetzt wäre alles vorbereitet, um die Authentifizierung zu implementieren.

JWT Authentication Middleware aktivieren und verwenden

Zuerst lassen wir uns einen Secret-Key erzeugen und lassen den in die .env schreiben. Dieser muss geheim bleiben und wird später zur Erzeugung des Tokens verwendet.

php artisan jwt:secret

In der .env-Datei wurde nun ein Eintrag hinzugefügt.

JWT_SECRET=abc…

Anpassungen an der bootstrap/app.php

In der bootstrap/app.php registrieren wir die Authentication-Middleware, in dem wir folgende Zeilen auskommentieren.

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
]);

Jetzt registrieren wir den Service Provider aus dem JWT-Modul.

$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);

Erstellen der config/auth.php

Diese Datei ist im erzeugten Lumen-Projekt standardmäßig nicht vorhanden.
Wir können die Datei aus dem Ordner vendor/laravel/lumen-framework/config kopieren.
Diese passen wir nun an.

return[
  'defaults' => [
    'guard' => env("AUTH_GUARD", "api"),
    'passwords' => 'users',
  ],
  'guards' => [
      'api' => [
          'driver' => 'jwt',
          'provider' => 'users',
      ],
  ],
  'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
  ],
  'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_resets',
        'expire' => 60,
    ],
  ],
]

Erweiterung des User-Models

Die User-Classe in der app/User.php erweitern wir un die JWT-Funktionen.

namespace App;

use Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
    use Authenticatable, Authorizable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token'
    ];

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

AuthController erstellen

Jetzt können wir einen AuthController erstellen, in dem wir die Methoden für die Routen zur Authentifizierung implementieren. Also User anlegen, einloggen und Token erzeugen, etc.
Die Datei AuthController.php erstellen wir unter app/Http/Controllers.
Im Konstruktor legen wir fest, dass die Auth-Middleware zur Anwendung kommen soll. Ausgenommen werden Registrierung und Login.

$this->middleware('auth:api', ['except' => ['login','register']]);

In der Login-Methode lassen wir aus E-Mail-Passwort-Kombination aus dem Request die Credentials prüfen und bei Erfolg den Token erzeugen.

$credentials = $request->only(['email', 'password']);
if (! $token = Auth::attempt($credentials)) {
   return response()->json(['error' => 'Unauthorized'], 401);
}

Und hier der komplette AuthController.

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\User;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login','register']]);
    }

    public function register(Request $request)
    {
        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => app('hash')->make($request->password, ['rounds' => 12]),
         ]);

        $token = Auth::login($user);

        return $this->respondWithToken($token);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $credentials = $request->only(['email', 'password']);
        if (! $token = Auth::attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(Auth::user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        Auth::logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth()->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => Auth::factory()->getTTL() * 60
        ]);
    }
}

Routen für die Authentifizierung

In der web.php im Ordner routes können wir nun unsere Routen für die Authentifizierung festlegen und entsprechend auf die Methoden des AuthControllers mappen.
Als Prefix legen wir „auth“ fest.

Route::group([
  'prefix' => 'auth'
], function ($router) {
  Route::post('register', 'AuthController@register');
  Route::post('login', 'AuthController@login');
  Route::post('logout', 'AuthController@logout');
  Route::post('refresh', 'AuthController@refresh');
  Route::post('me', 'AuthController@me');
});

Wenn wir jetzt einen Request feuern, der Authorisierung erfordert (z.B. ‚me‘), erhalten wir den Status „401 Unauthorized“ zurück. Aber auch die Route ‚login‘ scheitert im Moment noch. Das liegt daran, dass wir noch keinen User in der Datenbank haben.
Legen wir zunächst einen User an, in dem wir einen Request mit den Credentials (email, password) posten.
Dieser wird automatisch eingeloggt und wir bekommen einen Token zurück.

Jetzt können wir über die Route ‚me‘ einen Request mit Bearer-Token senden und wir erhalten Status „200 OK“ und ein JSON von unserem User zurück.

Die restlichen Routen schützen

Abschließend wollen wir die restlichen Routen der API schützen, also die, über welche die eigentlichen Daten abgerufen werden.
Als Beispiel nehmen wir die Routen aus dem letzten Artikel.

Den Prefix ‚api‘ können wir lassen, damit grenzen wir diese Routen von der Authentifizierung ab.
Im Beispiel-Projekt hatten wir zwei GET-Routen, die einmal alle Items abrufen und einmal nur eins über die Id.

Route::group([
  'prefix' => 'api'
], function ($router) {
  Route::get('items', 'ItemController@all');
  Route::get('items/{id}', 'ItemController@get');
});

Gemappt wurden sie auf den ItemController.
Und genau diesem müssen wir nun noch sagen, dass er die Auth-Middleware dazwischenschalten soll.
Im Konstruktor des ItemControllers ergänzen wir die Middleware.

public function __construct() {
	$this->middleware('auth:api');

	$this->items = array();
	for($i = 0; $i<10; $i++) {
		$item = array(
			'id' => $i,
			'name' => "item-" . $i
		);
		$this->items[] = $item;
	}	
}

Senden wir nun einen Request mit gültigem Token, erhalten wir Status „200 OK“, andernfalls Status „401 Unauthorized“.
Damit hätten wir unsere Api-Endpunkte geschützt.

Ich hoffe wie immer, diese erstbeste Anleitung war hilfreich.


Quellen:


Weitere Beiträge unserer Lumen-Serie:

Wie man mit dem Lumen Framework eine REST API erstellt

Wie man Mails versendet mit dem Lumen Framework

Ähnliche Beiträge

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert