LaravelのEloquentでGroupByの使い方について、まとめたいと思います。 まずはデータアクセス方法を整理 Laravelはデータソースに対するデータアクセス方法として、Eloquent…
Laravel + Vue.jsの構成にOAuth2.0 PKCE認証を追加する方法です。
Vue.jsは完全に独立したSPAとして作り、Laravel(バックエンド)に対してはAPIで通信するだけの関係の例となります。
OAuth2.0 PKCE認証とは
Yahooの中の人が書いて下さっている記事が一番分かりやすいので紹介します。
OAuth2.0拡張仕様のPKCE実装紹介 〜 Yahoo! ID連携に導入しました
https://techblog.yahoo.co.jp/entry/20191219790463/
Laravel Passportをインストールする
LaravelでOAuth認証を実装するには、公式からも推奨されているLaravel Passportパッケージをインストールします。
参考:https://readouble.com/laravel/8.x/ja/passport.html
composer require laravel/passport
マイグレーションを実行する
Passportインストール後にマイグレーションを実行し、関連するテーブルを追加します。
マイグレーションファイルはvendorディレクトリ下のPassportの実体がある場所に入っています。
php artisan migrate
暗号化キーを生成する
アクセストークンを生成するために暗号化キーが必要になります。
サーバーセットアップ時に1回だけ以下のコマンドからキーを生成します。
作成されたキーはデフォルトで /storage/
ディレクトリの中に保存されます。
php artisan passport:keys
ルーティングを追加する
Passportで必要なルーティングを追加します。
App\Providers\AuthServiceProvider
<?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { /** * アプリケーションのポリシーのマップ * * @var array */ protected $policies = [ 'App\Models\Model' => 'App\Policies\ModelPolicy', ]; /** * 全認証/認可サービスの登録 * * @return void */ public function boot() { $this->registerPolicies(); if (! $this->app->routesAreCached()) { Passport::routes(); } } }
クライアントを作成する
これまでの手順は標準的なLaravel Passportのセットアップ手順です。
ここからはOAuth2.0 PKCE認証のセットアップ手順になります。
https://readouble.com/laravel/8.x/ja/passport.html
認証対象のクライアントを作成します。
これは利用ユーザー単位ではなく、SPA、モバイルアプリ、外部連携先、などのバックエンドから見たクライアントの種別単位になります。
php artisan passport:client --public
認可サーバー(Laravel)が認可を終えてトークンレスポンスの送信先(リダイレクトURL)の入力を求められます。
今回の例では
https://localhost/frontend/authenticate/auth
となります。
フロントエンドを実装する
Vue.jsでフロントエンドを実装していきます。
js-cookieライブラリをインストールする
取得したトークンをクッキーに保存するため、クッキーに読み書きするライブラリを利用します。
JavaScriptで自分でクッキーに書き込んでも良いですが、レガシーなインターフェースで有効期限等のオプションを書き込もうと思うと面倒なので使ったほうが楽です。
npm i js-cookie
js-pkceライブラリをインストールする
PKCE認証はクライアント側で暗号化・復号化のために code_verifier
や code_challenge
を生成する必要があります。
自分で生成するコードを書いても良いですが、誰が書いても同じでライブラリが存在しているので利用します。
自分で書くときはランダム性を持たせてキー生成する必要があるので注意が必要です。
js-pkce – npm
https://www.npmjs.com/package/js-pkce
npm i js-pkce
routeを定義する
認証に必要なcode_veriferを生成する(preparation)と、
認証後に認可サーバーからトークンがリダイレクトで叩かれてくるURLを定義します。
<template> import Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter); const routes = [ { path: '/authenticate/preparation', name: 'Preparation', meta: { title: '' }, // 認証用code_verifer生成・転送 component: () => import('../views/Authenticate/Preparation.vue'), }, { path: '/authenticate/auth', name: 'Auth', meta: { title: '認証' }, component: () => import('../views/Authenticate/Auth.vue'), }, ]; const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, }); export default router;
認可リクエストページを作成する
code_challengeを生成して認可リクエストを送信する画面を作成します。
Webページのコンテンツは何もなく、マウントが完了したらcode_challengeを生成して認可サーバーへリクエストを送信します。
frontend/src/views/Authenticate/Preparation.vue
<template> <div /> </template> <script> import PKCE from 'js-pkce'; export default { name: 'Preparation', mounted() { const pkce = new PKCE({ client_id: '1', // oauth_clientsテーブルに追加されているクライアントのID redirect_uri: 'https://localhost/frontend/authenticate/auth', // oauth_clientsテーブルに追加されているredirect authorization_endpoint: 'https://localhost/backend/oauth/authorize', // Laravel Passportが定義する認証エンドポイントのルート requested_scopes: '*', }); // 認証URLへ遷移する location.replace(pkce.authorizeUrl()); }, }; </script> <style></style>
Laravel Passportで具体的にどんなルートが追加されているかは、 ルートリストを表示するArtisanコマンドで確認できます。
php artisan route:list
認証ページを作成する
ID・パスワードで認証する画面です。
認証画面はバックエンド側で用意するため、Laravelのbladeで作成します。
自社SPAのためのバックエンドの場合でも、GoogleやTwitterで外部認証する場合に認証画面は認証先が提供するように認可サーバーが用意します。
(違ってたら教えて下さい)
backend/resources/views/login.blade.php
<form method="post" action="login"> @csrf <label for="email">Email:</label> <input type="text" name="email"> <label for="password">Password:</label> <input type="password" name="password"> <button>Login</button> </form>
トークンリクエストを受け取るページを作成する
認証後に認可サーバーからトークンがリダイレクトされるページを作成します。
src/views/Authenticate/Auth.vue
<template> <div> <h1>認証中...</h1> </div> </template> <script> import PKCE from 'js-pkce'; import Cookies from 'js-cookie'; export default { mounted() { const pkce = new PKCE({ client_id: '1', redirect_uri: 'https://localhost/frontend/authenticate/auth', token_endpoint: 'oauth/token', }); pkce.exchangeForAccessToken(document.location.href).then(response => { // アクセストークンをセット const expiresMinutes = 30; Cookies.set('ACCESS_TOKEN_KEY', response.access_token, { expires: new Date(new Date().getTime() + expiresMinutes * 60 * 1000), sameSite: 'lax', }); // リフレッシュトークンをセット const expiresDays = 5; Cookies.set('REFRESH_TOKEN_KEY', response.refresh_token, { expires: expiresDays, sameSite: 'lax', }); // 認証後に遷移するページへ this.$router.replace({ name: 'Dashboard' }); }); }, }; </script> <style></style>
axiosのリクエストヘッダーに追加する
axiosでHTTPリクエストする時にヘッダーにトークンをセットします。
axiosを直接呼び出さず、HttpClientクラスとしてラップして通信することでトークンをセット処理を共通化しています。
Vueのプラグイン機能でaxiosを管理し、シングルトンとして扱い、初回にトークンをセットしてもOKです。
import router from '../router'; class HttpClient { constructor() { const config = { baseURL: process.env.VUE_APP_API_URL, }; this.client = axios.create(config); this.client.interceptors.request.use(this.setAccessToken); } /** * アクセストークンをリクエストヘッダーにセット * @param {object} config * @returns {object} */ setAccessToken = async config => { const accessToken = Cookies.get(ACCESS_TOKEN_KEY); config.headers.Authorization = `Bearer ${accessToken}`; return config; }; /** * GETリクエスト * @param {string} url * @param {object} config * @returns {Promise} */ get = async (url, config = {}) => { return await this.client.get(url, config); }; /** * POSTリクエスト * @param {string} url * @param {any} data * @param {object} config * @returns {Promise} */ post = async (url, data = undefined, config = {}) => { return await this.client.post(url, data, config); }; /** * PUTリクエスト * @param {string} url * @param {any} data * @param {object} config * @returns {Promise} */ put = async (url, data = undefined, config = {}) => { return await this.client.put(url, data, config); }; /** * DELETEリクエスト * @param {string} url * @param {object} config * @returns {Promise} */ delete = async (url, config = {}) => { return await this.client.delete(url, config); }; } const httpClientAuth = new HttpClientAuth(); export { httpClientAuth, HttpClientAuth };
コメントを書く