laravel 認証試す

Laravel8系で公式が用意している認証を試す。


## Laravelプロジェクトを作成

```
$ composer create-project laravel/laravel
```

## laravel uiを追加

```
composer require laravel/ui
```

laravel uiではなくLaravel BreezeやJetstreamを使うよう案内されているがとりあえずlaravel uiを使う

https://github.com/laravel/ui

> This legacy package is a very simple authentication scaffolding built on the Bootstrap CSS framework. While it continues to work with the latest version of Laravel, you should consider using Laravel Breeze for new projects. Or, for something more robust, consider Laravel Jetstream.


laravel uiをインストールするとコマンドが二つ使用可能になる。
ui:authでlogin等に使うviewを作成し、ui:contollersでLogincontroller等を作成する。

```
$ php artisan list
省略...
 ui
  ui:auth              Scaffold basic login and registration views and routes
  ui:controllers       Scaffold the authentication controllers
```

```
$ php artisan ui:auth
$ php artisan ui:controllers
```

## route追加
ログイン後にリダイレクトされるHomeController@indexとauthに必要なrouteを追加

```web.php
Route::middleware('auth:web')->group(function () {
    Route::get('/home', [\App\Http\Controllers\HomeController::class, 'index']);
});
\Illuminate\Support\Facades\Auth::routes();
```


## webサーバー起動
dbセットアップの後php artisan serveでwebサーバーを起動する。

http://127.0.0.1:8000/
![スクリーンショット 2021-01-17 20.46.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278474/8db812e4-fa4e-5e88-6bec-95c258e4427e.png)

右上でログイン/ログアウトできる。

## authミドルウェアを読む

Kernel.phpのrouteMiddlewareにauthが登録されている。authというkeyでAuthenticate::classが使用される。

```Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
```

Authenticate.phpはIlluminate\Auth\Middleware\Authenticateを継承している。

```
use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
```

## Illuminate\Auth\Middleware\Authenticate を読む
handle関数ではauthenticate関数が実行される。

```
    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($request, $guards);

        return $next($request);
    }
```


指定されたguardsを全てcheckし、guardが通ればshouldUseを実行。全てチェックに失敗すればunauthenticatedを実行する。

```php
    // 第二引数のguardsは `Route::middleware('auth:web')` で指定した時のwebが与えられる。
    protected function authenticate($request, array $guards)
    {
        // 引数が指定されていない場合はnullを持つ配列にする。
        // $this->auth->guardでnullを渡すとconfigのauth.phpのdefault guardが使用される。
        if (empty($guards)) {
            $guards = [null];
        }


        foreach ($guards as $guard) {
            // `$this->auth` で参照されるインスタンスはコンストラクタでDIされる。
            // Authの型はIlluminate\Foundation\Applicationで指定されており、\Illuminate\Auth\AuthManager::classになる。
            // $this->auth->guard関数でguard文字列からguardの実態を返す
            // check関数で現在のユーザーが認証されているかどうかを判断します。
            if ($this->auth->guard($guard)->check()) {
                // 認証済みのguardがあればデフォルトドライバーとして登録する
                return $this->auth->shouldUse($guard);
            }
        }

        // 未認証の場合はunauthenticatedを実行
        $this->unauthenticated($request, $guards);
    }
```

guardの実態は↓の命名規則で決まる `\Illuminate\Auth\AuthManager` の関数で返されるインスタンスか、カスタムガード登録時に指定したインスタンスになる。
 

```php

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';
        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }
```

https://qiita.com/tomoeine/items/40a966bf3801633cf90f#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0-guard-%E3%81%AF%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E7%99%BB%E9%8C%B2%E3%81%95%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%98%E3%82%83

### unauthenticated関数
unauthenticatedでは認証エラーをスローする。redirectToでリダイレクトさせるpathを指定することが可能。

```
    protected function unauthenticated($request, array $guards)
    {
        throw new AuthenticationException(
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }
```

## session driver
guardにsessionを指定するとAuthManager.phpのcreateSessionDriver関数が実行され、`SessionGuard`インスタンスが作成される。

SessionGuardのcheck関数で現在のユーザーが認証されているかどうかを判断する。check関数自体はGuardHelpers traitで実装されている。

```php

    public function check()
    {
        return ! is_null($this->user());
    }
```

`$this->user()`はGuardHelpersを実装したクラスで実装されている。

### session driverのuser関数
セッションからuserを取り出して返す。

