NuxtとLaravel Sanctumで認証し、ログインページへの直アクセスにも対策する
..
前提条件
- SSRを想定しています。
- Nuxt は2系です。
- JWTではなくセッションIDをCookieに保存するタイプの認証を行います。
- Laravel Sanctum と @nuxtjs/auth-next を使用する方法です。
ローカルのDocker環境で、Laravel SanctumとNuxtのAuthモジュールを使って認証を行うまでの手順をまとめます。
実際の設定内容
NODE_ENV='development'
AUTH_SCHEME='local'
AXIOS_HTTPS=false
NODE_URL=http://0.0.0.0:4001
SERVER_ORIGIN=http://<Nginxコンテナ名>:80
export default {
/*
* 今回の記事に関係してる部分だけ
*/
ssr: true,
axios: {
prefix: '/api/',
https: process.env.AXIOS_HTTPS,
proxy: true,
credentials: true,
},
proxy: {
'/laravel': {
target: process.env.NODE_URL,
pathRewrite: { '^/laravel': '/' }
},
'/api/login': {
target: process.env.SERVER_ORIGIN,
pathRewrite: {'^/api/': '/'}
},
'/api/': {
target: process.env.SERVER_ORIGIN,
},
},
publicRuntimeConfig: {
AUTH_SCHEME: process.env.AUTH_SCHEME,
NODE_ENV: process.env.NODE_ENV
},
auth: {
redirect: {
home: '/',
login: '/login',
logout: '/',
callback: false,
},
strategies: {
local: {
token: {
required: false,
type: false
},
endpoints: {
login: { url: `/login`, method: 'post' },
logout: false,
user: false
}
},
laravelSanctum: {
provider: 'laravel/sanctum',
url: process.env.SERVER_ORIGIN,
cookie: {
name: 'XSRF-TOKEN',
},
endpoints: {
csrf: {
url: '/sanctum/csrf-cookie'
},
login: { url: '/login', method: 'post' },
logout: false,
}
}
},
},
router: {
middleware: ['auth'],
},
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth-next',
],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
}
}
export const actions = {
async nuxtServerInit({state}, { app }) {
await app.$axios
.$get('/user')
.then((authUser) => {
app.$auth.setUser(authUser)
})
.catch((err) => {
app.$auth.setUser(null)
})
}
}
認証方法の違いについて
JWTトークン認証は、クライアントごとにユニークなトークン文字列を持っておくことでそれを鍵にしようという方法です。REST原則においてはサーバーとクライアントは状態を持ちませんので、サーバーも認証においてセッション情報を保持したりしません。
しかしながらセッション認証と比較した場合にJWTトークンはあまりセキュアではないという懸念があることで、認証状態についてはステートフルじゃなくていいんでねえの、という雰囲気になってもいるようです。Laravel Sanctumはどちらの認証方法もサポートしているものの、公式にはセッション認証を推奨しています。
Authモジュールについて
既存の記事では、
- axiosでPOSTしてログイン
- nuxtServerInitメソッド内でユーザー情報を取得(認証ガード付きAPIへGET)してstoreへ格納
- middlewareでstoreを参照
- storeにユーザー情報があれば認証済み、なければ未認証としてログインページへリダイレクト
という流れを自前実装しているものも多いです。
しかし、これらの大部分はNuxtが公式にサポートしているAuthモジュール側で実装してくれているようだったので、とりあえずそれをインストールしてあとは細かい部分を自分で入れればどうにかなりました。
Laravel側の設定
CORSを許可しないといけません。
例えば筆者の環境では、Laravelコンテナはhttp://localhost:9000
で動作し、リクエスト自体はNginxが動いているコンテナhttp://localhost:8080
が処理しています。リクエストを送るクライアントであるNuxtコンテナはhttp://localhost:4001
で動作しているため、この場合はlocaihost:4001
がlocalhost:9000
の作成・返却したレスポンスを参照・使用することを許可する必要があります。
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => [
'api/*',
'login',
'logout',
'sanctum/csrf-cookie',
],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONT_ORIGIN')],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
.env
内にFRONT_ORIGIN=http://0.0.0.0:4001
を追加し、その環境変数をconfig/cors.php
で呼び出します。
Sanctumの設定
普通に公式ページや他の記事でも書いてあることをするだけなので、読み飛ばしてもいいところです。
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
SESSION_DRIVER=cookie
SESSION_DOMAIN=0.0.0.0
SANCTUM_STATEFUL_DOMAINS=0.0.0.0:4001
'paths' => [
'api/*',
'login',
'logout',
'sanctum/csrf-cookie',
],
//略
'supports_credentials' => true,
ログイン処理を追加
ここは各自いろいろ書き方のある部分かと思います。(メールアドレスの代わりにユーザーIDを使いたいとか)
とりあえずやるべきことは、ログインの場合はattempt
メソッドで入力値をデータベースと照合することとセッションを生成すること、ログアウトの場合はセッションを破棄することです。
<?php
// 略
// とりあえず動作させるためhttps://qiita.com/ucan-lab/items/3e7045e49658763a9566からコピペしたソースです。
// 実際はAuthファサードは直に呼ばず、リポジトリクラスを追加してそこにAuthを使いたいメソッドを切り出したりします。
// バリデーションもRequestクラスを別途作成して行うなどする予定です。
class LoginController extends Controller
{
/**
* @param AuthManager $auth
*/
public function __construct(
private AuthManager $auth,
) {
}
/**
* @Method POST
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
if (auth()->attempt($credentials)) {
$request->session()->regenerate();
return response()->json(Auth::user());
}
return response()->json(['message' => 'ユーザーが見つかりません。'], 422);
}
/**
* Undocumented function
*
* @param Request $request
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse
{
if ($this->auth->guard()->guest()) {
return new JsonResponse([
'message' => 'Already Unauthenticated.',
]);
}
$this->auth->guard()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return new JsonResponse([
'message' => 'Unauthenticated.',
]);
}
}
ログインルートは、基本的にはweb.php
内に書くようです。はっきりとした理由はドキュメントを読んでもよくわかりませんでしたけど、
通常、SanctumはLaravelの「web」認証ガードを利用してこれを実現します。
とあるので、webルート用に実装されてるミドルウェアを呼ぶためなのかな?とは思います。
実際Sanctumが何をするかというと、SANCTUM_STATEFULL_DOMAIN
に書かれたドメインに限ってセッションをステートフルにすること、CSRFトークンを使ってCORSを許可しているサイトかどうかを識別すること、セッションに認証情報がなければモバイルトークンの有無を見るなどで、ルートに対するガードを行う機能はありません。
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LoginController::class, 'logout']);
Nuxt側の設定
ふたつのモジュールをインストールする必要があります。
axios
modules: [
'@nuxtjs/axios',
],
インストールしたこれらをmodules
にて呼び出します。
これによりNuxtアプリケーション自体に彼らが組み込まれるため、インポートせずともコンポーネント内であればthis.$axios
とか、nuxtServerInit({ commit }, { app })
の中であればapp.$auth
とかの記述で使えるようになります。
axios: {
prefix: '/api/',
proxy: true,
credentials: true,
},
proxy: {
'/laravel': {
target: process.env.NODE_URL,
pathRewrite: { '^/laravel': '/' }
},
'/api/login': {
target: process.env.SERVER_ORIGIN,
pathRewrite: {'^/api/': '/'}
},
'/api/': {
target: process.env.SERVER_ORIGIN,
},
},
axiosの設定はこんな感じにしてます。SERVER_ORIGIN
はnginxコンテナの、NODE_URL
はNuxtコンテナのオリジンを格納している環境変数です。
設定値の解説(axiosのドキュメントをかいつまんで引用)はこんな感じ。
-
prefix
…proxy
をtrue
にする場合のbaseURL
の代わり。後述するRuntimeConfig
が読み込まれていない場合のデフォルト値。 -
credentials
…true
にする。異なるドメイン間でCookie等の資格情報をやりとりするために必要。
他には下にある配列でプロキシ設定を行うわけですが、これは、ブラウザからXMLHttpRequestによって送信されたリクエストをNuxt(Node.js)側で受けたあと、外部サーバーへそのリクエストを転送する際にリクエスト先を指定するための設定だと理解しています。
たとえば上記の例では$axios.<なんらかのHTTPメソッド>('/api')
は、この設定により$axios.<なんらかのHTTPメソッド>(http://0.0.0.0:8080/api)
へのリクエストに書き換わります。
proxy: {
'/laravel': {
target: process.env.NODE_URL,
pathRewrite: { '^/laravel': '/' }
},
'/api/login': {
target: process.env.SERVER_ORIGIN,
pathRewrite: {'^/api/': '/'}
},
'/api/': {
target: process.env.SERVER_ORIGIN,
},
},
▲この部分、なぜ/api/
と/api/login/
の両方にプロキシを設定しているかというと、Laravel側ではAPIリクエストは/api〜
で受けますが、認証系については/login
で受けるようにしているからです。こういう処理になります。
-
/login
へリクエストする -
prefix: '/api/'
を設定しているので/api/login
へのリクエストに書き換わる -
pathRewrite: {'^/api/': '/'}
がprefix
を消すので再度/login
へ戻る
どんな値を指定してもなかなか Nuxt から Laravel へリクエストを届けられなかったのですが、なにかの記事で「オリジンはコンテナ名で指定する必要がある」ということを知り、SERVER_ORIGIN
にはhttp://<Nginxコンテナの名前>:80
を入力。http://<Nginxコンテナの名前>:80/api/<ルート名>
へリクエストするようにしたらうまくいきました。
コンテナ名はdocker compose ps
を実行すると確認できます。
auth
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth-next', // 同様に追加
],
authモジュールがやってくれるのは、例えばこんなことです。
- ログイン中かどうかを真偽値で返してくれる(
$auth.loggedIn
) - ログインしたユーザーをストア(
auth.js
がエディタからは見えないけどある)へ格納してくれる - 認証方法のパターンをいくつか指定できる(Laravel Sanctum とか Google とか)
axios単体で認証機能を実装しようとしたら色々と自前で実装する必要が出てきますが、authモジュールはそのへんの需要を諸々満たしてくれてます。
auth: {
redirect: {
login: '/login',
logout: '/',
callback: '/',
home: '/'
},
strategies: {
laravelSanctum: {
provider: 'laravel/sanctum',
url: process.env.SERVER_ORIGIN,
cookie: {
name: 'XSRF-TOKEN',
},
endpoints: {
csrf: {
url: '/sanctum/csrf-cookie'
},
login: { url: '/login', method: 'post' },
logout: { url: '/logout', method: 'post' },
}
}
}
},
authモジュールは内部的にaxiosモジュールを使用してリクエストを飛ばすため先程の設定が必要でした。
それに加え、auth: {}
の中にauthモジュール側の設定を色々書いていきます。
redirect
の各設定値はこんな感じです。
- login (ログインが必要な場合のリダイレクト先URL)
- logout (ログアウトしたあとのリダイレクト先URL)
- callback (ログイン終了後、GoogleなどのIDプロバイダがリダイレクトするURL)
- home (ログイン終了後、Nuxt内でリダイレクトされるURL)
ログインを求めたいページへ未認証でアクセスされた場合にログインページへリダイレクトする、ログインできたらトップページへリダイレクトする…といった処理は、全てこのオブジェクトを配置すれば実現できます。
strategies
は、認証方法それぞれで個別に使用する設定をごにょごにょ書く場所です。今回はLaravel Sanctumを使ってセッションに状態をもたせる形の認証を行うため、/sanctum/csrf-cookie
へGETリクエストを送ってXSRFトークンをもらう設定とか、/login
や/logout
へPOSTする際にCookie内のXSRFトークンを参照する設定とかを書きます。
あとはこんなふうにログインページのコンポーネントを作れば…
<template>
<div>
<v-form>
<v-text-field
label="ログインID" v-model="loginId"
/>
<v-text-field
label="パスワード"
v-model="password"
@click:append="showPassword = !showPassword"
v-bind:type="showPassword ? 'text' : 'password'"
append-icon="mdi-eye-off"
/>
</v-form>
<div>
<v-btn @click="login" depressed>ログイン</v-btn>
</div>
</div>
</template>
<script>
export default {
data: () => ({
showPassword: false,
loginId: '',
password: '',
}),
methods: {
async login() {
await this.$auth.loginWith(this.$config.AUTH_SCHEME, {
data: {
loginId: this.loginId,
password: this.password
}
})
.then((res) => {
})
.catch(err => {
});
}
}
}
</script>
ログイン機能は出来上がりです。
ログアウトも、Laravel側でログアウトの処理を書いていれば、あとはこんなふうに作れます。
<template>
<button @click="logout">
ログアウト
</button>
</template>
<script>
export default {
methods: {
async logout() {
await this.$auth.logout();
}
}
}
</script>
nuxtServerInitでフェッチを行う
export const actions = {
async nuxtServerInit({ commit }, { app }) {
await app.$axios
.$get('/api/user')
.then((authUser) => {
app.$auth.setUser(authUser)
})
.catch((err) => {
app.$auth.setUser(null)
})
}
}
セッションが継続している(ログイン中である)場合は、authuser
にユーザーの情報が入ってくるので、この時点でstoreに格納してしまいます。もしセッションが切れている場合は401 Unauthorized
が返ってくるので、その例外をキャッチしてstoreの中身をリセットします。
以上の実装を行うことで、ログインした後はログアウトするまでログインページへアクセスできなくなります。
一般的な感覚では、ログイン中にログインページへのリンクは表示されないようv-if
等で出し分けるとは思いますが、仮に NuxtLink が残ってしまっていても、あるいはアドレスバーから直にURLを叩かれても、有効なセッションがあればホームへリダイレクトされます。
ログアウトについて
auth: {
redirect: {
login: '/login',
logout: '/',
callback: '/',
home: '/'
},
strategies: {
laravelSanctum: {
provider: 'laravel/sanctum',
url: process.env.SERVER_ORIGIN,
cookie: {
name: 'XSRF-TOKEN',
},
endpoints: {
csrf: {
url: '/sanctum/csrf-cookie'
},
login: { url: '/login', method: 'post' },
logout: false,
}
}
}
},
今回はサーバーサイド側にもログアウトAPIを実装しましたが、設定でlogout: false
を記述すると、APIへのリクエストはなしにCookieやJWTトークンの削除を行うようです。それだけで有効な認証状態はなくなるため、もしログアウト時にサーバーサイドでやりたいことがなければ、エンドポイントそのものが不要であると考えてもよさそうです。
参考記事リンク
-
nuxt/axios
https://axios.nuxtjs.org/ -
nuxt/auth
https://auth.nuxtjs.org/ -
NuxtJS
https://nuxtjs.org/docs/directory-structure/nuxt-config/
https://nuxtjs.org/docs/configuration-glossary/configuration-runtime-config/ -
Nuxtのライフサイクル
https://qiita.com/yosuke_takeuchi/items/059b2d33fa9f46e7eb07 -
Readouble.com
https://readouble.com/laravel/8.x/ja/sanctum.html
https://readouble.com/laravel/8.x/ja/authentication.html -
Laravel Sanctumとログイン処理
https://qiita.com/ucan-lab/items/3e7045e49658763a9566 -
Sanctumの仕様について
https://qiita.com/pikanji/items/040fa4ab6976059f3762?msclkid=77c41d34cf2e11ec92863298c5701f6d
https://dev.to/nicolus/laravel-sanctum-explained-spa-authentication-45g1 -
nuxt/authによる実装方法やCORS許可の設定など
https://zenn.dev/nagi125/articles/78d931f9b72fff1f1a7d
https://zenn.dev/nagi125/articles/7d01336868c79654b4af -
Cookieとセッション
https://qiita.com/hththt/items/07136ad74127999df271?msclkid=53f6c5a7cf2e11ecbe87e6b0fced88bd -
CORS
https://dev.classmethod.jp/articles/about-cors/?msclkid=07725a5bcf2b11eca746f5f13c481a8c
https://dev.classmethod.jp/articles/cors-cross-origin-resource-sharing-cross-domain/
https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy?msclkid=98ea0ae3cf2b11eca06e47fb7bf1c7a0 -
nuxtServerInitでstoreに値をぶちこむやり方
https://qiita.com/h23k/items/f70a6d208fc4e720e11c
https://qiita.com/kiyoshi999/items/9ef7c89e3eaff444286d