3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Nest.jsで認証機能を実装するメモ

Posted at

実装手順の概要

  1. Authモジュールのセットアップ
    nest g module auth
    nest g controller auth
    nest g service auth
    
  2. userエンティティを作成
  3. 以下コマンドでマイグレーション
    npx typeorm migration:generate -n createUser
    
    npx typeorm migration:run  
    
  4. 実装
    1. dtoクラスを作成・実装
    2. UserRepositoryを作成・実装
    3. RepositoryをAuthModuleにimportする
    4. Service実装・実装
    5. Controllerを実装

パスワードなどをhash化する

  1. ライブラリのインストール
    npm i --save bcrypt
    npm i --save-dev @types/bcrypt
    
  2. 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を使った認証

  1. JWTの取得 
    1. 新規作成やログインなどの機能でサーバーにユーザー名やパスワード等を送る
    2. サーバーが認証情報を検証して
    3. 問題なければ秘密鍵を使ってTokenを生成する
    4. レスポンスとして送信
    5. ユーザー側はローカルストレージ等に保存する
  2. JWTの認証
    1. ユーザー側がリクエストのAuthorizationヘッダーに追加してサーバーに送信
    2. サーバーでトークンを検証
    3. ユーザーに返す

JWTのメリット

  • 署名が含まれているため改ざんのチェックが可能
  • 有効期限を設けてセキュアなToken発行が可能
  • 状態をサーバーで管理する必要がない
  • 任意のデータをTokenに含めることが可能

JWTモジュールの設定

  1. ライブラリのインストール
    npm i --save @nestjs/jwt @nestjs/passport passport passport-jwt
    
    npm i --save-dev @types/passport-jwt
    
  2. モジュールに登録
    • 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 {}
    

ログイン機能の実装

  1. 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;
    }
    
  2. 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(
                'ユーザー名やパスワードを確認してください'
            );
        }
    }
    
    
  3. 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つのステップがある。

  1. リクエストからJWTを受け取り検証・認証
  2. その認証処理を認証が必要な部分に実装

■PR

■手順

  1. 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();
        }
    }
    
  2. strategyをAuthモジュールに登録
    src/auth/auth.module.ts
    // 省略
    
    providers: [AuthService, JwtStrategy],
    exports: [JwtStrategy], // JwtStrategyを他のモジュールで使えるようにする
    
    // 省略
    
  3. 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') {}
    
  4. guardsをAuthモジュールに登録
    src/auth/auth.module.ts
    // 省略
    
    providers: [AuthService, JwtStrategy, JwtAuthGuard],
    exports: [JwtStrategy, JwtAuthGuard], // JwtStrategyとJwtAuthGuardを他のモジュールで使えるようにする
    
    // 省略
    
  5. 利用するモジュールで設定
    1. 利用先のモジュールでimport
    2. controllerかcontroller内のメソッドに@UseGuards(JwtAuthGuard)を記載

Jwt認証を利用した機能の実装(リクエストからユーザーを取得する)

  1. エンティティの更新
    *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;
    
  2. migrationを行う

    npx typeorm migration:generate -n AddRelation
    npx typeorm migration:run
    
  3. カスタムデコレーターを実装

    src/auth/decorators/items.decorator.ts
    import { ExecutionContext, createParamDecorator } from "@nestjs/common";
    
    export const GetUser = createParamDecorator((_, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        return request.user;
    });
    
  4. コントローラーでデコレーターを記述してユーザーを取得

    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ではじめるサーバーサイド開発」

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?