クイック エンジニアリングブログ

株式会社クイック Web事業企画開発本部のエンジニアリングチームが運営する技術ブログです。

Laravel Sanctumを使って簡単にパーソナルアクセストークン作ってみよう

こんにちは、Czです。
最近急に朝寒くなりましたね。

パーソナルアクセストークン(PAT)とは

Githubとかで使用されているパスワードの代わりに使用するトークンです。
これを使うことでコマンドラインまたはAPIの認証をすることができます。

Laravel Sanctumとは

f:id:aimstogeek:20211021180613p:plain

Sanctumとは「聖域」という意味らしいです。
認証周りを提供してくれているので名前通りかもしれませんね。

Laravel Sanctumは以下の2つの複雑さを解消するために存在しています。

今回は「APIトークン」に焦点を当ててお伝えします。

なお、Laravel Sanctum は Laravel8 から標準で入っています。
もし7以前で使いたい場合は、以下のコマンドでインストールすれば行けるはずです。

composer require laravel/sanctum

準備

ではまっさらな状態から作っていきましょう。
Laravel Sail を使って環境を構築します。

curl -s "https://laravel.build/sanctum-sample" | bash
cd sanctum-sample
./vendor/bin/sail up

これで環境が立ち上がりました。
http://localhost にアクセスして動いているかどうか確認してみてください。

...

では続いてSanctumを使ってtokenを作っていきたいのですが、tokenを作るためにはログインできなければなりません。

ログイン機能を1から作ると今(午後5時)から始めると日が暮れてしまうので、Laravel Jetstream を使ってサクッと作っていきましょう。
ちなみにJetstreamを入れなくてもSanctumの恩恵は受けることができるので最後まで見て不要だなと思われた方はインストールしなくても大丈夫です。

Laravel Jetstream のインストール

以下のコマンドでJetstreamをインストールすることができます。

./vendor/bin/sail composer require laravel/jetstream
./vendor/bin/sail artisan jetstream:install inertia

install時に指定している「inertia」ですが、他にも「livewire」を指定することができます。
両者の違いとしてlivewireは「blade」、inertiaは「Vue」を使ってフロント側が構築されます。
私の所属するプロジェクトではVueを使っているので今回はinertiaを指定しています。

...

これで認証機能が使えるようになりました。

使ってみよう(Jetstream)

では早速 http://localhost/register へアクセスしてみましょう。

以下のように登録画面が表示されたはずです。

f:id:aimstogeek:20211021155041p:plain

入力して REGISTER ボタンを押して登録してみます。

f:id:aimstogeek:20211021153943p:plain

このようにダッシュボード画面が表示されたはずです。

ダッシュボードの右上のアカウント名をクリックしてみます。

「Profile」と「Log Out」の2つの項目がありますね。

f:id:aimstogeek:20211021154026p:plain

デフォルトではToken機能が有効になっていないので有効化する必要があります。

jetstream.phpを開き、Features::api()コメントアウトを外します。

config/jetstream.php

'features' => [
    // Features::termsAndPrivacyPolicy(),
    // Features::profilePhotos(),
    Features::api(),
    // Features::teams(['invitations' => true]),
    Features::accountDeletion(),
],

そうすると、先ほどのメニューに「Api Tokens」が追加されます。

f:id:aimstogeek:20211021154052p:plain

tokenを作ってみよう

メニューからApi Tokensを選択すると次の画面が表示されます。

f:id:aimstogeek:20211021154121p:plain

Nameに「readonly」を入れて、
readにのみチェックを入れて、
CREATE ボタンを押します。

f:id:aimstogeek:20211021154138p:plain

こんな感じでTokenが表示されたはずです。
このTokenは一度限りしか表示されないのでメモしておきましょう。

uScF3qsBW2duh4UoCzyIkkpKwzlEHrDs0mtc0G7B

このtokenを使ってAPI認証することがPATの役割となっています。

tokenを使ってみよう

api.phpを見てみます。

routes/api.php

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

middlewareで「auth:sanctum」ガードが指定されています。 このガードではAuthorizationヘッダのトークンを使ってリクエストの認証を行います。

では、以下のコマンドを実行してみましょう。

// curl -H "Authorization: Bearer <token>" http://localhost/api/user
curl -H "Authorization: Bearer uScF3qsBW2duh4UoCzyIkkpKwzlEHrDs0mtc0G7B" http://localhost/api/user
{"id":2,"name":"Cz","email":"cz-hogehoge-dummy@google.com","email_verified_at":null,"current_team_id":null,"profile_photo_path":null,"created_at":"2021-10-20T16:14:15.000000Z","updated_at":"2021-10-20T16:14:15.000000Z","profile_photo_url":"https:\/\/ui-avatars.com\/api\/?name=Cz&color=7F9CF5&background=EBF4FF"}

