laravelでauth0を試す
リポジトリ
前提条件
1. laravelはdocker環境ではなくローカル
1-1. php => 8.1.7
1-2. composer => 2.3.7
1-3. laravel => 9
1-4. auth0/login => ^7.1
2. dbのみdockerで作成する
2-1. postgresql => latest
ひとまず環境を作ってmigration
修正箇所
'default' => env('DB_CONNECTION', 'pgsql'),
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=develop
DB_USERNAME=user
DB_PASSWORD=password
postgresqlのdocker
version: "3.7"
services:
postgres:
image: postgres:latest
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: develop
TZ: "Asia/Tokyo"
ports:
- 5432:5432
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
これらを参考にしていく
ファイルの変更点はこんな感じ(公式ドキュメント参考)
configを編集
'defaults' => [
'guard' => 'auth0',
],
'guards' => [
'auth0' => [
'driver' => 'auth0',
'provider' => 'auth0',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'auth0' => [
'driver' => 'auth0',
'repository' => App\Auth\CustomUserRepository::class // 自作のRepositoryを使いたい
],
自作のReposirtoryを作ることができるので作る(defaultのままというというのは少し辛い)
今回はauth0から手に入れたjwtに含まれてるsubをもつUserのみ認証が通るようにする
<?php
declare(strict_types=1);
namespace App\Auth;
use Auth0\Laravel\Contract\Auth\User\Repository as Repository;
use App\Models\User;
class CustomUserRepository implements Repository
{
/**
* @inheritdoc
*
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
*/
public function fromSession(
array $user
): ?\Illuminate\Contracts\Auth\Authenticatable {
// Unused in this quickstart example.
return null;
}
/**
* @inheritdoc
*/
public function fromAccessToken(
array $user
): ?\Illuminate\Contracts\Auth\Authenticatable {
$loginUser = User::where('auth0_id', $user['sub'])->first();
return $loginUser;
}
}
【注意】Userモデルを返したい場合はUserモデルにあるクラスをimplementsしなければいけない
1.ステートフルcookie)時 => Auth0\Laravel\Contract\Model\Stateful\User
2. ステートレス(jwt)時 => Auth0\Laravel\Contract\Model\Stateless\User
今回はjwtの時を採用する
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Auth0\Laravel\Contract\Model\Stateless\User as Stateless; // jwtの時はimplements
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements Stateless
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'auth0_id', // auth0のuuid
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
ルーティング追加
/**
* - auth0.authorize
* Requires a valid bearer token to access the route.
*/
Route::get('/api/private', function () {
return response()->json([
'authorized' => true,
// 'user' => json_decode(json_encode((array) Auth::user(), JSON_THROW_ON_ERROR), true),
'user' => Auth::user(),
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth0.authorize']);
auth0ってmiddlewareに酒類がある。
種類
- auth0.authorize
- auth0.authorize:scope
- auth0.authorize.optional
それぞれ何?
- は普通の認証
- はAuth0のスコープという機能に関係があるらしい... 後で調べたい
- は認証チェックをするけど、認証できなかった場合でもリクエストは止まらない?らしい 「ゲストユーザーとして扱いたい場合に使う」と公式には書いてあった。
トークン手に入れてpostman叩く
auth0はjwtを手に入れるcurlコマンドをすぐ手に入れられる
Testをスクロールするとcurlコマンドが手に入るのでそれを使おう
jwt.ioでtokenの中身が見れる
jwtのpayload部分にあるsubをdbのユーザーが持つようにmigrationファイルやUserFactoryファイル, Seederファイルを修正していく
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('auth0_id');
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => fake()->name(),
];
}
/**
* Indicate that the model's email address should be unverified.
*
* @return static
*/
public function unverified()
{
return $this->state(function (array $attributes) {
return [
'email_verified_at' => null,
];
});
}
}
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
\App\Models\User::factory()->create([
'auth0_id' => {ここにsub}, // 直接は良くないので環境変数で宣言してconfigファイルから呼び出すのが良さそう
]);
}
}
FacrotyとSeederはお好みです。別に使い捨てのコードなので笑
tinkerで直接入れるのもありです
postman叩く
取れるね。あとjwtが間違ってると403が返る
※ laravel9でやるとapi.phpのファイルのルーティングはprefixで"api"がつくからroutingに"api"をつける必要はないのかも
Route::get('/private', function () {
return response()->json([
'authorized' => true,
'user' => Auth::user(),
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth0.authorize']);
403返して欲しくないから401を返したい
Q. そもそもなんで403を返してるのか?
A. コードにそう書いてあるからです(暴論)
自分なりの予想を書いてみる(合っているかはわからない)
<?php
declare(strict_types=1);
namespace Auth0\Laravel;
final class ServiceProvider extends \Spatie\LaravelPackageTools\PackageServiceProvider implements \Auth0\Laravel\Contract\ServiceProvider
{
/**
* @inheritdoc
*/
public function configurePackage(
\Spatie\LaravelPackageTools\Package $package
): void {
$package
->name('auth0')
->hasConfigFile();
}
/**
* @inheritdoc
*/
public function registeringPackage(): void
{
app()->singleton(Auth0::class, static fn (): \Auth0\Laravel\Auth0 => new Auth0());
app()->singleton('auth0', static fn (): \Auth0\Laravel\Auth0 => app()->make(Auth0::class));
app()->singleton(StateInstance::class, static fn (): \Auth0\Laravel\StateInstance => new StateInstance());
app()->singleton(\Auth0\Laravel\Auth\User\Repository::class, static fn (): \Auth0\Laravel\Auth\User\Repository => new \Auth0\Laravel\Auth\User\Repository());
}
/**
* @inheritdoc
*/
public function bootingPackage(): void
{
auth()->provider('auth0', static fn ($app, array $config): \Auth0\Laravel\Auth\User\Provider => new \Auth0\Laravel\Auth\User\Provider(app()->make($config['repository'])));
auth()->extend('auth0', static fn ($app, $name, array $config): \Auth0\Laravel\Auth\Guard => new \Auth0\Laravel\Auth\Guard(auth()->createUserProvider($config['provider']), $app->make('request')));
$router = app()->make(\Illuminate\Routing\Router::class);
$router->aliasMiddleware('auth0.authenticate', \Auth0\Laravel\Http\Middleware\Stateful\Authenticate::class);
$router->aliasMiddleware('auth0.authenticate.optional', \Auth0\Laravel\Http\Middleware\Stateful\AuthenticateOptional::class);
$router->aliasMiddleware('auth0.authorize', \Auth0\Laravel\Http\Middleware\Stateless\Authorize::class);
$router->aliasMiddleware('auth0.authorize.optional', \Auth0\Laravel\Http\Middleware\Stateless\AuthorizeOptional::class);
}
}
middlewareを定義されてるみたい。今回はauth0.authorizeを使うので...
<?php
declare(strict_types=1);
namespace Auth0\Laravel\Http\Middleware\Stateless;
/**
* This middleware will configure the authenticated user using an available access token.
* If a token is not available, it will raise an exception.
*
* @package Auth0\Laravel\Http\Middleware
*/
final class Authorize implements \Auth0\Laravel\Contract\Http\Middleware\Stateless\Authorize
{
/**
* @inheritdoc
*/
public function handle(
\Illuminate\Http\Request $request,
\Closure $next,
string $scope = ''
) {
$user = auth()->guard('auth0')->user();
if ($user !== null && $user instanceof \Auth0\Laravel\Contract\Model\Stateless\User) {
if (strlen($scope) >= 1 && auth()->guard('auth0')->hasScope($scope) === false) {
return abort(403, 'Unauthorized');
}
auth()->login($user);
return $next($request);
}
return abort(403, 'Unauthorized');
}
}
確かに403だね。abortで返してるね
見よう見まねでMiddlewareを作って401を返そう
php artisan make:middleware Auth0Authorize
<?php
namespace App\Http\Middleware;
use Auth0\Laravel\Contract\Http\Middleware\Stateless\Authorize;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthenticationException;
class Auth0Authorize implements Authorize
{
/**
* @inheritdoc
*/
public function handle(
Request $request,
Closure $next,
string $scope = ''
) {
$user = auth()->guard('auth0')->user();
if ($user !== null && $user instanceof User) {
if (strlen($scope) >= 1 && auth()->guard('auth0')->hasScope($scope) === false) {
throw new AuthenticationException('Invalid Token.');
}
auth()->login($user);
return $next($request);
}
throw new AuthenticationException('Invalid Token.');
}
}
Middlewareに登録
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.authorize' => \App\Http\Middleware\Auth0Authorize::class,
];
ルーティングもしとく
Route::get('/private/self-made', function () {
return response()->json([
'authorized' => true,
'user' => Auth::user(),
], 200, [], JSON_PRETTY_PRINT);
})->middleware(['auth.authorize']);
postmanを叩く
※HeadersのAccept application/json設定を忘れずに
ひとまず認証まではできた