Chapter 07

Auth0による認証の準備

Eringi_V3
Eringi_V3
2021.09.16に更新

今回のTodoアプリケーションは、どのユーザーがどのTodoを作成したのかを識別するために認証が必要になります。
今回はIDaaSにAuth0を使用します。

Apollo Serverのcontext取得処理

Apollo Serverではcontextというオブジェクトがすべてのリゾルバの実装に渡されます。
フロントエンドからのリクエスト時にAuthorozationヘッダーにトークンを乗せてもらう想定なので、リクエストオブジェクトからトークンを取得し、それをもとにユーザー情報を取得し、contextオブジェクトに詰めます。
こうすることで、リクエストを送信してきたユーザーの情報を各リゾルバで参照することが可能になります。

以下のようなイメージのことをやりたいです。

context取得処理
const server = new ApolloServer({
  schema: schemaWithResolvers,
  context: async (ctx) => {
    const token = ctx.req.headers.authorization?.replace('Bearer ', '');
    // tokenからユーザーを取得
    const user = await getUserInfoByToken(token)
    return {
      user,
    };
  },
});
リゾルバ
export const getTodos: QueryResolvers['getTodos'] = async (
  parent,
  args,
  context,
  info
) => {
  const todos = await prisma.todo.findMany({
    where: {
      userId: context.user?.id,
    },
    include: {
      user: true,
    },
  });
  return todos;
};

Auth0のセットアップ

詳細は省きますが、今回のTodoアプリケーション用のテナントを新しく作成し、クライアントの追加とAPIの追加を行う必要があります。

クライアントの追加

以下のドキュメントのConfigure Auth0のセクションだけドキュメント通りに進めてください。
https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0

APIの追加

以下のドキュメントのConfigure Auth0 APIsのセクションだけドキュメント通りに進めてください。
https://auth0.com/docs/quickstart/backend/nodejs/01-authorization#configure-auth0-apis

フロントエンドのセットアップ

以下のリポジトリをcloneしてください。
https://github.com/EringiV3/vite-react-todo-app

.envファイルを作成し、環境変数を追加します。
Auth0の管理画面からフロントエンドのアプリケーションを選択し、必要な項目を入力してください。
自分の場合は以下のようになります。

VITE_AUTH0_AUDIENCEはアクセストークンのリソース識別子として使用されるだけなので、実在しないドメインを使ったURLを指定して問題ありません。

.env
VITE_API_HOST=http://localhost:4000/graphql
VITE_AUTH0_DOMAIN=apollo-server-prisma-todo-app.jp.auth0.com
VITE_AUTH0_CLIENT_ID=aUD1RQ95pCRmKGITpCxnpcyk8wZFknjk
VITE_AUTH0_AUDIENCE=https://apollo-server-prisma-todo-app.eringiv3.com

yarn devで起動して、表示されたログインボタンを押したらAuth0の画面にリダイレクトされればOKです。

フロントエンドのセットアップはここまでです。

Apollo Serverのcontext取得処理を実装する

流れとしては、

  • jwtを検証してペイロードを取得
  • Auth0のユーザー情報を取得
  • jwtのsub(ユーザーごとに一意な値をアプリケーション内のユーザーIDとして扱う)とユーザー情報をContextとして返す
    といった感じです。
src/index.ts
const server = new ApolloServer({
  schema: schemaWithResolvers,
  cors: true,
  context: async (ctx) => {
    const token = ctx.req.headers.authorization?.replace('Bearer ', '');
    if (token === undefined) {
      return {
        user: undefined,
      };
    }

    try {
      const user = await new Promise<JwtPayload>((resolve, reject) => {
        const client = jwksClient({
          jwksUri: `${AUTH0_DOMAIN}/.well-known/jwks.json`,
        });
        jwt.verify(
          token,
          (header, cb) => {
            client.getSigningKey(header.kid, function (err, key) {
              const signingKey = key.getPublicKey();
              cb(null, signingKey);
            });
          },
          {
            audience: `${AUTH0_AUDIENCE}`,
            issuer: `${AUTH0_DOMAIN}/`,
            algorithms: ['RS256'],
          },
          (err, decoded) => {
            if (err) {
              return reject(err);
            }
            if (decoded === undefined) {
              return reject('decoded is invalid.');
            }
            resolve(decoded);
          }
        );
      });

      const userInfo = await fetch(`${AUTH0_DOMAIN}/userinfo`, {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
      }).then((res) => res.json());

      return {
        user: {
          id: user.sub,
          name: userInfo.nickname,
          email: userInfo.email,
        },
      } as Context;
    } catch (error) {
      return {
        user: undefined,
      };
    }
  },
});

定数は環境変数として持っておき、ハードコーディングを避けたいので一箇所にまとめておきましょう。

yarn add dotenv
config/constsnts.ts
import dotenv from 'dotenv';

dotenv.config();

export const APOLLO_SERVER_PORT = process.env.PORT ?? '4000';
export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN ?? '';
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE ?? '';

ユーザー作成処理(createUserリゾルバ)の実装

アプリケーション内で扱うユーザー(Auth0のユーザーとは別の概念)を作成するリゾルバだけ実装しておきましょう。

まず、リゾルバを集約するファイルを作成しておきます。

src/resolvers/index.ts
import { Resolvers } from '../types/generated/graphql';
import { createUser } from './mutation/createUser';
import { dateScalar } from './scalar/date';

const resolvers: Resolvers = {
  Query: {
    getUser: () => null,
    getTodos: () => [],
    getTodoById: () => null,
  },
  Mutation: {
    addTodo: () => null,
    updateTodo: () => null,
    deleteTodo: () => null,
    createUser: createUser,
    updateUser: () => null,
  },
  Date: dateScalar,
};

export default resolvers;

prismaクライアント用のファイルを作成します。

src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

createUserリゾルバを実装します。

src/resolvers/mutation/createUser.ts
import { prisma } from '../../lib/prisma';
import { MutationResolvers } from '../../types/generated/graphql';

export const createUser: MutationResolvers['createUser'] = async (
  parent,
  args,
  context,
  info
) => {
  const userId = context.user?.id;
  if (!userId) {
    throw new Error('Authentication Error.');
  }

  const user = await prisma.user.findUnique({
    where: {
      id: userId,
    },
  });

  if (user) {
    throw new Error('Alredy exists user.');
  }

  const createdUser = await prisma.user.create({
    data: {
      id: userId,
      name: args.input.name,
    },
  });
  return createdUser;
};

ここまでできたら、先程のフロントエンドアプリケーションからAuth0でSignUpしてみましょう。
問題なければUserのレコードがDBに作成されているはずです。
yarn prisma studioで確認してみましょう。

これでContextのユーザーに紐付いたデータが作成できました。