Nuxtで認証認可をやる
jwtと認証認可の基礎知識を学んだので、実際に動かしてみる。 このブログはNext.jsで作っているが、今回はNuxtを使ってやってみる。
nuxt-community には、認証系のモジュールのテンプレートや、大体のアプリケーションに必須な機能がデフォルトで組み込まれているテンプレートが存在し、それらを使用することで基本機能の実装はスキップできる。
https://github.com/nuxt-community/auth-module https://github.com/nuxt-community/typescript-template
今回はnuxt/authモジュールは使わず、一方でtypescriptを使いたいので、スターターキットから下記のテンプレートを拝借します。
https://github.com/nuxt-community/typescript-template
いつもどおりyarnして起動準備。
とりあえず名前は nuxt-auth-spa-sample
としておきました。長い
yarn dev
すると起動します。
事前準備
Auth0を組み込む前に準備をします。
ポート番号を変更する
まずはポート番号を変えておきましょう。バックエンドにRailsを使う予定なので、そのままだとポート番号の3000が被ってしまいます。 番号は他とかぶらなければ何でも良いので、今回は5000とかにしました。
デフォルトのポート番号を変更するために、pacage.jsonのdevの部分にオプションを付け足します。
"dev": "HOST=0.0.0.0 PORT=5000 nuxt"
変更したら、指定したポートで開いて確認します。
SPA設定する
nuxtでspa設定するのは簡単で、nuxt.config.tsファイルにssrオプションを設定するだけで完了する。
ssr: false
modeというオプションもあるが、こちらは現時点で非推奨になっている。
デフォルトでもnuxtはnextと違ってnuxt.configファイルを変更しても再起動を自動でしてくれるみたい。
これで一旦フロントの最低限の設定はできたので、Auth0を組み込んでいく。
Auth0を使う準備をする
nuxtには専用のauthモジュールがあるので、実際にアプリケーションを作成する事となった場合、それらを採用するのが良さそう。
今回はフレームワーク専用のモジュールを使わずに実装してみたいので、Auth0を適用できる通常のライブラリを使用する。 サラッと調べたところ、auth0をjsで使用するには大きく2つ選択肢があるっぽい。
- auth0-js
- auth0-lock
auth0-jsはカスタマイズ性の高いライブラリっぽい。auth0-lockは最初からフォームを組み込んでくれているラッパーといった感じ。
フォームをカスタマイズしていくのは必要になってくる事が多いが、今回は本題とそれるのでlockを使って組み込みのフォームを利用させてもらおう。
yarn add auth0-lock @types/auth0-lock
その後、plugins以下にauth0.tsというファイルを作成し、下記のように記載。
import Auth0Lock from 'auth0-lock'
import nuxtConfig from '~/nuxt.config'
const config = nuxtConfig.auth0
class Auth0Util {
showLock(container: any) {
const lock = new Auth0Lock(config.client_id, config.domain, {
container,
closable: false,
auth: {
responseType: 'token id_token',
redirectUrl: this.getBaseUrl() + '/callback',
params: {
scope: 'openid profile email',
},
},
})
lock.show()
}
getBaseUrl() {
return `${window.location.protocol}//${window.location.host}`
}
}
export default (context: any, inject: (arg0: string, arg1: Auth0Util) => void) => {
inject('auth0', new Auth0Util())
}
client_idはキャメルケースにすると怒られたのでスネークケースにしてる。
環境変数で渡す
Auth0のClientIDなどをconfigに書いていきたいが、そのまま直打ちすると流出してしまう恐れがあるので環境変数経由で見るようにしたい。
https://ja.nuxtjs.org/docs/2.x/configuration-glossary/configuration-env/
NUXT_ENV
で始まる環境変数を定義すれば、ビルドフェーズで値が自動的に注入されるとのこと。
その他に、dotenvモジュールを入れる方法もあり、今回はこっちを使っていく。
yarn add -D @nuxtjs/dotenv
インストールしたら、nuxt.config.tsのモジュールに追加して使えるようにします。
modules: [
'@nuxtjs/dotenv'
],
そして、auth0の情報もnuxt.config.tsに追加します。
auth0: {
domain: process.env.AUTH0_DOMAIN,
client_id: process.env.AUHT0_CLIENTID
}
全体像はこんな感じになった
import type { NuxtConfig } from '@nuxt/types'
const config: NuxtConfig = {
build: {},
buildModules: [
'@nuxtjs/composition-api',
'@nuxt/typescript-build'
],
ssr: false,
css: [],
env: {},
head: {
title: 'nuxt-community/typescript-template',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'A boilerplate to start a Nuxt+TS project quickly' }
],
link: []
},
loading: { color: '#0c64c1' },
modules: [ '@nuxtjs/dotenv'],
plugins: [
'~/plugins/auth0',
'~/plugins/truncate'
],
auth0: {
domain: process.env.AUTH0_DOMAIN,
client_id: process.env.AUHT0_CLIENTID
},
}
export default config
Loginページを作る
基本機能はできたので、loginページを作る。
pages/login.vue
というファイルを作成し、ログイン用のAuth0のフォームが出てくるようにする。
<template>
<div id="show-auth"></div>
</template>
<script>
export default {
setup() {},
mounted() {
this.$auth0.showLock('show-auth')
},
}
</script>
composition-api?知りませんねぇ。
上記のようにフォームが出てこればおk。
ここでloginしてみてもいいが、エラーになる。
nuxt側でcallbackに対応するページを作成することと、Auth0側で「Allowed Callback URLs」「Allowed Web Options」の2つを許可してあげる必要がある。
開発環境なので、localhostと開けてるポート番号を登録する。今回はnuxt用の5000と、Railsで使用する3000を開けた。
<template>
<p>callback</p>
</template>
<script>
export default {}
</script>
<style></style>
これらの設定をすべて整えた上でGoogle ログインを選択してみる。
googleでログインを済ませると、callback.vueファイルにリダイレクトする。
リダイレクトしたら、URL欄を見てみる。localhost:xxxx/callbackの後ろに膨大な文字列がくっついてくる。
これはauth0で取得したトークン群である。 access_tokenやid_token,expire_atなど色々ついてきているが、ここではid_tokenを見ていきたい。
id_tokenはjwtである。ここでは、auth0.tsのスコープに指定した情報が入っている。
jwt.ioでトークンを複合してみる。 urlからid_token以下の文字列を抜き取り、jwt.ioに貼り付け。
みたいに自分のログイン情報が表示されていればオッケー。
トークンの保存
現状だとただURLに返ってきただけなので、それを使って色々できていない状態。とりあえずSPAで必要な情報をLocalStrageに保存したい。
yarn add jwt-decode query-string @types/jwt-decode @types/query-string
jwtのデコードを行えるライブラリと、クエリパラメータをいい感じに取得できるライブラリを追加。
追加できたらauthutilに保存処理用のメソッドを書いていく。
import Auth0Lock from 'auth0-lock'
import nuxtConfig from '~/nuxt.config'
import jwtDecode from 'jwt-decode'
import queryString from 'query-string'
const config = nuxtConfig.auth0
class Auth0Util {
getQueryParams() {
return queryString.parse(location.hash)
}
setTokenToLocalStorage({ access_token, id_token, expires_in }: any) {
const localStorage = window.localStorage
localStorage.setItem('accessToken', access_token)
localStorage.setItem('idToken', id_token)
localStorage.setItem(
'expiresAt',
(expires_in * 1000 + new Date().getTime()).toString()
)
localStorage.setItem('user', JSON.stringify(jwtDecode(id_token)))
}
setTokenByQuery() {
this.setTokenToLocalStorage(this.getQueryParams())
}
showLock(container: any) {
const lock = new Auth0Lock(config.client_id, config.domain, {
container,
closable: false,
auth: {
responseType: 'token id_token',
redirectUrl: this.getBaseUrl() + '/callback',
params: {
scope: 'openid profile email',
},
},
})
lock.show()
}
getBaseUrl() {
return `${window.location.protocol}//${window.location.host}`
}
}
export default (
context: any,
inject: (arg0: string, arg1: Auth0Util) => void
) => {
inject('auth0', new Auth0Util())
}
localStrageにurlをparseして各情報を入れていく。
定義し終わったら、urlに情報が入ってくるcallback.vueに呼び出す処理を追加する。
<template>
<p>callback</p>
</template>
<script>
export default {
mounted() {
this.$auth0.setTokenByQuery()
},
}
</script>
<style></style>
このようにすることによって、callbackがマウントされたときにlocalStrageに値を入れることができる。
確認できたら、ログイン後にはトップページにリダイレクトするようにしておこう。
this.$router.replace('/')
nuxtの場合は、上記の一行でルートを変更できる。
ログイン状態を判定する
トップページに遷移した後、今ログインしているのかどうかを知りたい。 Auth0のデフォルトだと、アクセストークンの有効期限は2時間、jwtの有効期限は10時間となる。
今回はアクセストークンが有効期限なのかどうかを判定するようにしてみる。
isAuthenticated() {
const expiresAt = window.localStorage.getItem('expiresAt')
if (!expiresAt) {
return false
}
return new Date().getTime() < Number(expiresAt)
}
expiresAtはログインしていない状態だとnullになるので、その場合はfalseを返すようにしておきます。
isAuthenticated(): boolean {
const expiresAt = window.localStorage.getItem('expiresAt')
if (!expiresAt) {
return false
}
return new Date().getTime() < Number(expiresAt)
}
boolean型を返すので、v-ifで分岐して使えるようになった。この分岐を使用して、loginしているときだけユーザー情報を表示したいと思う。 表示するのはnicknameをとりあえず表示し、その後必要な情報があれば追加で出すことのできる状態にする。
getUserData() {
const userString = window.localStorage.getItem('user')
return userString ? JSON.parse(userString) : null
}
index.vueに分岐と、定義した2つのメソッドを使用できるようにする。
<template>
<main>
<div v-if="!loggedIn()">
<div>ログインしてください。</div>
<nuxt-link class="button is-primary" to="/login">
<span>login</span>
</nuxt-link>
</div>
<div v-else>
<div>{{ userInfo().nickname }}</div>
</div>
</main>
</template>
<script lang="ts">
export default {
methods: {
loggedIn() {
return this.$auth0.isAuthenticated()
},
userInfo() {
return this.$auth0.getUserData()
}
}
}
</script>
ログアウトできるようにする
ログアウトもできるようにしておく。ログアウトはローカルストレージに保存されたデータをクリアすることで、ログアウト扱いになる。(isAuthenticated()メソッドがfalseになる)
logOut() {
const localStorage = window.localStorage
localStorage.removeItem('accessToken')
localStorage.removeItem('idToken')
localStorage.removeItem('expiresAt')
localStorage.removeItem('user')
}
clearすると既存のlocalstrageのデータが全部消えてしまい、他のアプリにも影響が出たりするかもしれないので、一個づつ消した。
ログアウトボタンは、ログイン済みの場合に表示し、ボタンを押したらログアウト処理+topページにリダイレクトするようにする。
index.vueに追加。
<div v-if="!loggedIn()">
<div>ログインしてください。</div>
<nuxt-link class="button is-primary" to="/login">
<span>login</span>
</nuxt-link>
</div>
<div v-else>
<div>{{ userInfo().nickname }}</div>
<button @click="logout()">
ログアウトする
</button>
</div>
...
<script>
....
logout() {
this.$auth0.logOut()
this.$router.replace('/')
}
....
</script>
アクセストークンを使用して、ユーザー情報を取得する
現状、localStrageのUserキーでユーザー情報を格納しているので、そこから情報を取得することはできたが、本来は取得したaccess_tokenを使用して、リソース元から情報を取得するのが普通である。
APIへのアクセスには、AuthorizationヘッダーのBearerスキームというものを使用する。 保護された情報にアクセスしたい場合、ヘッダーにアクセストークンを付与する。
簡単に確認したい場合はcurlかPostmanのようなアプリを使用するのが良い。
curlの場合は、
curl -H "Authorization: Bearer アクセストークン" https://<Auth0のテナント名>.auth0.com/userinfo
みたいな感じで試すとよい。
フロントの認証のベースはできたので、次回はバックエンド側を実装してみる。