実装手順の概要
- Authモジュールのセットアップ
nest g module auth nest g controller auth nest g service auth
- userエンティティを作成
- 以下コマンドでマイグレーション
npx typeorm migration:generate -n createUser npx typeorm migration:run
- 実装
- dtoクラスを作成・実装
- UserRepositoryを作成・実装
- RepositoryをAuthModuleにimportする
- Service実装・実装
- Controllerを実装
パスワードなどをhash化する
- ライブラリのインストール
npm i --save bcrypt npm i --save-dev @types/bcrypt
- Repositoryでhash化を実装
以下は実装例。パスワードをhash化している。import { User } from "src/entities/user.entity"; import { EntityRepository, Repository } from "typeorm"; import { CreateUserDto } from "./dto/create-user.dto"; import * as bcrypt from 'bcrypt'; @EntityRepository(User) export class UserRepository extends Repository<User> { async createUser(createUserDto: CreateUserDto): Promise<User> { const { username, password, status } = createUserDto; // saltを作成。ハッシュ値の復元をより難しくする const salt = await bcrypt.genSalt(); const hashPassword = await bcrypt.hash(password, salt); const user = this.create({ username, password: hashPassword, status }); await this.save(user); return user; } }
JWT
JWTとは
- JSON Web Token
- Json形式の認証
- 電子署名で改ざんの検知が可能
- 認証用のトークンとして利用される
JWTの構成
- 3つの要素で構成
- ヘッダ:ハッシュアルゴリズムなどメタデータ
- ペイロード:認証対象の情報。ユーザー名やIDなど任意の情報
- 署名:ヘッダとペイロードをエンコードしたものに秘密鍵を加えてhash化したもの
- 要素ごとにBase64される
- 3つの要素が「.」で結合される
JWTを使った認証
- JWTの取得
- 新規作成やログインなどの機能でサーバーにユーザー名やパスワード等を送る
- サーバーが認証情報を検証して
- 問題なければ秘密鍵を使ってTokenを生成する
- レスポンスとして送信
- ユーザー側はローカルストレージ等に保存する
- JWTの認証
- ユーザー側がリクエストのAuthorizationヘッダーに追加してサーバーに送信
- サーバーでトークンを検証
- ユーザーに返す
JWTのメリット
- 署名が含まれているため改ざんのチェックが可能
- 有効期限を設けてセキュアなToken発行が可能
- 状態をサーバーで管理する必要がない
- 任意のデータをTokenに含めることが可能
JWTモジュールの設定
- ライブラリのインストール
npm i --save @nestjs/jwt @nestjs/passport passport passport-jwt npm i --save-dev @types/passport-jwt
- モジュールに登録
- AuthモジュールのimportにPassportModuleを追加する。
- AuthモジュールのimportにJwtModuleを追加する。
@Module({ imports: [ TypeOrmModule.forFeature([UserRepository]), PassportModule.register({ defaultStrategy: 'jwt' }), // デフォルトの認証情報をJWTに設定 JwtModule.register({ // JWTの設定 secret: 'secretKey123', // 秘密鍵。本来は環境変数などに設定して外部にもらさないこと。 signOptions: { expiresIn: 3600, // トークンの有効期限(秒) }, }), ], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}
ログイン機能の実装
- dtoクラスの作成
src/auth/credentials.dto.ts
import { IsString, MaxLength, MinLength, IsNotEmpty } from "class-validator"; export class CredentialsDto { @IsString() @IsNotEmpty() username: string; @IsString() @MinLength(8) @MaxLength(32) password: string; }
- authサービスでJwtServiceをDIし利用
/src/auth/auth.service.ts
import { CredentialsDto } from './dto/credentials.dto'; import { User } from 'src/entities/user.entity'; import { UserRepository } from './user.repository'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor( private UserRepository: UserRepository, private JwtService: JwtService, ) {} // 省略。。。 async signIn( credentialsDto: CredentialsDto ): Promise<{ accessToken: string }> { const { username, password } = credentialsDto; // credentialsDtoを展開 const user = await this.UserRepository.findOne({ username }); // usernameからユーザーを取得 // パスワードの比較 // bcryptにより、平文のパスワードとハッシュ値を比較することができる if (user && (await bcrypt.compare(password, user.password))) { // JWTを生成 const payload = { id: user.id, username: user.username }; const accessToken = await this.JwtService.sign(payload); // 署名されたtoken return { accessToken }; } // ユーザーが見つからない場合はエラーを返す throw new UnauthorizedException( 'ユーザー名やパスワードを確認してください' ); } }
- Controllerに実装
/src/auth/auth.controller.ts
@Post('/signin') async signin( @Body() credentialsDto: CredentialsDto ): Promise<{ accessToken: string }> { return await this.authService.signIn(credentialsDto); }
JWTの確認・デバッグ
-
jwt.ioにアクセスしてデバッグ
- Postmanなどで動作確認してアクセストークンを生成
- エンコードの欄に生成したアクセストークンを張り付けて確認を行う。
- VERIFY SIGNATUREの部分に秘密鍵に指定した値(AuthモジュールのJwtServiceの部分で設定)を張り付けて確認
JWT認証の実装
■2つのステップがある。
- リクエストからJWTを受け取り検証・認証
- その認証処理を認証が必要な部分に実装
■PR
■手順
- jwt.strategy.tsを実装
src/auth/jwt.strategy.ts
import { UserRepository } from './user.repository'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { User } from 'src/entities/user.entity'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private userRepository: UserRepository) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearerトークンを取得 ignoreExpiration: false, // 期限切れのトークンを拒否 secretOrKey: 'secretKey123', // 秘密鍵 }) } async validate(payload: { id: number, username: string }): Promise<User> { const { id, username } = payload; const user = await this.userRepository.findOne({ id, username }); // 認証に成功した場合はユーザー情報を返す if(user) { return user; } throw new UnauthorizedException(); } }
- strategyをAuthモジュールに登録
src/auth/auth.module.ts
// 省略 providers: [AuthService, JwtStrategy], exports: [JwtStrategy], // JwtStrategyを他のモジュールで使えるようにする // 省略
- gaurdを実装
src/auth/guards/jwt-auth.guards.ts
import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
- guardsをAuthモジュールに登録
src/auth/auth.module.ts
// 省略 providers: [AuthService, JwtStrategy, JwtAuthGuard], exports: [JwtStrategy, JwtAuthGuard], // JwtStrategyとJwtAuthGuardを他のモジュールで使えるようにする // 省略
- 利用するモジュールで設定
- 利用先のモジュールでimport
- controllerかcontroller内のメソッドに
@UseGuards(JwtAuthGuard)
を記載
Jwt認証を利用した機能の実装(リクエストからユーザーを取得する)
-
エンティティの更新
*Userと関連付けする場合のみ
今回はUserと商品を関連付けする。src/entities/user.entity.ts// 省略 // Itemエンティティと1対多の関係を定義 @OneToMany(() => Item, (item) => item.user) items: Item[];
src/entities/item.entity.ts// 省略 @ManyToOne(() => User, (user) => user.items) user: User; @Column userId: string;
-
migrationを行う
npx typeorm migration:generate -n AddRelation npx typeorm migration:run
-
カスタムデコレーターを実装
src/auth/decorators/items.decorator.tsimport { ExecutionContext, createParamDecorator } from "@nestjs/common"; export const GetUser = createParamDecorator((_, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; });
-
コントローラーでデコレーターを記述してユーザーを取得
src/items/items.controller.ts@Post() @UseGuards(JwtAuthGuard) async create( @Body() createItemDto: CreateItemDto, @GetUser() user: User, ): Promise<Item> { return await this.itemsService.create(createItemDto, user); }
参考
Udemy「NestJS入門 TypeScriptではじめるサーバーサイド開発」