今回のTodoアプリケーションは、どのユーザーがどのTodoを作成したのかを識別するために認証が必要になります。
今回はIDaaSにAuth0を使用します。
Apollo Serverのcontext取得処理
Apollo Serverではcontextというオブジェクトがすべてのリゾルバの実装に渡されます。
フロントエンドからのリクエスト時にAuthorozationヘッダーにトークンを乗せてもらう想定なので、リクエストオブジェクトからトークンを取得し、それをもとにユーザー情報を取得し、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のセクションだけドキュメント通りに進めてください。
APIの追加
以下のドキュメントのConfigure Auth0 APIsのセクションだけドキュメント通りに進めてください。
フロントエンドのセットアップ
以下のリポジトリをcloneしてください。
.envファイルを作成し、環境変数を追加します。
Auth0の管理画面からフロントエンドのアプリケーションを選択し、必要な項目を入力してください。
自分の場合は以下のようになります。
VITE_AUTH0_AUDIENCEはアクセストークンのリソース識別子として使用されるだけなので、実在しないドメインを使ったURLを指定して問題ありません。
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として返す
といった感じです。
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
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のユーザーとは別の概念)を作成するリゾルバだけ実装しておきましょう。
まず、リゾルバを集約するファイルを作成しておきます。
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クライアント用のファイルを作成します。
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
createUserリゾルバを実装します。
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のユーザーに紐付いたデータが作成できました。