Laravelで独自認証を利用する

# Laravelで独自認証ロジックを追加する

Laravelで独自認証ロジックを追加する方法を探してみたところ、バージョンの古いものは見つかるのだけど、新しいもの(2018/12現在は、Laravel 5.7)にそのまま入れてもうまくいかなかったので、こうやったらうまくいったというのをメモ。

## 何をしたかったか

APIを実装するにあたって、認証をBearer Tokenを使って、認証後はLaravelの標準処理に任せたかった。

## やろうと試みたこと（実装にあたって試みたこと)

- config/auth.php のguards、providersに適当なクラスを入れ込んで確認
    どうやってもセッションで認証をしようとする。

- app/Providers/AuthServiceProvider.php
    追加はなんとなく出来たが、カスタマイズがしにくく、さらにroutes/api.phpでコントロールがしにくかった。

## 最終的に実装した方法

色々やってみた結果、以下の実装に落ち着いた。
- [app/Guards/BearerGuard.phpを作成](#guard)
- [app/Http/Middleware/Guard/Provider/Modelを作成](#middleware)
- [app/Providers/AuthCustomServiceProvider.phpを作成](#provider)
- [app/Models/Auth/User.phpを作成](#model)
- [app/Http/Kernel.phpにMiddlewareクラスを追加](#kernel)
- [app/Exceptions/Handler.phpの処理を変更](#exceptions)

### Guard

<details>
<summary>認証方法が独自(特定のリクエストヘッダーに設定されているトークンを使って認証)なので、新しいGuardを作成する。</summary>
<div>

```app/Guards/BearerGuard.php
<?php

namespace App\Guards;

use Log;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\GuardHelpers as GuardHelpers;

class BearerGuard implements Guard
{
    use GuardHelpers;

    /** @var \Illuminate\Http\Request Request instance */
    protected $request;
    /** @var \Illuminate\Contracts\Auth\UserProvider Provider instance */
    protected $provider;
    /** @var \App\Models\Auth\User User Model instance */
    protected $user;

    /**
     * Create a new authentication guard.
     *
     * @param  \Illuminate\Contracts\Auth\UserProvider $provider
     * @param  \Illuminate\Http\Request $request
     * @return void
     */
    public function __construct(UserProvider $provider, Request $request)
    {
        $this->request = $request;
        $this->provider = $provider;
        $this->user = null;
    }

    /**
     * Determine if the current user is authenticated.
     *
     * @return bool
     */
    public function check()
    {
        return !is_null($this->user());
    }

    /**
     * Determine if the current user is a guest.
     *
     * @return bool
     */
    public function guest()
    {
        return !$this->check();
    }

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if (!is_null($this->user)) {
            return $this->user;
        }

        $user = new User();

        // Login with Bearer Token
        $token = $this->request->bearerToken();
        if (!is_null($token) && $this->user = $this->provider->retrieveByToken($user->getRememberTokenName(), $token)) {
            $this->fireAuthenticatedEvent($this->user);
        }

        return $this->user;
    }

    /**
     * Get the ID for the currently authenticated user.
     *
     * @return string | null
     */
    public function id()
    {
        if ($user = $this->user()) {
            return $this->user()->getAuthIdentifier();
        }
        return null;
    }

    /**
     * Validate a user's credentials.
     *
     * @param array $credentials credentials user input
     * @return bool
     */
    public function validate(array $credentials = [])
    {
        return new \Exception('Not implemented');
    }

    /**
     * Set the current user.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user User info
     * @return void
     */
    public function setUser(Authenticatable $user)
    {
        $this->user = $user;
    }

    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array $credentials
     * @param  bool $remember
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false)
    {
        return new \Exception('Not implemented');
    }

    /**
     * Determine if the user matches the credentials.
     *
     * @param  mixed $user
     * @param  array $credentials
     * @return bool
     */
    protected function hasValidCredentials($user, $credentials)
    {
        return new \Exception('Not implemented');
    }

    /**
     * Fire the authenticated event if the dispatcher is set.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @return void
     */
    protected function fireAuthenticatedEvent($user)
    {
        if (isset($this->events)) {
            $this->events->dispatch(
                new Events\Authenticated(
                    $this, $user
                ));
        }
    }
}
```

</div>
</details>

### Middleware

<details>
<summary>routes/api.phpを使って分離したかった＆認証エラーのときにリダイレクトせずに401を返したかったので、Middlewareを新たに作成。</summary>
<div>

```app/Http/Middleware/AuthenticateViaApi.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;

class AuthenticateViaApi
{
    /** @var \Illuminate\Contracts\Auth\Factory The authentication factory instance. */
    protected $auth;

    /**
     * @var string Application Token name
     */
    const APPLICATIN_TOKEN = 'x-application-token';

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @param  string[] ...$guards
     * @return mixed
     */
    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($request, $guards);

        return $next($request);
    }

    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  array $guards
     * @return void
     */
    protected function authenticate($request, array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        abort(401);
    }

    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request $request
     * @return string
     */
    protected function redirectTo($request)
    {
        return response()->json('Unauthorized', 401);
    }
}
```

</div>
</details>

### Provider

<details>
<summary>config/api.php 内のproviderなどを有効にするための処理を入れ込む</summary>
<div>

```app/Providers/AuthCustomServiceProvider.php
<?php

namespace App\Providers;

use App\Extensions\UserCustomProvider;
use App\Guards\BearerGuard;
use App\Models\Auth\User;
use Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthCustomServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        $this->app->bind('App\Models\Auth\User', function ($app) {
            return new User();
        });
        // add custom guard provider
        Auth::provider('auth.bearer', function ($app, array $config) {
            return new UserCustomProvider($app->make('App\Models\Auth\User'));
        });
        // add custom guard
        Auth::extend('bearer', function ($app, $name, array $config) {
            $userProvider = app(UserCustomProvider::class);
            $request = $app->make('request');
            return new BearerGuard($userProvider, $request);
        });
    }

}
```

</div>
</details>

<details>
<summary>ユーザープロバイダーとモデルを関連付ける</summary>
<div>

```app/Extensions/UserCustomProvider.php
<?php

namespace App\Extensions;

use Hash;
use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\UserProvider;
use App\Models\Auth\User;

class UserCustomProvider implements UserProvider
{
    /** @var object User Model */
    private $model;

    /**
     * Create a new user provider.
     *
     * @param \App\Models\Auth\User User Model
     */
    public function __construct(\App\Models\Auth\User $userModel)
    {
        $this->model = $userModel;
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable | null
     */
    public function retrieveByCredentials(array $credentials)
    {
        return new \Exception('Not implemented');
    }

    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  array $credentials Request credentials
     * @return bool
     */
    public function validateCredentials(\Illuminate\Contracts\Auth\Authenticatable $user, Array $credentials)
    {
        return new \Exception('Not implemented');
    }

    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable | null
     */
    public function retrieveById($identifier)
    {
        return $this->model->where($this->model->getAuthIdentifierName(), $identifier)->first();
    }

    /**
     * Retrieve a user by by their unique identifier and "remember me" token.
     *
     * @param  mixed $identifier
     * @param  string $token
     * @return \Illuminate\Contracts\Auth\Authenticatable | null
     */
    public function retrieveByToken($identifier, $token)
    {
        return $this->model->where($this->model->getAuthIdentifierName(), $identifier)
            ->where($this->model->getRememberTokenName(), $token)
            ->first();
    }

    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  string $token
     * @return void
     */
    public function updateRememberToken(\Illuminate\Contracts\Auth\Authenticatable $user, $token)
    {
        $this->model->where($this->model->getAuthIdentifierName(), $user->getAuthIdentifier())
            ->update([$this->model->getRememberTokenName() => $token]);
    }
}
```

</div>
</details>

### Model

<details>
<summary>ユーザー情報を保存するモデルを作成。Token認証だけ考えるので、シンプルに。</summary>
<div>

```app/Models/Auth/User.php
<?php

namespace App\Models\Auth;

use Carbon\Carbon;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

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

    // Specify table name
    protected $table = 'users';
    // Stop automatic updating created_at and updated_at
    public $timestamps = false;
    // Specify format for the columns type of "Date / Time"
    protected $dateFormat = 'U';

    private $username;
    private $password;
    private $rememberTokenIdentifierName = 'token';

    protected $fillable = [
        'id',
        'token',
    ];
    protected $hidden = [
    ];

    public function __construct()
    {
        $this->hidden[] = $this->rememberTokenIdentifierName;
    }

    /**
     * Fetch user by Credentials
     *
     * @param array $credentials
     * @return Illuminate\Contracts\Auth\Authenticatable
     */
    public function fetchUserByCredentials(Array $credentials)
    {
        return new \Exception('Not implemented');
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
     */
    public function getAuthIdentifier()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
     */
    public function getAuthPassword()
    {
        return $this->password;
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
     */
    public function getRememberToken()
    {
        return $this->{$this->getRememberTokenName()};
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
     */
    public function setRememberToken($value)
    {
        $this->{$this->getRememberTokenName()} = $value;
    }

    /**
     * {@inheritDoc}
     * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
     */
    public function getRememberTokenName()
    {
        return $this->rememberTokenIdentifierName;
    }

}
```

</div>
</details>

### Kernel

<details>
<summary>routeに新しいMiddlewareを追加する。</summary>
<div>

```app/Http/Kernel.php
    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'auth.bearer' => \App\Http\Middleware\AuthenticateViaApi::class,  // この行を追加して読み込ませる
    ];
```

</div>
</details>

### Exceptions

<details>
<summary>JSONでリクエストを受けたときに、JSONでエラーを返せるように修正する。</summary>
<div>

```app/Exceptions/Handler.php

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Exception $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        $is_api = $request->is('api/*') || $request->expectsJson();
        if ($this->isHttpException($exception)) {
            if ($status_code = $exception->getStatusCode()) {
                $message = "";
                switch ($status_code) {
                    case 401:
                        $message = 'Unauthorized';
                        break;
                    case 403:
                        $message = 'Permission Denied';
                        break;
                    case 404:
                        $message = 'Not Found';
                        break;
                    case 410:
                        $message = 'Gone';
                        break;
                    default:
                        $message = 'Error Message Undefined';
                }
                return $is_api ?
                    response()->json(['status' => $status_code, 'error' => $message], $status_code) :
                    response($message, $status_code);
            }
        }

        return parent::render($request, $exception);
    }
```

</div>
</details>

## 使い方

<details>
<summary>このトークン認証を使うものは、別のところにまとめたかったので、app/Http/Controllers/Api 配下に配置。</summary>
<div>

```app/Http/Controllers/Api/TestController.php
<?php

namespace App\Http\Controllers\Api;

use Log;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Validator;

class TestController extends Controller
{
    use AuthenticatesUsers;

    public function test(Request $request)
    {
        $user = Auth::user();

        return response()->json(['status' => 'successful']);
    }
}
```

</div>
</details>

# 最後に

たぶん抜けてはいないと思いますが･･･不要だったはずの処理やコメントを削ったので、足りなかったらご指摘ください。

# 変更履歴
- 2018/12/10

    ソースコードが長くて見づらかったので、\<details>\</details>を使って、初期状態で非表示になるように変更