Chapter 08無料公開

✅認証したユーザをContextに渡す

たった
たった
2021.06.25に更新
このチャプターの目次
アプリケーションコード
src
>├── middleware     ... 認証・認可とGraphQLのコンテキスト
├── domain         ... ビジネスロジックの共通化
├── usecases       ... アプリケーションロジック
├── infrastructure ... 外部サービスとのやりとり
├── entities       ... エンティティとGraphQLのフィールド
├── resolvers      ... GraphQLのリゾルバー
└── inversify.config.ts ... 依存性の注入(以下、DI)の設定

このチャプターで使用するライブラリ

  • ApolloServer
  • TypeGraphQL

概要

認証は開発者を悩ませる実装の1つです。私見としては認証にはベストプラクティスと呼べるものがあるので、それを実装するのが良いと思います。[1]

GraphQLでは、リクエストしてきたユーザをContextに渡すことで、Resolverに共有することができます。ユーザをどのように扱うかは後述します。

ここでは、以下の実装にフォーカスします。

  1. リクエストを認証する
  2. 認証されたユーザをContextに渡す

今回はIDaaSの一つである、Auth0を例に使って紹介します。[2]
JWTを使って手軽にセキュアな認証が実現できるのが強みだと思います。

実装

まずはContextを定義します。

be/src/middleware/types/ContextType.ts
type CurrentUser = {
  id: number
  role: UserRole
}

export interface IContext {
  currentUser: CurrentUser | undefined
}

エントリーポイントに認証を実装します。
ここでは、JWTを検証してサーバにContextの一部として渡します。

be/src/index.ts
import { ApolloServer } from 'apollo-server-express'
import express from 'express'
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'

import buildSchema from 'src/middleware/buildSchema'

async function bootstrap() {
  const app = express()
  
  // ここでJWTを検証
  const checkJwt = jwt({
    secret: jwksRsa.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${YOUR_DOMAIN}/.well-known/jwks.json`,
    }),
    audience: `${YOUR_DOMAIN}`,
    issuer: [`https://${YOUR_DOMAIN}/`],
    algorithms: ['RS256'],
  })
  app.use(checkJwt)

  new ApolloServer({
    schema: await buildSchema(),
  }).applyMiddleware({
    app,
    path: '/',
  })

  app.listen(4000, () => console.log('Server has started!'))
}

bootstrap()

つづいてContextに対して、認証処理を行います。
JWTの検証結果からIDを取得して、IDを用いてユーザを取得します。[3]

be/src/middleware/bindAuthenticatedUserToContext.ts
import { AuthenticationError, ExpressContext } from 'apollo-server-express'
import { getConnection } from 'typeorm'

import {
  IAuthenticatedRequest,
  IContext,
} from 'src/middleware/types/ContextType'

export const bindAuthenticatedUserToContext = async (
  ctx: ExpressContext
): Promise<IContext> => {
  // JWTの検証結果からIDを取得する
  const auth0ID = (ctx.req as IAuthenticatedRequest).user?.sub
  // 取り出せなかったら、認証に失敗
  if (!auth0ID) throw new AuthenticationError('wrong request')

  const conn = getConnection()
  const currentUser = await conn.getRepository(User).findOne({
    auth0ID: auth0ID,
  })

  // Contextを返す
  return {
    currentUser: !currentUser
      ? undefined
      : {
          id: currentUser.id,
          role: currentUser.role,
        }
  }
}

これをApolloServerのcontextオプションに渡してあげれば、操作しているユーザをContext経由で共有することができます。

be/src/index.ts
import buildSchema from 'src/middleware/buildSchema'
+ import { bindAuthenticatedUserToContext } from 'src/middleware/bindAuthenticatedUserToContext'

async function bootstrap() {
  const app = express()
  /* 略 */
  new ApolloServer({
    schema: await buildSchema(),
+    context: bindAuthenticatedUserToContext,
  }).applyMiddleware({
    app,
    path: '/',
  })

  app.listen(4000, () => console.log('Server has started!'))
}

bootstrap()
脚注
  1. ユーザー アカウント、認証、パスワード管理に関する 13 のベスト プラクティス2021 年版- Google Cloud ↩︎

  2. さらにauth0のことを知りたい方は、こちらのチュートリアルを試してみてください。 ↩︎

  3. 実際には、auth0IDをそのままuserテーブルのカラムにするケースは少ないと思います。 ↩︎