トークンを発行した時のユーザの情報が取れていれば成功です。

...

「ん〜動いたけど、Jetstreamがよしなにやってくれてるからどうやって使うかイメージ湧かない。 」 って方はこの先も読み進めることをお勧めします。

理解しよう

ここからはJetstreamとSanctumの処理を追っていきながら、どう使えばいいか理解を深めていきましょう。

Jetstreamで有効にしたAPI機能の中を見てみる

先ほど config/jetstream.php でFeatures::api()のコメントアウトを外しましたが、
それによってどうなるかを見てみます。

今回はinertiaでインストールしているので以下のファイルを確認します。

vendor/laravel/jetstream/routes/inertia.php

// API...
if (Jetstream::hasApiFeatures()) {
    Route::get('/user/api-tokens', [ApiTokenController::class, 'index'])->name('api-tokens.index');
    Route::post('/user/api-tokens', [ApiTokenController::class, 'store'])->name('api-tokens.store');
    Route::put('/user/api-tokens/{token}', [ApiTokenController::class, 'update'])->name('api-tokens.update');
    Route::delete('/user/api-tokens/{token}', [ApiTokenController::class, 'destroy'])->name('api-tokens.destroy');
}

このようにindex,store,update,destroyの4つの機能が使えるようになるみたいです。

tokenを生成処理はApiTokenController#store()を呼び出しているようなので中を確認してみます。

vendor/laravel/jetstream/src/Http/Controllers/Inertia/ApiTokenController.php

/**
 * Create a new API token.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\RedirectResponse
 */
public function store(Request $request)
{
    $request->validate([
        'name' => ['required', 'string', 'max:255'],
    ]);

    $token = $request->user()->createToken(
        $request->name,
        Jetstream::validPermissions($request->input('permissions', []))
    );

    return back()->with('flash', [
        'token' => explode('|', $token->plainTextToken, 2)[1],
    ]);
}

ログインしているユーザのcreateToken()を使ってトークンの生成が行われていることがわかります。

この機能はUserモデルがtraitで実装している「HasApiToken」で提供されているようです。

HasApiTokenのcreateToken()を見てみる

vendor/laravel/sanctum/src/HasApiTokens.php

/**
 * Create a new personal access token for the user.
 *
 * @param  string  $name
 * @param  array  $abilities
 * @return \Laravel\Sanctum\NewAccessToken
 */
public function createToken(string $name, array $abilities = ['*'])
{
    $token = $this->tokens()->create([
        'name' => $name,
        'token' => hash('sha256', $plainTextToken = Str::random(40)),
        'abilities' => $abilities,
    ]);

    return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}

これを見るとplainTextTokenはStr::random()で生成されており、それをsha256でハッシュ化したものをtokenとして保存しているようですね。
(保存する際にkeyを|で結合しちゃっているのが気になりますが...)

さらに見ていくと、 戻り値はNewAccessTokenというクラスで、
accessTokenとして保存されるクラスはPersonalAccessTokenだということがわかります。

vendor/laravel/sanctum/src/NewAccessToken.php

/**
 * Create a new access token result.
 *
 * @param  \Laravel\Sanctum\PersonalAccessToken  $accessToken
 * @param  string  $plainTextToken
 * @return void
 */
public function __construct(PersonalAccessToken $accessToken, string $plainTextToken)
{
    $this->accessToken = $accessToken;
    $this->plainTextToken = $plainTextToken;
}

PersonalAccessTokenを見るとEloquenモデルを継承しているのでDBで管理していることがわかりますね。

use Illuminate\Database\Eloquent\Model;
use Laravel\Sanctum\Contracts\HasAbilities;

class PersonalAccessToken extends Model implements HasAbilities

対応するpersonal_access_tokenテーブルを確認してみましょう。

personal_access_tokenテーブルを見てみる

Laravel sailではdocker-composeの機能を内包しているので、以下のコマンドでdockerで立ち上がっているmysqlサーバにアクセスすることができます。

./vendor/bin/sail mysql

personal_access_tokenテーブルが存在するsanctum_samleのDBに切り替えます。

mysql> use sanctum_sample;
Database changed
mysql> show tables;
+--------------------------+
| Tables_in_sanctum_sample |
+--------------------------+
| failed_jobs              |
| migrations               |
| password_resets          |
| personal_access_tokens   |
| sessions                 |
| users                    |
+--------------------------+

そして登録されているtoken情報を確認してみます。

mysql> select * from personal_access_tokens\G;
*************************** 1. row ***************************
            id: 1
tokenable_type: App\Models\User
  tokenable_id: 1
          name: readonly
         token: 2dbe71b931d2f008c9e3555cce089b399d66b99e39acbf31b059bb63f25be3c3
     abilities: ["read"]
  last_used_at: NULL
    created_at: 2021-10-18 15:18:04
    updated_at: 2021-10-18 15:20:17
