本記事はうるる Advent Calendar 2019 20日目の記事です。
はじめに
Webサービスを提供する際には認証・認可の機能は殆どの場合で必須になります。
しかし、自前でこの仕組みを実装しようとなるとけっこう大変です。加えて、セキュアに実装をしなければならなかったり、ソーシャルログインに対応したりなど、、、やることは山積みです。
そこで、今回はAuth0というサービスを使って、ソーシャルログインの実装とAPIに対する認証機能を実装したいと思います。
開発環境を構築する
前提
- API : NestJS
- ライブラリ
passport
@nestjs/passport
passport-jwt
jwks-rsa
- ライブラリ
- クライアント : Nuxt.js
- ライブラリ
@nuxtjs/auth
- ライブラリ
- 開発環境 : Docker Compose
今回使用する開発環境はこんな感じです。
フレームワークなどは完全に筆者の好みです。
また、フレームワークのインストールとライブラリのインストールは済ませている状態です。
version: '3'
services:
api:
build:
context: ./api
volumes:
- ./api:/api
ports:
- 3210:3000
app:
build:
context: ./app
volumes:
- ./app:/app
ports:
- 8888:3000
# ディレクトリ構成
.
├── api
│ ├── Dockerfile
│ ├── README.md
│ ├── dist
│ ├── nest-cli.json
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── test
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── tslint.json
├── app
│ ├── Dockerfile
│ ├── README.md
│ ├── assets
│ ├── components
│ ├── jest.config.js
│ ├── jsconfig.json
│ ├── layouts
│ ├── middleware
│ ├── node_modules
│ ├── nuxt.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ ├── plugins
│ ├── static
│ ├── store
│ └── test
└── docker-compose.yml
また、NestJSとNuxt.jsそれぞれのDockerfileはこんな感じです。
(Dockerfileを使ってビルドをしましたが、もしかしたらいらなかったかもしれません、、、。)
FROM node:12-alpine
WORKDIR /api
ADD package.json package-lock.json ./
RUN npm i
ADD . ./
CMD ["npm", "run", "start:dev"]
FROM node:12-alpine
WORKDIR /app
ADD package.json package-lock.json ./
RUN npm i
ADD . ./
CMD ["npm", "run", "dev"]
NestJSでAPIを実装する
APIの実装
ではまず認証機能を実装するAPIの実装をしていきたいと思います。
NestJSのデフォルト設定では、src
配下に*.ts
ファイルを配置して行くことでdistのディレクトリにそれを*.js
にコンパイルして配置してくれます。
早速、user
というディレクトリを作成して各種ファイルを配置していきましょう。
import { Controller, Get } from '@nestjs/common';
@Controller('user')
export class UserController {
constructor() {}
@Get()
list() {
return [
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Bravo' },
{ id: 3, name: 'Charlie' },
];
}
}
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
@Module({
imports: [],
controllers: [UserController],
providers: [],
})
export class UserModule {}
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './use/user.module';
@Module({
imports: [UserModule], // UserModuleをimport
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
ここでやっていることは簡単で、http://localhost:3210/api/user
にアクセスするとユーザーのリストが返ってくるAPIを作成しています。
NestJSは少なくとも1つのmoduleを持っている必要があり、moduleをもとにClass間の依存関係を解決します。初期状態ではapp.module.ts
のみが配置されており、これをルートモジュールと呼ぶそうです。
基本的にはリソース単位でモジュールを作成することになると思います。
今回はuser.module.ts
を作成してapp.module.ts
のimportsにUserModule
を記述します。
※ 今回はやりませんでしたが、 cliでserviceやcontrollerを作成すると、自動的にAppModuleのproviders
やcontrollers
に挿入してくれるようです。
Postmanなどでアクセスしてみると、下記のような結果を受け取ることができます。
ここで一旦、APIの実装は終わりです。
Auth0
Auth0とは
ユニバーサルログインやSSO、MFAなんかを比較的簡単に実装できる認証・認可のプラットフォームです。
IDaaSとも言うらしいです。類似サービスとして、Firebase Authentication や AWS Cognitoなどが挙げられます。
個人的に嬉しかったのはサンプルコードを公式が用意してくれていることです。
また、SDKやドキュメントも豊富にあって入門しやすい印象でした。
Auth0 Applicationsの作成
Auth0を使ってクライアント側からログインをしたり、jwtトークンの発行などをする場合は、Applicationsの作成が必要になります。
そのためにまず、ダッシュボードでサイドメニューのApplicationsを選択して、CREATE APPLICATION
というボタンを押します。typeは様々選べるのですが、Nuxt.jsはSPAモードを選択するので、Single Page Web Application
を選択してください。
そして、各種入力欄に下記のように入力して、ページ下部にあるSAVE CHANGE
ボタンを押してください。
Allowed Callback URLs : http://localhost:8888/callback
Allowed Web Origins : http://localhost:8888
Allowed Origins (CORS) : http://localhost:3210
加えて、Default Audienceの設定を行います。
入力するのは、Allowed Origins (CORS)
と同じ値です。
こちらは本来であれば設定をしなくてよいのですが、Nuxt.jsの仕様で設定をしないとうまく動きません。
https://manage.auth0.com/dashboard/us/dev-<自分のID>/tenant/general のページで、設定できますので忘れないように設定してください。
これで、Auth0での設定は完了しました。
APIに認証機能を実装する
Auth0の設定も完了しましたので、先程実装したAPIに認証機能を実装していきたいと思います。
src
にauth
ディレクトリを作成してAuthModule
とJwtStrategy
というクラスをそれぞれ実装してください。
import { Module } from '@nestjs/common';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
providers: [JwtStrategy],
exports: [],
})
export class AuthModule {}
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt, VerifiedCallback } from 'passport-jwt';
import { passportJwtSecret } from 'jwks-rsa';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor() {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://dev-<自分のID>.auth0.com/.well-known/jwks.json`,
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
issuer: 'https://dev-<自分のID>.auth0.com/',
audience: 'http://localhost:3210', // apiサーバーのドメイン
algorithms: ['RS256'],
});
}
async validate(payload: any, done: VerifiedCallback) {
if (!payload) {
done(new UnauthorizedException(), false);
}
return done(null, payload);
}
}
これらを実装したら、app.module.ts
を下記のように修正します。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './use/user.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [UserModule, AuthModule], // AuthModuleを追加
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
そして、user.controller.ts
を下記のように修正します。
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('users')
@UseGuards(AuthGuard('jwt')) // Guardsを追加
export class UserController {
constructor() {}
@Get()
list() {
return [
{ id: 1, name: 'Alpha', age: 12 },
{ id: 2, name: 'Bravo', age: 24 },
{ id: 3, name: 'Charlie', age: 36 },
];
}
}
@UseGuards(AuthGuard('jwt'))
を追加することで、このコントローラー内に作られるエンドポイントのAPIはすべて認証が必須となります。
(これはNestJSのGuardsという機能で、認証機能を実装する際によく使われるそうです。)
作成したAPIをPostmanなどで叩くと、401が返ってくることが確認できます。
この認証を通すためにはAPIコールの際にJWTトークンを渡してやる必要があります。このJWTトークンはクライアント側でソーシャルログインをしたときに発行され、axiosでAPIコールする際にNuxtのモジュールがヘッダーに自動で差し込んでくれます。
なので、サーバーサイドは受け取ったトークンをどう処理するかだけを実装すればいいのです。
Nuxt.jsでクライアント側を実装する
nuxt.config.js 設定
続いて、Nuxt.jsでログイン機能を実装していきたいと思います。
各種設定はnuxt.config.jsに記述していくことになります。今回はaxios
やauth
などの設定をしています。
domain
とclient_id
に関しては、Auth0のApplication詳細ページにあるDomain
とClient ID
を入力してください。
...
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth' // こいつが重要
],
axios: {
host: 'localhost',
port: 3210,
prefix: '/api'
},
// Auth0 setting
auth: {
strategies: {
auth0: {
domain: '****************', // Applicationsの詳細ベージで確認
client_id: '****************' // Applicationsの詳細ベージで確認
}
},
redirect: {
login: '/', // 未ログイン時のリダイレクト先
logout: '/', // ログアウト処理を実行した直後のリダイレクト先
callback: '/callback', // コールバックURL
home: '/users' // ログイン後に遷移するページ
}
},
// 認証済みでないと各種ページにアクセスできないようにする
// index.vueとcallback.vueのページは認証無しでアクセス可
router: {
middleware: ['auth']
},
...
各種ページを作成
最後に、各種ページを作成していきたいと思います。
まず、layout/default.vue
とpages/index.vue
を下記のように修正します。
<template>
<v-app>
<v-app-bar app>
<v-toolbar-title v-text="title" />
<v-spacer />
<v-btn v-if="!this.$auth.$state.loggedIn" color="primary" @click="login">
Login
</v-btn>
<v-btn v-else text @click="logout">Logout</v-btn>
</v-app-bar>
<v-content>
<v-container>
<nuxt />
</v-container>
</v-content>
</v-app>
</template>
<script>
export default {
data() {
return {
title: 'Auth0 Demo'
}
},
methods: {
login() {
// authモジュールを使用してのログイン
this.$auth.loginWith('auth0')
},
logout() {
this.$auth.logout()
}
}
}
</script>
<template>
<v-layout justify-center>
<v-card width="800px">
<v-card-title primary-title>
Let's Login !!!
</v-card-title>
</v-card>
</v-layout>
</template>
次に、pages/callback.vue
を作成します。
こちらのページは、Auth0からコールバックされるページの作成です。
許可コードを発行する際に一瞬だけ表示されるページですので、templateのみにしています。
<template>
<div>
<p>Please Wating</p>
</div>
</template>
最後に、ログイン後に遷移するユーザーの一覧ページです。
<template>
<v-layout justify-center>
<v-data-table :headers="headers" :items="users" item-key="id">
</v-data-table>
</v-layout>
</template>
<script>
export default {
data() {
return {
headers: [
{ text: 'id', value: 'id' },
{ text: 'name', value: 'name' },
{ text: 'age', value: 'age' }
],
users: []
}
},
created() {
// APIコールの際に自動でJWTトークンをヘッダーのAuthorizationにつけて送信してくれます。
this.users = await this.$axios.$get('users')
}
}
</script>
ここまで実装が完了すると、Nuxt.jsでの実装も一通り完了です!
とりあえず、うまく動くか確認してみましょう。
まずは、http://localhost:8888/
にアクセスしてみましょう。
そして、右上のログインボタンを押してみるとこんな見た目のページに遷移すると思います。
ここで、「Googleで続ける」を選択してみてください。
そうすると、現在のブラウザでログインしているGoogleのアカウント一覧が表示されたと思います。
その中から、ログインしたいアカウントを選択して、ログインが成功すると自分が作ったウェブサイトのhttp://localhost:8888/users
に遷移すると思います。
ログイン後に、遷移するページは、http://localhost:8888/users
となります。
このページでは、しっかりとAPIの認証も完了してデータを取ってくるとができています。
ちなみに、こんな感じでBearerトークンとして挿入してくれています。
ちゃんと認証も通っていることが確認できます。
ひとこと
Auth0を使うことで、こんなに簡単に認証が実装できてしまうのは驚きました。
また、煩わしいソーシャルログインの仕組みもAuth0がすべて肩代わりしてくれるので、開発体験が最高でした。
今回はGoogleのソーシャルログイン機能のみを試しましたが、もっと多くの機能がAuth0にはあるので、気になった方は是非試してみてください!!