AdonisJSに認証機能を追加する
AdonisJSに認証機能を追加する
2022/11/01

こちらは、現時点で最新のAdonisJS v5の記事になります。

前回データベースに接続できたので、次は認証機能を追加してユーザーをDBに保存できるようにする。
v4ではJWT認証で実装していたが、v5では代わってOpaqueAccessTokenを使っての認証になるので、実装方法を変える必要があったので、修正箇所も説明します。

公式リファレンス
https://docs.adonisjs.com/guides/auth/introduction

AdonisJS v5へのアップグレードで他にやったことはこちらにまとめています。

Authパッケージと設定の追加

設定追加時に認証方式をsessions, basic auth, API tokensから選ぶのだが、apiサーバーとして使ってるのでAPI tokensを使っていく。

$ yarn add @adonisjs/auth
$ node ace configure @adonisjs/auth
# いくつか入力を求められる
❯ Select provider for finding users · lucid
❯ Select which guard you need for authentication (select using space) · api

❯ Enter model name to be used for authentication · User
# すでにtableはあるからmigration fileは生成しない
❯ Create migration for the users table? (y/N) · false
❯ Select the provider for storing API tokens · database
# v4ではtokensテーブルだったので、api_tokensのmigration fileは生成する
❯ Create migration for the api_tokens table? (y/N) › true

CREATE: app/Models/User.ts
CREATE: database/migrations/1664350142101_api_tokens.ts
CREATE: contracts/auth.ts
CREATE: config/auth.ts
CREATE: app/Middleware/Auth.ts
CREATE: app/Middleware/SilentAuth.ts
UPDATE: tsconfig.json { types += "@adonisjs/auth" }
UPDATE: .adonisrc.json { providers += "@adonisjs/auth" }
CREATE: ace-manifest.json file

api_tokensテーブルを作成

トークン情報が保存されるapi_tokensテーブルを新たに作るためにマイグレーションを実行する。
—dry-runを付けてテスト実行してみたらエラーがでた。

$ node ace migration:run --dry-run

  Exception 

 Migration completed, but unable to release database lock

adonis_schema_versionsテーブルがAdonis v4にはなかったためエラーになったようで、再度—dry-runすると問題なかった。

$ node ace migration:run --dry-run
------------- database/migrations/1664350142101_api_tokens -------------

