パスキー

Laravel/passkeysの導入に苦労した

開発元が用意したものを使えばあっさり解決したが、当初は提案のママにAIに書いてもらったコードを使ったがうまくいかず、苦労したので、メモ。フロントエンド側とバックエンド側に開発元が用意したものがあるので、それを使うと簡単に実装できた。公式は以下。

laravel/passkeys - Packagist.org
Passwordless authentication using WebAuthn/passkeys for Lara…
React: 19.2
@laravel/passkeys: 0.2.0
laravel: 12
laravel/passkeys: 0.2.0

フロントエンド

登録と認証と削除が開発元で用意されている模様。以下は登録と認証。削除はなく、自分でapiルートに送って、バックエンド側で開発元の用意した削除機能を使う。

import { Passkeys } from "@laravel/passkeys";

const deviceName = getDeviceName();
await Passkeys.register({
        name: deviceName,
        routes: {
          options: `${BASE}/api/passkeys/options`, // laravelのapiルートに合わせる
          submit: `${BASE}/api/passkeys`,  // laravelのapiルートに合わせる
        },
      });
await Passkeys.verify({
        routes: {
          options: `${BASE}/api/passkeys/login/options`, // laravelのapiルートに合わせる
          submit: `${BASE}/api/passkeys/login`, // laravelのapiルートに合わせる
        },
      })

function getDeviceName() {
  const ua = navigator.userAgent;

  let device = "My Device";
  console.log(ua);
  if (/iPhone/i.test(ua)) device = "iPhone";
  else if (/iPad/i.test(ua)) device = "iPad";
  else if (/Android/i.test(ua)) device = "Android";
  else if (/Macintosh/i.test(ua)) device = "Mac";
  else if (/Windows/i.test(ua)) device = "Windows PC";

  let browser = "Browser";
  if (/Edg/i.test(ua)) browser = "Edge";
  else if (/Firefox/i.test(ua)) browser = "Firefox";
  else if (navigator.brave && typeof navigator.brave.isBrave === "function")
    browser = "Brave";
  else if (/Chrome/i.test(ua)) browser = "Chrome";
  else if (/Safari/i.test(ua)) browser = "Safari";

  return `${device} (${browser})`;
}

バックエンド

Passkeyモデルを作ったが、使わなかった。その代わり、Userモデルに追加する必要があるようだ。

use Laravel\Passkeys\Contracts\PasskeyUser;

class User extends Authenticatable implements PasskeyUser {
use PasskeyAuthenticatable;

    public function passkeys(): HasMany
    {
        return $this->hasMany(Passkey::class, 'user_id', 'user_id');
    }

apiルートで、開発元が用意したコントローラーを使えば、うまくいった。一覧取得は独自作成のPasskeyControllerを経由した。サインインの時に、開発元の返り値がリダイレクトURLのため、ユーザーを返して認証にしていた自分の仕組みにマッチしなかったので独自関数にしたが、optionsubmitは開発元のコントローラーを使う場合、両方使わないとセッションが噛み合わないとかで、エラーになってしまうようだ。そのため、AIに提案されたのがAppServiceProviderへの追記。返り値を変えることができるそう。

use Laravel\Passkeys\Http\Controllers\PasskeyRegistrationController;
use Laravel\Passkeys\Http\Controllers\PasskeyLoginController;
use App\Http\Controllers\PasskeyController;

Route::get('/passkeys/login/options', [PasskeyLoginController::class, 'index']);
Route::post('/passkeys/login', [PasskeyLoginController::class, 'store']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/passkeys/options', [PasskeyRegistrationController::class, 'index']);
    Route::post('/passkeys', [PasskeyRegistrationController::class, 'store']);
    Route::get('/passkeys', [PasskeyController::class, 'index']);
    Route::delete('/passkeys/{passkey}', [PasskeyRegistrationController::class, 'destroy']);


use Laravel\Passkeys\Actions\VerifyPasskey;
use Laravel\Passkeys\Http\Requests\PasskeyVerificationRequest;

class PasskeyController extends Controller
{
    public function index(Request $request)
    {
        $user = $request->user();
        $passkeys = $user->passkeys()->orderBy('created_at', 'desc')->get();

        return response()->json($passkeys);
    }

    
use Laravel\Passkeys\Contracts\PasskeyLoginResponse;

class AppServiceProvider extends ServiceProvider
{
public function boot(): void
    {
        $this->app->singleton(PasskeyLoginResponse::class, function () {
            return new class implements PasskeyLoginResponse, \Illuminate\Contracts\Support\Responsable {
                public function toResponse($request)
                {
                    $user = $request->user();

                    $token = $user->createToken('auth_token')->plainTextToken;

                    return response()->json([
                        'user' => $user,
                        'token' => $token,
                    ]);
                }
            };
        });
    }

こう見るとスゴイ単純だが、情報が少ないためか、AIが提案する内容に間違いが多い。値を吐かせて、型を作ったりしたが、途中で噛み合わなくなったり、うまくいかなかった。

コメント