Laravel5.5でAPI認証のパッケージ(Laravel Passport)を利用する

- Laravel Passportの動作をmacOSで確認したメモ

# 環境
## macOS Sierra

## APIサーバー
- Vagrant
- VirtualBox
- Laravel5.5

https://github.com/niiyz/Laravel-API-Auth-Sample

## クライアントサーバー

- MacのPHPビルトインサーバー

https://github.com/niiyz/Laravel-API-Auth-Client-Server-Sample

# MacのPHPバージョンを7.0にする

- Laravel5.5はPHP7.0以上が必須

## brewを使用してPHP7インストール

```
brew update
brew install homebrew/php/php70  
```

## パス設定
- シェルの設定に追記後、反映

``` sh
vi ~/.zshrc
export PATH="$(brew --prefix homebrew/php/php70)/bin:$PATH" <-1行追記する
source ~/.zshrc
```

## 確認
- PHP7.0を確認

```sh
php -v                                                                                   
PHP 7.0.22 (cli) (built: Aug  7 2017 14:07:27) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
```

# APIサーバー(Laravel5.5)

## Laravel5.5インストール

```php
composer create-project --prefer-dist laravel/laravel Laravel-API-Auth-Sample dev-develop
```

## プロジェクト直下にHomesteadインストール(vagrant)

```
composer require laravel/homestead --dev
php vendor/bin/homestead make
```

- 起動

```
vagrant up
```

- SSH接続

```
vagrant ssh
```

## ユーザー認証(通常のユーザー作成準備)

- ユーザーテーブルSeeder作成

```
php artisan make:seeder UsersTableSeeder
```

### データ追加処理

- ユーザーテーブルSeeder追記

```database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use App\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(User::class)->create([
            'name' => 'テスト',
            'email' => 'test@example.com'
        ]);
    }
}
```

- DatabaseSeeder.phpのコメント解除

``` database/seeds/DatabaseSeeder.php
// コメントアウト
$this->call(UsersTableSeeder::class);
```

### ログイン機能追加

- ログイン画面用のリソースなどが追加される

```php
php artisan make:auth
```


## API認証

### passportインストール

```php
composer require laravel/passport
```

### ~~Provider追記~~
 <a href='#comment-50a6d2ba03f5f77affbd'>Laravel PassportパッケージはPackage Auto-Discoveryに対応しているためインストールした際に自動でサービスプロバイダに追加されるのでconfig/app.php記述する儀式が不要になったようです。</a>

``` config/app.php
/*
 * Package Service Providers...
 */
// Laravel\Passport\PassportServiceProvider::class, // 1行追記

/*
 * Application Service Providers...
 */
```

### DB初期化

```
php artisan migrate
```

- UserSeederに記述したユーザーがusersテーブルに作成される

```
id         1
name       テスト
email      test@example.com
password   secret
```

- OAuth用のテーブルが作成される

```
oauth_auth_codes
oauth_clients
oauth_personal_acces_clients
oauth_refresh_tokens
```

### OAuth認証のユーザーを作成する
- 「user ID」 はusers.id を指定する
- callback用のURLを指定
- 「Client ID」,「Client secret」がoauth_clientsテーブルに追加される


```shell-session
$ php artisan passport:client

 Which user ID should the client be assigned to?:
 > 1

 What should we name the client?:
 > test

 Where should we redirect the request after authorization? [http://localhost/auth/callback]:
 > http://localhost:3333/callback.php

New client created successfully.
Client ID: 1
Client secret: cZ9W9ysdUFKlowO5d2CzzFFJ1uOBfggjv5Jx5cwr
```




# クライアントサーバー(Macビルドインサーバー)

### 認証画面を通すやり方

- よくあるtwitterのアカウントで別サービスにログインする感じのやつ

#### oAuthサバーへアクセスする

```get_token.php
<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client as Client;

$guzzle = new Client;

$query = http_build_query([
    'client_id'     => '1',
    'redirect_uri'  => 'http://localhost:3333/callback.php',
    'response_type' => 'code',
    'scope'         => '',
]);

header('Location: http://192.168.10.10/oauth/authorize?' . $query);
```