CREATE TABLE `api_tokens` (`id` INT unsigned NOT NULL auto_increment PRIMARY KEY, `user_id` INT unsigned, `name` VARCHAR(255) NOT NULL, `type` VARCHAR(255) NOT NULL, `token` VARCHAR(64) NOT NULL, `expires_at` TIMESTAMP NULL, `created_at` TIMESTAMP NOT NULL);
ALTER TABLE `api_tokens` ADD CONSTRAINT `api_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
ALTER TABLE `api_tokens` ADD UNIQUE `api_tokens_token_unique`(`token`)

------------- END -------------

マイグレーションを実行して、テーブル作成する。

$ node ace migration:run
❯ error database/migrations/1664350142101_api_tokens
[ error ]  create table `api_tokens` (`id` int unsigned not null auto_increment primary key, `user_id` int unsigned, `name` varchar(255) not null, `type` varchar(255) not null, `token` varchar(64) not null, `expires_at` timestamp null, `created_at` timestamp not null) - Invalid default value for 'created_at'

エラーでた。。。

created_atのデフォルト値を指定する必要がありそうだ。

https://knexjs.org/guide/schema-builder.html#timestamp

-      table.timestamp('created_at', { useTz: true }).notNullable()
+      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(this.now())

再実行したら、作成できた。

$ node ace migration:run
❯ migrated database/migrations/1664350142101_api_tokens

Migrated in 296 ms

Social認証のためにallyパッケージと設定の追加

$ yarn add @adonisjs/ally
$ node ace configure @adonisjs/ally

各ソーシャルのログイン認証に必要な環境変数を追加する。

export default Env.rules({
  // ### Variables for Twitter provider
  TWITTER_CLIENT_ID: Env.schema.string(),
  TWITTER_CLIENT_SECRET: Env.schema.string(),
  // ### Variables for Facebook provider
  FACEBOOK_CLIENT_ID: Env.schema.string(),
  FACEBOOK_CLIENT_SECRET: Env.schema.string(),
})
TWITTER_CLIENT_ID=clientId
TWITTER_CLIENT_SECRET=clientSecret
FACEBOOK_CLIENT_ID=clientId
FACEBOOK_CLIENT_SECRET=clientSecret

Auth middlewareを登録

リクエストに対して、認証されているかを確認するためにAuth Middlewareを使用する。

Middlewareを以下のように設定すると、HTTPリクエストがルートハンドラに到達する前にMiddlewareに設定した内容が実行され、リクエストを終了させたり次の関数に転送したりする。

Route.get('mypage', 'MypageController.index').middleware(['auth'])

node ace configure @adonisjs/auth実行時に、app/Middleware/Auth.tsという認証チェックするものが作成されているので、それをauthというミドルウェアとして登録する。

Server.middleware.registerNamed({
  auth: () => import('App/Middleware/Auth'),
})

ログインRouteを作成

ログイン認証するAPIを作成する。

例としてTwitterログインをできるようにしていく。
/api/v1/user/twitterに認証周りのAPIを作成していく。

Route.group(() => {
  Route.get('user/login/twitter', 'UserController.loginTwitter')
  Route.get('user/login/twitter/callback', 'UserController.loginTwitterCallback')
}).prefix('api/v1').namespace('App/Controllers/Http/Api/v1')
// namespaceがrootからのパスに変わってる。
// v4では、namespace('Api/v1')だった

処理はUserControllerに書いていく。

$ node ace make:controller Api/v1/UserController

CREATE: app/Controllers/Http/Api/v1/UserController.ts
export default class UsersController {
	// ここにアクセスしてきたら、Twitter上の認証画面にリダイレクト
  public async loginTwitter({ ally }: HttpContextContract) {
    return ally.use('twitter').redirect()
  }
 // 認証画面からのコールバック
  public async loginTwitterCallback({ ally, auth }: HttpContextContract) {
    try {
      const twitter = ally.use('twitter')
			Logger.info(await twitter.user())
			// 認証後の処理
      // DBにユーザーを追加したり、トークンを発行したりして
      // クライアント側に戻す
   } catch (error) {
      Logger.error(error)
      return 'Unable to authenticate. Try again later'
    }
  }
}

http://127.0.0.1:3333/api/v1/user/login/twitterにアクセスしてログインができるか確認しておく。

Callback用のViewを作っていく

自分の場合は、ログインボタン押下→別ウィンドウが開く→ログインが完了したらウィンドウを閉じる→元ウィンドウをマイページに遷移させる といった動作にしている。

なので、元ウィンドウにトークン情報を渡すために、Adonis側で一部Viewを用意している。

viewパッケージと設定の追加

$ yarn add @adonisjs/view
$ node ace configure @adonisjs/view

Callback用のViewテンプレートを生成

$ node ace make:view user/social-callback
CREATE: resources/views/user/social_callback.edge

social-callback.edge、ハイフンで指定していたが、Adonis v5ではsocial_callback.edge、アンダーバーで生成されることに注意

<p>Authenticated successfully.</p>
<script type="text/javascript">
  // 元ウィンドウにトークンを送る
  window.opener.postMessage({{{toJSON(token)}}}, '{{targetOrigin}}')
  window.close();
</script>

UserControllerは以下のようにした。

  public async loginTwitterCallback({ ally, auth, view }: HttpContextContract) {
    try {
      const twitter = ally.use('twitter')

      if (twitter.accessDenied()) {
        return 'Access was denied'
      }
      if (twitter.stateMisMatch()) {
        return 'Request expired. Retry again'
      }
      if (twitter.hasError()) {
        return twitter.getError()
      }

      const twUser = await twitter.user()

      if (!twUser.email) {
        return 'Cannot get email.'
      }

      // user details to be saved
      const userDetails = {
        email: twUser.email,
        source: 'twitter',
      }
      const whereClause = {
        email: twUser.email,
        source: 'twitter',
      }
      const user = await User.firstOrCreate(whereClause, userDetails)

      const token = await auth.use('api').generate(user, {
        expiresIn: '14 days',
      })
      Logger.info(JSON.stringify(token))

      view.share({
        token,
        targetOrigin: Env.get('URL'),
      })

      return view.render('user/social_callback')
    } catch (error) {
      Logger.error(error)
      return 'Unable to authenticate. Try again later'
    }
  }

renderの書き方がv4と少し変わっていた。
viewを指定するときドットで繋いでいたのがDeprecatedされて、スラッシュ/で繫ぐようになった。

- return view.render('user.social-callback')
+ return view.render('user/social_callback')

以上にて、Adonisでログイン認証設定が完了。

AdonisJS v5へのアップグレードが大変だったのでこちらにまとめました。