1 rows in set (0.01 sec)

先ほど作成したtokenがありましたね。 read権限はabilitiesカラムの中におり、json形式で複数設定できるようにしているみたいですね。

保存先が「personal_access_tokens」ということがわかったところで一旦戻ります

plainTextTokenの返却部分を見てみる

再度ApiTokenController#store()を見てみましょう

vendor/laravel/jetstream/src/Http/Controllers/Inertia/ApiTokenController.php

/**
 * Create a new API token.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\RedirectResponse
 */
public function store(Request $request)
{
    $request->validate([
        'name' => ['required', 'string', 'max:255'],
    ]);

    $token = $request->user()->createToken(
        $request->name,
        Jetstream::validPermissions($request->input('permissions', []))
    );

    return back()->with('flash', [
        'token' => explode('|', $token->plainTextToken, 2)[1],
    ]);
}

前の画面にリダイレクトする際にflash messageとしてplainTextTokenを返しています。 これでユーザにtokenを渡しているということですね。
なおplainTextTokenはこのように($token->getKey().'|'.$plainTextToken)keyが結合されているので、わざわざ|で分割して純粋なtokenのみを抽出しています。
(explodeの第3引数でlimit=2を指定しているところは丁寧だなぁと思いました)

ここまでがSanctumとJetstreamで行っているtoken生成の流れでした。

最小構成で作ってみよう

Jetstreamのコードを追っていって作り方が分かったと思うので最小構成で作ってみましょう。

tokenの作成

supermanというabilityを持つtokenを生成するロジックです。
以下のコードをweb.phpに追記します。(たったこれだけ)

routes/web.php

Route::middleware(['auth:sanctum', 'verified'])->get('/token/create/{name}', function (Request $request) {
    $token = $request->user()->createToken(
        $request->name,
        ['superman']
    );
    return explode('|', $token->plainTextToken, 2)[1];
})

/token/create/<任意のtoken名>にアクセスするとplainTextTokenが画面に表示されるはずです。
とりあえずこのtokenは控えておきます。

DBにレコードが追加されているか確認してみましょう。

mysql> select * from personal_access_tokens order by id desc limit 1\G;
*************************** 1. row ***************************
            id: 4
tokenable_type: App\Models\User
  tokenable_id: 1
          name: hoge
         token: 54c8de8c2b2346ece1c4d6257c0c23e40b02681c220eb96c04f2611a590a1bbd
     abilities: ["superman"]
  last_used_at: NULL
    created_at: 2021-10-21 03:38:42
    updated_at: 2021-10-21 03:38:42
1 row in set (0.00 sec)

ちゃんと追加されていましたね。

tokenの使用

続いてAPIを作成しましょう。

supermanアビリティを持つユーザでアクセスすると「You are Superman!!」を
持っていない場合は「You are NOT Superman.」を返します。

以下のコードをapi.phpに追記します。(たったこれだけ)

routes/api.php

Route::middleware('auth:sanctum')->get('/superman', function (Request $request) {
    return response()->json([
        'message' => $request->user()->tokenCan('superman')
            ? 'You are Superman!!'
            : 'You are NOT Superman.',
    ]);
})

そして実行...

# supermanアビリティを持つtokenでアクセス
curl -H "Authorization: Bearer VWWjM8k09fwIayViFaVIBCod0e4GrJIGqoOwCBPy" http://localhost/api/superman
{"message":"You are Superman!!"}

# supermanアビリティを持たないtokenでアクセス
curl -H "Authorization: Bearer uScF3qsBW2duh4UoCzyIkkpKwzlEHrDs0mtc0G7B" http://localhost/api/superman
{"message":"You are NOT Superman."}

# 正しくないtokenでアクセス
curl -H "Authorization: Bearer VWWjM8k09fwIayViFaVIBCod0e4GrJIGqillegal" http://localhost/api/superman
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://localhost/login'" />

        <title>Redirecting to http://localhost/login</title>
    </head>
    <body>
        Redirecting to <a href="http://localhost/login">http://localhost/login</a>.
    </body>
</html>

tokenが正しくない時にlogin画面へ行っちゃいましたが、tokenが正しい時はアビリティによって振る舞いを変えることができましたね。

最後に

と、まぁこんな感じで簡単にパーソナルアクセストークンを使うことができました。
パーソナルアークセストークンを使えばブラウザを介さずとも操作できるので、社内ツールの拡張機能として使うのもアリかもですね!

下の方が余ったのでコロナ禍で迎え入れたうちのうーちゃんを紹介します。
サバトラ白/♀)

f:id:aimstogeek:20211021154332j:plain
uh-chan
ほんと毎日癒されています。



\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //

919.jp