#### 承認画面

<img width="1093" alt="スクリーンショット 2017-09-05 20.23.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/9907/bb60a354-8c77-9c75-9384-234a05443365.png">

- 「Authorize」ボタンを押下するとcodeパラメータ付きでコールバックURLに戻る
- 「Cancel」ボタン押下するとerrorパラメータ付きでコールバックURLに戻る
- 承認するとoauth_access_tokens, oauth_auth_codesテーブルにデータが格納される

#### コールバック

```get_token_callback.php
<?php 
require 'vendor/autoload.php';

use GuzzleHttp\Client as Client;

$http = new Client;

if ($_GET['code']) {
    $response = $http->post('http://192.168.10.10/oauth/token', [
        'form_params' => [
            'grant_type'    => 'authorization_code',
            'client_id'     => '1',
            'client_secret' => 'cZ9W9ysdUFKlowO5d2CzzFFJ1uOBfggjv5Jx5cwr',
            'redirect_uri'  => 'http://localhost:3333/callback.php',
            'code'          => $_GET['code'],
        ],
    ]);
    var_dump(json_decode((string)$response->getBody(), true));
}
```

### 出力結果

- 取得したaccess_tokenを使って外部API認証に使う

```php
array(4) {
  ["token_type"]=>
  string(6) "Bearer"
  ["expires_in"]=>
  int(1296000)
  ["access_token"]=>
  string(1071) "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImYxMmE0NDZkNmQxZjBlNzA4MWMwZWEwMzg5MDkxOWI1NWEzYzE5YTMxMGRlMGYwOWQ0ZjA4MmMzYTcxNGU1MzhmOGMzZTk3MzllZTc3OWQ1In0.eyJhdWQiOiIxIiwianRpIjoiZjEyYTQ0NmQ2ZDFmMGU3MDgxYzBlYTAzODkwOTE5YjU1YTNjMTlhMzEwZGUwZjA5ZDRmMDgyYzNhNzE0ZTUzOGY4YzNlOTczOWVlNzc5ZDUiLCJpYXQiOjE1MDQ2MDg2NTcsIm5iZiI6MTUwNDYwODY1NywiZXhwIjoxNTA1OTA0NjU3LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.kbvE4yrfGXcI4Bg0VW10-sDlFpsiG-_UB4uRIi_ylpB46kUkto3JdmlfWLINM83RWNrSkBdnzq6IYfPiHZY1wOw6qwObKG97pWhQVZO9yizVlZCe-ejRO-xv4do0QbXZQHzQqACwhSAYdQwymEJr3JqjrcaNUU8UV5wftlfLsDYNp-NVdo9hvq_7o49B79tIGG7F2izj0mmDHKXfhWLg5s8rFgkHw1q-Wn6o-YCMHgQhfWR9IVvJJ9bpywe45gcvse-MqaaFIQL4q-PHcSLAP0JHMrWLTHy0S2O-D10nWNE2XJyh3VPnxnKLH2jrGZpQVRpUN6RNQlftCxVJEoPruhyeE0Emb5KNJkwvFeIn2hEmLIseXqD4kuk0VESNlCz3VD0r6HRtvfpCTbHBK2uj_l69oYV0yUC6r_D-BfBK6ATZdTk-RIk3HV41ksMqOfpuSPymLoAieq_WqmU0i93VoACwoycirP6z1JTMuT4E1z8i_etgiodcOGpRWZQrePACURCxxGk1xGCaSRMVF_c42WNC5cw10wcHld9FToeFD4-KT_nBryz_eOMJ9Iw6foiyusRbR0vriGExOAesSiLpgLwnDEFBh3YpsDiOEVPvMJolMlVQ6tOnaUwyC3qunjrjVlbF9YR40r1b9DVJ7ZhhWGJNgYg0qadbNGt2uLIYscU"
  ["refresh_token"]=>
  string(706) "def50200ce6524158569f59de11fd955552ae132336954dbd53dffc22544a3af145d1cc0b2b78c36b3c65c238c1486f24b3b551061591a84ec92d68d4438c62de3e6bcffa0df6ce1fa7ea127078a107390b8d3023897eff6cef63064e39b9376ab4ae4e685cc5b9b6b3ed67efe312de73c06458b78c6e7b8e6719d4e11cc9e57315ebb36b0b5e869ac9c0a8ae5f973e12ae2d26f61192d0532c7aeff90e6b7802fc6bd78a7303583bee7f14fabf31486e6431320e6f3d206c61af9dfb482b8195fb0577e9f5cefeb765742400aec2f3f9e16d3b70c5d21732347190bae5287cd13ad01f0b42e1cd3cfc51c193bfce2ffcb5b06f75224c1a7c6498657d2320fd44a78bc047b9bf2bdd17654b69ce9c74280d42bab8be6619be419ee84700d7086ff638a9b715290286906274bccbe9b876afb1b83382b2b2c9840423210e3b5ca4c9cab938391844e261ca6b247e88b3bc96b18d505784c87ef6932c1ed9dec7cc8"
}
```

