🐸

Laravel Socialite で認証するときのパスワードをどうするのか問題

2023/01/05に公開

Laravel Socialite とは OAuth プロバイダを使って認証するための Laravel 向け公式パッケージです
Google / GitHub / Twitter 等 OAuth を提供している巨人の肩に乗って認証機構を構築できちゃう便利なパッケージです
Socialite Providers から確認できますが、他にもたくさんのプロバイダーと連携できます

ユーザーの会員登録の手間を省けるのももちろんですが、 Socialite 一本で認証を提供するようにすればパスワードの変更や忘れたときの処理を実装するデベロッパーの手間も省けてとてもハッピーです

今回はそんな Socialite を使うときの User モデルの password をどうするかというお話です

TL; DR

先に結論

  • Socialite のみで認証する、つまりメールアドレス/パスワードによる認証機構を持たずすべて Socialite に認証を任せる場合は単に password カラムを消してしまうのがシンプル
  • Socialite + メールアドレス/パスワード認証を提供する場合は password フィールドを nullable にする

Socialite と password

Laravel を普通にセットアップすると User モデルの情報が格納されている users テーブルには password カラムが生えており NOT NULL です
つまり何かしら値を入れないとユーザーを作成できません
しかし Socialite 経由で OAuth プロバイダーを使って認証する場合、パスワードのような情報はアプリケーション側には存在しません

このミスマッチを解消する方法をいくつか紹介します

Socialite のみで認証する場合

Socialite のみで認証する場合、つまりメールアドレス/パスワードによる認証機構を持たずにすべて Socialite に認証を任せる場合は単に password フィールドを消してしまうのがシンプルな設計になると思います

以下のようなマイグレーションを実行する感じですね

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('password');
});

Socialite のみで認証する場合、そもそもパスワードを使う機会がなくなるのでいらないものは消してしまおうというアプローチです
パスワードを持たないという仕様を正確にモデルに反映できます

ログイン時は OAuth プロバイダーから得られたユーザーの ID 情報を基にログインを実行します

// Socialite + Google のログイン例
$googleUser = Socialite::driver('google')->user();
$user = User::firstOrCreate(
    ['googleId' => $googleUser->id],
    [
        'name' => $googleUser->name,
        'email' => $googleUser->email,
    ]
);
Auth::login($user);

Socialite 一本で認証する場合はこれがしっくり来ますが、User に password フィールドが存在する前提で書かれているコードがあったり、そのように作られているライブラリやシステムと連携してたりするとこのアプローチは難しいです

その場合は次のセクションで紹介しているアプローチが使えると思います
(が、そもそも Socialite のみでログインする場合、本質的にはアプリケーション内にパスワードという概念は存在しないため、password カラムが存在するという前提をパスできたとしてもまた別の箇所で別の不整合が起きそうではあります。。。)

Socialite + メールアドレス/パスワードで認証する場合

Socialite + メールアドレス/パスワード認証を共存させる場合、調べてみたところ以下の2つのパターンが定番っぽそうでした

  1. Socialite 経由でユーザーを作成する場合になんらかのパスワードを設定するパターン
  2. password カラムを nullable にするパターン

どちらのパターンでも共存させることはできますが、個人的には 2 のパターンがおすすめです

1. Socialite 経由でユーザーを作成する場合になんらかのパスワードを設定するパターン

OAuth プロバイダーと連携した時にランダム文字列などなんらかのパスワードを設定したユーザーを作成するパターンです
OAuth プロバイダーからコールバックで戻ってきた時にこのような処理をする感じですね

$googleUser = Socialite::driver('google')->user();
$user = User::firstOrCreate(
    ['googleId' => $googleUser->id],
    [
        'name' => $googleUser->name,
        'email' => $googleUser->email,
        'password' => 'ランダム文字列などのパスワード',
    ]
);
Auth::login($user);

このパターンのメリットとして挙がるのは、Socialite 経由でログインしたユーザーもメールアドレス/パスワードで登録したユーザーと同一に見做せるので、メールアドレス/パスワードでもログインできるようになるということです
このメリットを活かすためにはパスワードをユーザーが知っておく必要があるので

  • ランダムな文字列を設定する場合: メール等の手段で「仮パスワードを設定したのでこのパスワードでログインして設定画面から変更してください」などと伝える
  • ユーザーがパスワードを設定する場合: OAuth プロバイダーのコールバックから返ってきた時にユーザーにパスワードの入力を求めて設定する(上のコード片の 'ランダム文字列などのパスワード' がユーザーが入力した値になるイメージ)

