NestJS + GraphQL + Prisma + MySQL でJWT認証をつくる
NestJSでGraphQLとPrismaを使ってJWTを使った認証をつくってみます。
DBにはMySQLを使います。
できること
- GraphQLによるAPI
- DBのスキーマ定義からObject TypeやInput Typeなどの定義を自動生成
- GraphQLのスキーマ定義の自動生成
- ValidationPipeを使った自動的なバリデーション
- パスワードをハッシュ化して保存
- メールアドレスとパスワードによる認証(NestJSのデフォルトはユーザ名とパスワード)
- JWTトークンを使った認証
開発環境構築
VS Codeの拡張機能Remote - Containersを使ってDocker上に開発環境をつくります。
ここではプロジェクトのディレクトリをnest-graphql-prismaとします。
プロジェクトのディレクトリをVS Codeで開き、直下に*.devcontainer*ディレクトリを作成します。
.
└ nest-graphql-prisma/
└ .devcontainer/
.devcontainerディレクトリに下記ファイルを作成します。
{
"name": "nest-graphql-prisma",
"dockerComposeFile": "docker-compose.yml",
"service": "node",
"workspaceFolder": "/src",
"settings": {
},
"extensions": [
],
}
version: "3"
services:
node:
image: node:16-bullseye
environment:
TZ: "Asia/Tokyo"
ports:
- "3000:3000"
volumes:
- ../:/src
working_dir: /src
depends_on:
- mysql
command: /bin/sh -c "while sleep 1000; do :; done"
mysql:
platform: linux/x86_64
image: mysql:5.7
environment:
MYSQL_DATABASE: nest-db
MYSQL_USER: nest-user
MYSQL_PASSWORD: nest-pass
MYSQL_ROOT_PASSWORD: nest-pass
TZ: "Asia/Tokyo"
ports:
- "33306:3306"
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
タイムゾーンには"Asia/Tokyo"を設定しています。
ファイルを作成したらVS CodeでRemote-Containers: Reopen in Containerを実行します。
Nest CLIのインストールとNestプロジェクトの作成
下記のコマンドを実行してNest CLIのインストールとNestプロジェクトの作成をします。
今回Nestプロジェクトの名前をapiにしました。
$ npm i -g @nestjs/cli
$ nest new api
下記のように聞かれるので今回はnpmを選択しました。
? Which package manager would you ❤️ to use? (Use arrow keys)
❯ npm
yarn
pnpm
インストールが終了後、下記コマンドを実行することでNestJSが起動します。
$ cd api
$ npm run start
起動後、http://localhost:3000にアクセスすると下記のように表示されます。
Hello World!
起動することが確認できたらいったんcontrol + cで終了します。
参考:First steps | NestJS
Prismaのインストール
下記コマンドを実行してPrismaをインストールします。
$ npm install prisma --save-dev
続いて下記コマンドでPrismaの初期設定をします。
$ npx prisma init
api/prisma/schema.prismaが作成されます。
デフォルトではPostgreSQLを使う設定になっているのでMySQLを使うように変更します。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
- provider = "postgresql"
+ provider = "mysql"
url = env("DATABASE_URL")
}
また、.envファイルも作成され、DB接続設定が記述されているので、デフォルトの接続設定から今回の接続設定に変更します。
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
+ DATABASE_URL="mysql://root:nest-pass@mysql:3306/nest-db"
GraphQLのパッケージをインストール
下記コマンドでGraphQLのパッケージをインストールします。
$ npm i @nestjs/graphql graphql@^15 apollo-server-express
インストールが終わったらapi/src/app.module.tsにGraphQLModuleを追加します。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
+ import { GraphQLModule } from '@nestjs/graphql';
+ import { join } from 'path';
@Module({
- imports: [],
+ imports: [
+ GraphQLModule.forRoot({
+ autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
+ }),
+ ],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
autoSchemaFileのプロパティを設定することでNestJS起動時に自動でGraphQLのスキーマ定義ファイルが作成されるようになります。
参考:Harnessing the power of TypeScript & GraphQL | NestJS
ValidationPipeが使えるように設定
自動的にバリデーションが実行できるようにts:api/src/main.tsにグローバルスコープでValidationPipeを追加します。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
+ import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
参考:Global scoped pipes | NestJS
prisma-nestjs-graphqlのインストール
下記コマンドでPrismaのスキーマファイルからObject TypeやInput Typeを生成してくれるprisma-nestjs-graphqlをインストールします。
$ npm install --save-dev prisma-nestjs-graphql
api/prisma/schema.prismにgeneratorを追加します。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
+ generator nestgraphql {
+ provider = "node node_modules/prisma-nestjs-graphql"
+ output = "../src/@generated/prisma-nestjs-graphql"
+ fields_Validator_from = "class-validator"
+ fields_Validator_input = true
+ decorate_1_type = "CreateOne*Args"
+ decorate_1_field = data
+ decorate_1_name = ValidateNested
+ decorate_1_from = "class-validator"
+ decorate_1_arguments = "[]"
+ decorate_2_type = "CreateOne*Args"
+ decorate_2_field = data
+ decorate_2_from = "class-transformer"
+ decorate_2_arguments = "['() => {propertyType.0}']"
+ decorate_2_name = Type
+ }
参考:prisma-nestjs-graphql | GitHub
fields_Validator_from = "class-validator"
fields_Validator_input = true
decorate_1_type = "CreateOne*Args"
decorate_1_field = data
decorate_1_name = ValidateNested
decorate_1_from = "class-validator"
decorate_1_arguments = "[]"
decorate_2_type = "CreateOne*Args"
decorate_2_field = data
decorate_2_from = "class-transformer"
decorate_2_arguments = "['() => {propertyType.0}']"
decorate_2_name = Type
この部分はモデル定義のディレクティブで指定したバリデーションをNestJSのValidationPipeをつかってバリデーションするための設定です。
今回はprisma-nestjs-graphqlによって生成された正規表現CreateOne*Argsにマッチするファイルに対してバリデーションを追加しています。
参考:decorate | prisma-nestjs-graphql | GitHub
Userモデルを定義
api/prisma/schema.prismにUserモデルの定義を追加します。
model User {
/// @Field(() => ID)
id Int @id @default(autoincrement())
/// @Validator.@IsEmail()
email String @unique
/// @Validator.IsNotEmpty()
name String
/// @HideField()
/// @Validator.MinLength(8)
password String
/// @HideField({ input: true, output: true })
createdAt DateTime @default(now())
/// @HideField({ input: true, output: true })
updatedAt DateTime @updatedAt
}
///(3つのスラッシュ)からはじまるコメントでディレクティブを記述することでprisma-nestjs-graphqlによって生成されるObject TypeやInput Typeにディレクティブを追加できます。
passwordは@HideField()で出力不可、createdAtとupdatedAtは入出力不可にしています。
また、ここでは下記のようにバリデーションを設定しています。
フィールド名 | バリデーション |
---|---|
IsEmail() | |
name | IsNotEmpty() |
password | MinLength(8) |
参考:https://github.com/unlight/prisma-nestjs-graphql#field-settings
参考:https://github.com/typestack/class-validator#validation-decorators
Prismaクライアントを生成
下記コマンドでPrismaクライアントを生成します。
初回実行時に@prisma/clientパッケージがインストールされていない場合はインストールも実行されます。
$ npx prisma generate
api/src/@generated/prisma-nestjs-graphql配下にObject TypeやInput Type等のファイルが生成されます。
モデルに変更があった場合は必ずこのコマンドを実行します。
このコマンドによって出力されたObject Typeは下記のようになります。
import { Field } from '@nestjs/graphql';
import { ObjectType } from '@nestjs/graphql';
import { ID } from '@nestjs/graphql';
import { HideField } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => ID, {nullable:false})
id!: number;
@Field(() => String, {nullable:false})
email!: string;
@Field(() => String, {nullable:false})
name!: string;
@HideField()
password!: string;
@HideField()
createdAt!: Date;
@HideField()
updatedAt!: Date;
}
また、Input Typeは下記のようになります。
import { Field } from '@nestjs/graphql';
import { InputType } from '@nestjs/graphql';
import * as Validator from 'class-validator';
import { HideField } from '@nestjs/graphql';
@InputType()
export class UserCreateInput {
@Field(() => String, {nullable:false})
@Validator.IsEmail()
email!: string;
@Field(() => String, {nullable:false})
@Validator.IsNotEmpty()
name!: string;
@Field(() => String, {nullable:false})
@Validator.MinLength(8)
password!: string;
@HideField()
createdAt?: Date | string;
@HideField()
updatedAt?: Date | string;
}
Prisma Clientサービスの追加
src配下にprisma.service.tsを作成し、下記のようにします。
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient
implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
api/src/app.module.tsのprovidersに作成したPrisma Clientサービスを追加します。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
+ import { PrismaService } from './prisma.service';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
controllers: [AppController],
- providers: [AppService],
+ providers: [AppService, PrismaService],
})
export class AppModule {}
参考:Use Prisma Client in your NestJS services | NestJS
マイグレーション
下記コマンドでマイグレーションを実行します。
$ npx prisma migrate dev --name init
マイグレーションを実行した日時でそのマイグレーションの名前がつきますが、--name
に指定することで名前を追加できます。
マイグレーションを実行するとapi/prisma/migrations/20220113135647_init/migration.sqlのようなファイルが作成されますが、今回--name init
としているのでディレクトリ名に20220113135647_initがついています。
bcryptのインストール
パスワードをハッシュ化して保存するためにbcryptをインストールします。
$ npm i bcrypt
$ npm i -D @types/bcrypt
Users Module、Service、Resolverの作成
下記のコマンドでUsers Module、Service、Resolverを作成します。
$ npx nest generate module users
$ npx nest generate service users
$ npx nest generate resolver users
Users Moduleを編集
PrismaServiceを使用するので下記のようにUsers Moduleを編集します。
また、
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersResolver } from "./users.resolver";
+ import { PrismaService } from 'src/prisma.service';
@Module({
- providers: [UsersService, UsersResolver],
+ providers: [UsersService, UsersResolver, PrismaService],
+ exports: [UsersService]
})
export class UsersModule {}
後で作成するAuthServiceでUserServiceを使えるようにexports: [UsersService]
も追加しています。
Users Serviceを編集
下記のようにUsers Serviceを編集します。
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
import { User } from 'src/@generated/prisma-nestjs-graphql/user/user.model'
import { FindFirstUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/find-first-user.args';
import { CreateOneUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/create-one-user.args';
import { FindUniqueUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/find-unique-user.args';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async findFirst(args: FindFirstUserArgs): Promise<User | null> {
return this.prisma.user.findFirst(args);
}
async findUnique(args: FindUniqueUserArgs): Promise<User | null> {
return this.prisma.user.findUnique(args);
}
async createUser(args: CreateOneUserArgs): Promise<User> {
return this.prisma.user.create(args);
}
}
findFirstは条件にあうUserを1件取得、findUniqueはユニークキー(ここではidかemail)によってUserを1件取得、createUserはUserを1件作成するメソッドです。
Users Resolverを編集
api/src/users/users.resolver.tsを下記のように編集し、prisma-nestjs-graphqlで生成されたObject TypeやArgsを使ってUserの参照・登録の処理を追加します。
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import * as bcrypt from 'bcrypt';
import { User } from 'src/@generated/prisma-nestjs-graphql/user/user.model'
import { CreateOneUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/create-one-user.args';
import { UsersService } from 'src/users/users.service';
import { FindFirstUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/find-first-user.args';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly userService: UsersService) {}
@Query(() => User)
user(
@Args() args: FindFirstUserArgs
) {
return this.userService.findFirst(args)
}
@Mutation(() => User)
async createUser(
@Args() args: CreateOneUserArgs
) {
args.data.password = await bcrypt.hash(args.data.password, 10);
return this.userService.createUser(args);
}
}
ここではbcryptを使い、ラウンド数を10でパスワードをハッシュ化しています。
User登録の動作確認
Userの登録ができるか動作確認をします。
下記コマンドでNestJSのサーバーを起動させます。
$ npm run start
サーバーが起動後http://localhost:3000/graphqlにアクセスするとGraphQL playgroundが表示されます。
参考:GraphQL playground | NestJS
下記を入力してユーザーを登録してみます。
mutation {
createUser(
data: {
email: "test@example.com",
name: "test",
password: "password12345",
}
) {
id
email
name
}
}
登録に問題がなければ下記のように表示されます。
{
"data": {
"createUser": {
"id": "1",
"email": "test@example.com",
"name": "test"
}
}
}
下記コマンドを実行するとPrismaをGUIで操作できるPrisma Studioが起動します。
$ npx prisma studio
Prisma Studio起動後、http://localhost:5556/にアクセスするとデータベースをGUIで操作できます。
Userテーブルを選択して登録したデータを表示すると、パスワードがハッシュ化されて登録されていることが確認できます。
バリデーションが実行されるか、下記のようにすべての項目を空にして確認します。
mutation {
createUser(
data: {
email: "",
name: "",
password: "",
}
) {
id
email
name
}
}
バリデーションがうまく実行されると下記の結果のようにエラーが返ってきます。
{
"errors": [
{
"message": "Bad Request Exception",
"extensions": {
"code": "BAD_USER_INPUT",
"response": {
"statusCode": 400,
"message": [
"data.email must be an email",
"data.name should not be empty",
"data.password must be longer than or equal to 8 characters"
],
"error": "Bad Request"
}
}
}
],
"data": null
}
Passportのインストール
下記コマンドで認証に使うPassportをインストールします。
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
参考:Authentication requirements | NestJS
下記コマンドでJWTを操作するためのパッケージもインストールします。
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
JWTのシークレットキーを設定
JWTで署名に使うシークレットキーを設定します。
ここでは下記のコマンドを実行してシークレットキーを生成します。
node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"
下記のシークレットキーが生成されました。
a1bfWJnEP4Gfh3LSLw+vLAYr7+0N+/oo4ycpfpqfSBYdcWjS1ch/I69a1sxhpSj4gcKbNCe38nTabXxAUWWNDUm73HKhqEU2+oeeLFZ38oA5+qGf2hZmkFb4vHUq/Z0gdsPL2bhhW5fyOPt5YuQke42jLERMa8UmkzAaSY6WBYxIhxJH2Gsm+Af/zMy+76jsKztEdimPVZOYA0OThiiJXJP18K7EIwNKUxmWKW3W9ajGTGkOW6djf8qsWpfkiSoFaMJx2OXmeS9UCtAbotEa89FsBRwGuiPBxzvG89rqgBVDK8YTXXwQFn6/d7EpuMtK+6Kr5Y1ROo07GRA+MqkeAw==
生成したシークレットキーをapi/.envに追加します。
JWT_SECRET=a1bfWJnEP4Gfh3LSLw+vLAYr7+0N+/oo4ycpfpqfSBYdcWjS1ch/I69a1sxhpSj4gcKbNCe38nTabXxAUWWNDUm73HKhqEU2+oeeLFZ38oA5+qGf2hZmkFb4vHUq/Z0gdsPL2bhhW5fyOPt5YuQke42jLERMa8UmkzAaSY6WBYxIhxJH2Gsm+Af/zMy+76jsKztEdimPVZOYA0OThiiJXJP18K7EIwNKUxmWKW3W9ajGTGkOW6djf8qsWpfkiSoFaMJx2OXmeS9UCtAbotEa89FsBRwGuiPBxzvG89rqgBVDK8YTXXwQFn6/d7EpuMtK+6Kr5Y1ROo07GRA+MqkeAw==
参考:Hapi Auth using JSON Web Tokens (JWT) | GitHub
ログインに使うInputTypeとObjectTypeを作成
今回ログインの認証にはメールアドレスとパスワードを使うので、api/src/auth/dtoディレクトリを作成し、その中にファイル名をlogin-user.input.tsでログイン用のInputTypeを下記のように作成します。
import { Field, InputType } from "@nestjs/graphql";
@InputType()
export class LoginUserInput {
@Field()
email: string
@Field()
password: string
}
ログイン成功時のレスポンスにJWTのトークンとUserを返すのでapi/src/auth/dtoディレクトリにlogin-response.tsを作成し下記のようにObjectTypeを記述します。
import { Field, ObjectType } from "@nestjs/graphql";
import { User } from "src/@generated/prisma-nestjs-graphql/user/user.model";
@ObjectType()
export class LoginResponse {
@Field()
access_token: string;
@Field(() => User)
user: User;
}
Auth Module、Service、Resolverの作成
下記コマンドでAuth Module、Service、Resolverを作成します。
$ npx nest generate module auth
$ npx nest generate service auth
$ npx nest generate resolver auth
AuthServiceを編集
api/src/auth/auth.service.tsを下記のように編集します。
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';
import { User } from 'src/@generated/prisma-nestjs-graphql/user/user.model';
import * as bcrypt from 'bcrypt';
import { LoginResponse } from 'src/auth/dto/login-response';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findUnique({where: {email: email}});
if (user && bcrypt.compareSync(password, user.password)) {
return user;
}
return null;
}
async login(user: User): Promise<LoginResponse> {
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
user: user
};
}
}
validateUserではUsersServiceのfindUniqueで入力されたemailからUserを取得しています。
bcrypt.compareSyncで入力されたパスワードとハッシュ化されたパスワードが正しいか比較しています。
loginでは認証を通ったUserにJWTトークンを発行しています。
LocalStrategyの作成
メールアドレスとパスワードによる認証のためのLocalStrategyを作成します。
api/src/auth/strategiesディレクトリを作成し、下記のようにlocal.strategy.tsを作成します。
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { User } from "src/@generated/prisma-nestjs-graphql/user/user.model";
import { AuthService } from "src/auth/auth.service";
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({ usernameField: 'email' })
}
async validate(email: string, password: string): Promise<User> {
const user = this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
デフォルトではusernameフィールドとpasswordフィールドで認証するようになっているので、下記部分でemailフィールドを使うように変更しています。
super({ usernameField: 'email' })
認証処理のvalidateではAuthServiceのvalidateUserでUserが取得できない場合はUnauthorizedExceptionの例外がでるようになっています。
GqlAuthGuardの作成
api/src/auth/guardsディレクトリを作成し、下記のようにLocalStrategy用のgql-auth.guard.tsを作成します。
import { ExecutionContext } from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";
import { AuthGuard } from "@nestjs/passport";
export class GqlAuthGuard extends AuthGuard('local') {
constructor() {
super();
}
getRequest(context: ExecutionContext): any {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext();
request.body = ctx.getArgs().loginUserInput;
return request;
}
}
AuthGuardクラスはREST用なのでGraphQLでも使えるようにオーバーライドしています。
リクエストの形式もLoginUserInputにしています。
JwtStrategyの作成
メールアドレスとパスワードによる認証後に発行されたJWTトークンを使った認証のためのJwtStrategyを作成します。
api/src/auth/strategiesディレクトリにjwt.strategy.tsを作成し下記のようにします。
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from "passport-jwt";
import { User } from 'src/@generated/prisma-nestjs-graphql/user/user.model';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
})
}
async validate(payload: { email: string, sub: string }): Promise<User | null> {
return this.usersService.findUnique({where: {email: payload.email}});
}
}
secretOrKeyには生成したprocess.env.JWT_SECRETを設定しています。
validateではJWTトークンからemailを取得し、それを使ってUserを取得しています。
JwtAuthGuardの作成
LocalStrategyと同様にJwtStrategyのAuthGuardクラスもREST用なのでGraphQLでも使えるようにオーバーライドします。
api/src/auth/guardsにjwt-auth.guard.tsを作成し下記のようにします。
import { Injectable, ExecutionContext } from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
AuthModuleの編集
下記のようにapi/src/auth/auth.module.tsを編集します。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from "@nestjs/jwt";
import { AuthResolver } from './auth.resolver';
import { LocalStrategy } from 'src/auth/strategies/local.strategy';
import { JwtStrategy } from 'src/auth/strategies/jwt.strategy';
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' }
})
],
providers: [AuthService, AuthResolver, LocalStrategy, JwtStrategy]
})
export class AuthModule {}
secretには生成したprocess.env.JWT_SECRET、有効期限には1時間を設定しています。
AuthResolverの編集
下記のようにapi/src/auth/auth.resolver.tsを編集します。
import { UseGuards } from '@nestjs/common';
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthService } from 'src/auth/auth.service';
import { LoginResponse } from 'src/auth/dto/login-response';
import { LoginUserInput } from 'src/auth/dto/login-user.input';
import { GqlAuthGuard } from 'src/auth/guards/gql-auth.guard';
@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
@Mutation(() => LoginResponse)
@UseGuards(GqlAuthGuard)
async login(
@Args('loginUserInput') loginUserInput: LoginUserInput,
@Context() context
) {
return this.authService.login(context.user)
}
}
loginのデコレータにメールアドレスとパスワードでの認証の@UseGuards(GqlAuthGuard)
をつけています。
UserResolverの編集
認証されたUserのみがuserを実行できるように下記を追加します。
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import * as bcrypt from 'bcrypt';
import { User } from 'src/@generated/prisma-nestjs-graphql/user/user.model'
import { CreateOneUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/create-one-user.args';
import { UsersService } from 'src/users/users.service';
import { FindFirstUserArgs } from 'src/@generated/prisma-nestjs-graphql/user/find-first-user.args';
+ import { UseGuards } from '@nestjs/common';
+ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly userService: UsersService) {}
@Query(() => User)
+ @UseGuards(JwtAuthGuard)
user(
@Args() args: FindFirstUserArgs
) {
return this.userService.findFirst(args)
}
@Mutation(() => User)
async createUser(
@Args() args: CreateOneUserArgs
) {
args.data.password = await bcrypt.hash(args.data.password, 10);
return this.userService.createUser(args);
}
}
ログインの動作確認
ログインの動作確認をするため、下記コマンドでNestJSのサーバーを起動させます。
$ npm run start
サーバーが起動後http://localhost:3000/graphqlにアクセスします。
User登録の動作確認で作成したUserを使ってログインしてみます。
mutation {
login(
loginUserInput: {
email: "test@example.com",
password: "password12345"
}
) {
user {
name,
email
},
access_token
}
}
下記のように表示されれば成功です。
{
"data": {
"login": {
"user": {
"name": "test",
"email": "test@example.com"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJzdWIiOjEsImlhdCI6MTY0MjYwMzgxNywiZXhwIjoxNjQyNjA3NDE3fQ.qKZ2iJjkVVye46RpJbKu5_QBt90mHr7nz2rrKNk33rk"
}
}
}
JWT認証の動作確認
取得したJWTトークンを使用して認証できるか確認します。
下記を入力して実行します。
query {
user(where: {email: {equals: "test@example.com"}}) {
id
email
name
}
}
JWTトークンを設定していないので下記のように認証エラーが表示されます。
{
"errors": [
{
"message": "Unauthorized",
"extensions": {
"code": "UNAUTHENTICATED",
"response": {
"statusCode": 401,
"message": "Unauthorized"
}
}
}
],
"data": null
}
GraphQL playgroundの左下にあるHTTP HEADERSをクリックして下記のように入力します。
{
"Authorization": "Bearer ログインで取得したJWTトークン"
}
入力後、再度実行し、下記のように表示されれば成功です。
{
"data": {
"user": {
"id": "1",
"email": "test@example.com",
"name": "test"
}
}
}
認証処理の流れのまとめ
メールアドレスとパスワードを使った認証の流れは
- メールアドレスとパスワードを入力してloginをリクエスト
- AuthResolverのloginのデコレータの@UseGuards(GqlAuthGuard)が実行
- @UseGuards(GqlAuthGuard)がLocalStrategyのvalidateを実行
- LocalStrategyのvalidateがAuthServiceのvalidateUserを実行
- validateUserでメールアドレスとパスワードをチェック
- 認証されたUserがContextのuserに渡される
- AuthResolverのloginに戻る
- Contextのuserに渡されたUserを使ってAuthServiceのloginを実行
- 渡されたUserの情報からJWTトークンを取得
- 取得したJWTトークンとともにUserを返却
となります。
また、JWTトークンでの認証の流れば
- メールアドレスとパスワードを使った認証で取得したJWTトークンを使ってuserをリクエスト
- UsersResolverのuserのデコレータの@UseGuards(JwtAuthGuard)が実行
- @UseGuards(JwtAuthGuard)がJwtStrategyのvalidateを実行
- JWTトークンに問題がなければJWTトークンに含まれる情報からUserを取得
- RequestのuserにUserを渡す
- UsersResolverのuser戻る
となります。
今回の使用した各種バージョン
[System Information]
OS Version : Linux 5.10
NodeJS Version : v16.13.1
NPM Version : 8.1.2
[Nest CLI]
Nest CLI Version : 8.1.8
[Nest Platform Information]
platform-express version : 8.2.4
schematics version : 8.0.5
passport version : 8.1.0
graphql version : 9.1.2
testing version : 8.2.4
common version : 8.2.4
core version : 8.2.4
jwt version : 8.0.0
cli version : 8.1.8
[Database]
MySQL : 5.7
Prisma Client : 3.8.0
まとめ
探してもやりたい構成を網羅した解説がなかったのでやってみました。
それなりに実用的にはなってると思います。
prisma-nestjs-graphqlがいろいろ自動生成してくれて便利。
続き
Discussion