## 認証画面なしでパスワード付きで外部からAPIを実行する

- 認証画面なしで外部からAPIを通してデータを保存したかったのでパスワード付きの認証にする

### OAuth認証のユーザーを作成(APIサーバー)

- パスワードオプション付きでOAuthユーザー作成
- usersテーブルのidとは紐付けなくても良いみたい

```shell-session
# php artisan passport:client --password --name=password-user
Password grant client created successfully.
Client ID: 6
Client Secret: IzRQarKQ5y7b8Ni4Sf4nBvlDxeDAujm10lH6r3OW

```

### API作成(APIサーバー)

- 外部からアクセスしてデータを保存するAPI

```routes/api.php
Route::middleware('auth:api')->get('/memo', function (Request $request) {

    $memo = $request->get('memo');

    // SAVE MEMO...

    return json_encode(array_merge(['status' => 'OK'], ['memo' => $memo . '-saved']));
});
```


### クライアントサーバー(Macビルドインサーバー)

- OAuthユーザーのID, Secretを使用する
- 併せて通常のLaravelログインユーザーのID, Passwordも使用する

```password_auth.php
<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client as Client;

/**
 * Class APIClient
 */
class APIClient {

    /**
     * @var Client
     */
    protected $http;

    /**
     * @var
     */
    protected $accessToken;

    /**
     *
     */
    const API_SERVER = 'http://192.168.10.10';

    /**
     * APIClient constructor.
     */
    public function __construct()
    {
        $this->http = new Client;
    }

    /**
     * @param array $config
     */
    public function auth(array $config = [])
    {

        $response = $this->http->post(self::API_SERVER . '/oauth/token', [
            'form_params' => [
                'grant_type'    => 'password',
                'client_id'     => $config['client_id'],
                'client_secret' => $config['client_secret'],
                'username'      => $config['username'],
                'password'      => $config['password'],
                'scope'         => '',
            ],
        ]);

        $body = json_decode((string)$response->getBody(), true);

        $this->accessToken = $body['access_token'];

    }

    /**
     * @param string $memo
     */
    public function save(string $memo = '')
    {

        $response = $this->http->request('GET', self::API_SERVER . '/api/memo', [
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer '. $this->accessToken,
            ],
            'query' => ['memo' => $memo],
        ]);

        $body = json_decode((string)$response->getBody(), true);

        if (isset($body['status']) && $body['status'] === 'OK') {
            var_dump('saved memo');
        }
    }
}

// write .env etc
$config = [
    'client_id'     => '6',
    'client_secret' => 'IzRQarKQ5y7b8Ni4Sf4nBvlDxeDAujm10lH6r3OW',
    'username'      => 'test@example.com',
    'password'      => 'secret',
];

$client = new APIClient;

$client->auth($config);
$client->save('ほげ');
// string(10) "saved memo" array(2) { ["status"]=> string(2) "OK" ["memo"]=> string(12) "ほげ-saved" }
```

# 参照

https://github.com/laravel/passport
https://laravel.com/docs/5.5/authentication#authenticating-users
https://laravel.com/docs/master/passport#introduction