```php

    public function user()
    {
        if ($this->loggedOut) {
            return;
        }

        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }

        // getName()はガードに付けられた名前で、基本的にはwebになる
        $id = $this->session->get($this->getName());

        // First we will try to load the user using the identifier in the session if
        // one exists. Otherwise we will check for a "remember me" cookie in this
        // request, and if one exists, attempt to retrieve the user using that.
        if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
            $this->fireAuthenticatedEvent($this->user);
        }

        // If the user is null, but we decrypt a "recaller" cookie we can attempt to
        // pull the user data on that cookie which serves as a remember cookie on
        // the application. Once we have a user we can return it to the caller.
        if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
            $this->user = $this->userFromRecaller($recaller);

            if ($this->user) {
                $this->updateSession($this->user->getAuthIdentifier());

                $this->fireLoginEvent($this->user, true);
            }
        }

        return $this->user;
    }
```


## ログイン処理を追う

laravel ui でログイン用のコントローラーが作成される。
具体的な処理は`AuthenticatesUsers`で実装されている

```LoginController.php
class LoginController extends Controller
{
    use AuthenticatesUsers;
```

ログイン処理では
1. requestのvalidateチェック
2. ログイン試行回数チェック
3. attemptLoginでログインを試す
4. ログインに失敗したらログイン試行回数をインクリメント

```AuthenticatesUsers.php 
    public function login(Request $request)
    {
        $this->validateLogin($request);

        // If the class is using the ThrottlesLogins trait, we can automatically throttle
        // the login attempts for this application. We'll key this by the username and
        // the IP address of the client making these requests into this application.
        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }
```

### attemptLogin
requestから認証に必要な情報を取得してguardに渡してログインを試す

```php
    protected function attemptLogin(Request $request)
    {
        // `$this->guard()`では`Auth::guard()`が実行され、configのauth.phpのデフォルトガード(で指定されているドライバー)が返される。
        // credentials関数でuserNameとpasswordだけ抽出する
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }
```

## SessionGuardのattempt
デフォルトでWebガードが指定されているのでSessionGuardのattemptを見る。
`$this->provider->retrieveByCredentials`でuserを取得し、`$this->hasValidCredentials($user, $credentials)`で認証している。
認証に通ればlogin関数を実行する

```php

    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);

        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

        // If an implementation of UserInterface was returned, we'll ask the provider
        // to validate the user against the given credentials, and if they are in
        // fact valid we'll log the users into the application and return true.
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

        // If the authentication attempt fails we will fire an event so that the user
        // may be notified of any suspicious attempts to access their account from
        // an unrecognized user. A developer may listen to this event as needed.
        $this->fireFailedEvent($user, $credentials);

        return false;
    }
```

プロバイダーは`CreatesUserProviders` traitの`createUserProvider`関数で作成しており、Eloquentプロバイダーの場合は`EloquentUserProvider.php`インスタンスが使用される。
EloquentUserProviderの`retrieveByCredentials`関数は以下になっている。
引数のcredentialsを使ってqueryを組み立てていることが分かる。ポイントとしてここではqueryにパスワードは含めない。
email/passwordでログインする場合はここではemailのみを使ったqueryになる。
ちなみにretrieve(リトリーブ)は取り出すと言う意味。retryと文字が似ているが関係ない。

```php
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            Str::contains($this->firstCredentialKey($credentials), 'password'))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        foreach ($credentials as $key => $value) {
            // パスワードはここではチェックしない
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }
```

`hasValidCredentials`関数で認証情報と一致しているかチェックする
条件は
1. userが取得できている
2. providerを使った秘密情報のvalidateに成功する

```php
    protected function hasValidCredentials($user, $credentials)
    {
        $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);

        if ($validated) {
            $this->fireValidatedEvent($user);
        }

        return $validated;
    }
```

`validateCredentials`関数は以下になっており、ユーザーが指定したパスワードとハッシュ化して保存してあるuserレコードのパスワードを一致判定している。

```php

    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }
```

## SessionGuardのlogin処理
providerによるuserの取得と認証検証に成功するとlogin処理が実行される。
ここでは以下が行われる。
1. sessionの更新
2. remember処理
3. $userを認証済みuserとして登録

```
    public function login(AuthenticatableContract $user, $remember = false)
    {
        $this->updateSession($user->getAuthIdentifier());

        // If the user should be permanently "remembered" by the application we will
        // queue a permanent cookie that contains the encrypted copy of the user
        // identifier. We will then decrypt this later to retrieve the users.
        if ($remember) {
            $this->ensureRememberTokenIsSet($user);

            $this->queueRecallerCookie($user);
        }

        // If we have an event dispatcher instance set we will fire an event so that
        // any listeners will hook into the authentication events and run actions
        // based on the login and logout events fired from the guard instances.
        $this->fireLoginEvent($user, $remember);

        $this->setUser($user);
    }
```

### sessionの更新

```
    protected function updateSession($id)
    {
        $this->session->put($this->getName(), $id);

        $this->session->migrate(true);
    }
```

sessionのキーは↓の書式になる

```
    public function getName()
    {
        return 'login_'.$this->name.'_'.sha1(static::class);
    }
```