のようなケアが必要になってきます

このような「外部サービスを使ったログインをしたあとにパスワードの管理を求められる」フローを採用しているサービスをたまに見かけますが、正直ユーザー体験はかなり悪めだと思います

パスワード設定などのユーザー登録を手間が省けることを期待して既に持っているソーシャルサービスのアカウントでログインしているのに、パスワードを通知されて管理を要求されたり、設定を要求されたりするとガッカリしませんか?

実際にはアプリケーションが発行/設定したパスワードを覚えていなくても Socialite 経由であればログインできるのですが、ユーザーにそこを区別してもらうのは難しいでしょう

面倒なだけではなく、このようなフローだとログインに使っているソーシャルサービスと結びついている印象を与えるので、ソーシャルサービス側のパスワードを要求されていると誤解しかねないですし、
パスワードの変更時にソーシャルサービス側のパスワードも変更されるのではないかという不安を与える可能性もありそうです

体験悪化を避けるために本当に形だけのパスワードを設定してユーザーに通知しないパターンも考えられますが、モデルが腐敗し複雑になっていきます
たとえば、ユーザーが自分の意思で設定したパスワードなのか、それとも仮に設定されただけのパスワードなのかを区別したくなったりしてくると大変そうですよね

以上の理由から「既存のシステムの制約でこうせざるを得ないんだ!」という状況を除けば、次に紹介する「password カラムを nullable にするパターン」が良いと思います

2. password カラムを nullable にするパターン

個人的に有力と考えているのがこのパターンです
password カラムを nullable にしてしまって、「パスワードを設定していない」という状態を表現できるようにします

やり方は単純で以下のようなマイグレーションを書くか、そもそも users テーブルの作成時に password を nullable にしておきます

Schema::table('users', function (Blueprint $table) {
    $table->string('password')->nullable()->change();
});

パスワードが存在しないことを許容できるため、Socialite 経由で登録したユーザーも適切に扱えます
1. Socialite 経由でユーザーを作成する場合になんらかのパスワードを設定するパターン で述べたような体験の問題も発生せず、DB に意味のないパスワードを保存する必要もありません

ただ、「パスワードを NULL にして認証大丈夫なの?」ってちょっと気になりませんか?
たとえば

  • 「メールアドレスによるログイン画面でパスワードフォームに何も入力せずにログインしてみたらいけちゃうんじゃないの?」
  • 「password に NULL が入った状態でログイン試行できちゃう経路があったら、メールアドレスだけ分かればログインできちゃうからやばいんじゃないの?」

とかですね

個人的に引っかかったポイントだったので Laravel のソースコードを追って確認してみました
結論から言うと大丈夫でした

ユーザーの認証時に使われるユーザープロバイダー( config/auth.phpproviders->users->driver で指定するアレです)には eloquentdatabase の2種類があります

この指定で使われる実際のユーザープロバイダークラスは以下の二つです

これらは UserProvider インターフェースを implements しており、ログイン時の検証で validateCredentials メソッドが呼び出されます
この関数が true を返せば認証成功、false を返せば認証失敗としているわけです

EloquentUserProvider クラスでは以下のような処理をしています

https://github.com/laravel/framework/blob/2b16766ef026ba0dca42bbc2189671f5fbfca934/src/Illuminate/Auth/EloquentUserProvider.php#L149-L156

$credentials['password'] に入力されたパスワードが入ってきますが、ここが仮に NULL であったとしてもしっかり false を返してくれています

続いて DatabaseUserProvider クラスです

https://github.com/laravel/framework/blob/2b16766ef026ba0dca42bbc2189671f5fbfca934/src/Illuminate/Auth/DatabaseUserProvider.php#L155-L160

こちらは Hasher の check メソッドを呼び出しており、実際には以下のメソッドが呼び出されます

https://github.com/laravel/framework/blob/2b16766ef026ba0dca42bbc2189671f5fbfca934/src/Illuminate/Hashing/AbstractHasher.php#L26-L33

こちらも NULL の場合 false を返すようになっていますね

password の入力値が NULL になってしまう経路が仮に存在したとしてもちゃんと弾いてくれていることが確認できました 🎉

まとめ

  • Socialite のみで認証する、つまりメールアドレス/パスワードによる認証機構を持たずすべて Socialite に認証を任せる場合は単に password カラムを消してしまうのがシンプル
  • Socialite + メールアドレス/パスワード認証を提供する場合は password フィールドを nullable にする

Laravel の勉強を始めてから気になったポイントだったので調査してまとめてみました
何かの参考になれば幸いです

Discussion