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

## 実装手順の概要
1. Authモジュールのセットアップ
    ```bash
    nest g module auth
    nest g controller auth
    nest g service auth
    ```
1. userエンティティを作成
1. 以下コマンドでマイグレーション
    ```bash
    npx typeorm migration:generate -n createUser

    npx typeorm migration:run  
    ```
1. 実装
    1. dtoクラスを作成・実装
    1. UserRepositoryを作成・実装
    1. RepositoryをAuthModuleにimportする
    1. Service実装・実装
    1. Controllerを実装


## パスワードなどをhash化する
1. ライブラリのインストール
    ```bash
    npm i --save bcrypt
    npm i --save-dev @types/bcrypt
    ```
2. Repositoryでhash化を実装
    以下は実装例。パスワードをhash化している。
    ```TypeScript
    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される
- ３つの要素が「.」で結合される

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

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

### JWTモジュールの設定
1. ライブラリのインストール
    ```bash
    npm i --save @nestjs/jwt @nestjs/passport passport passport-jwt

    npm i --save-dev @types/passport-jwt
    ```
2. モジュールに登録
    - AuthモジュールのimportにPassportModuleを追加する。
    - AuthモジュールのimportにJwtModuleを追加する。
    ```TypeScript
    @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クラスの作成
    ```TypeScript: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し利用
    ```TypeScript:/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に実装
    ```TypeScript:/src/auth/auth.controller.ts
    @Post('/signin')
    async signin(
        @Body() credentialsDto: CredentialsDto
    ): Promise<{ accessToken: string }> {
        return await this.authService.signIn(credentialsDto);
    }
    ```

### JWTの確認・デバッグ
- [jwt.io](https://jwt.io/)にアクセスしてデバッグ
    - Postmanなどで動作確認してアクセストークンを生成
    - エンコードの欄に生成したアクセストークンを張り付けて確認を行う。
        - VERIFY SIGNATUREの部分に秘密鍵に指定した値（AuthモジュールのJwtServiceの部分で設定）を張り付けて確認

### JWT認証の実装
■2つのステップがある。
1. リクエストからJWTを受け取り検証・認証
1. その認証処理を認証が必要な部分に実装

■PR


■手順
1. jwt.strategy.tsを実装
    ```TypeScript: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();
        }
    }
    ```
1. strategyをAuthモジュールに登録
    ```typescript:src/auth/auth.module.ts
    // 省略
    
    providers: [AuthService, JwtStrategy],
    exports: [JwtStrategy], // JwtStrategyを他のモジュールで使えるようにする

    // 省略
    ```
3. gaurdを実装
    ```Typescript: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モジュールに登録
    ```typescript:src/auth/auth.module.ts
    // 省略

    providers: [AuthService, JwtStrategy, JwtAuthGuard],
    exports: [JwtStrategy, JwtAuthGuard], // JwtStrategyとJwtAuthGuardを他のモジュールで使えるようにする

    // 省略
    ```
5. 利用するモジュールで設定
    1. 利用先のモジュールでimport
    1. controllerかcontroller内のメソッドに`@UseGuards(JwtAuthGuard)`を記載

## Jwt認証を利用した機能の実装（リクエストからユーザーを取得する）
1. エンティティの更新
    ＊Userと関連付けする場合のみ
    今回はUserと商品を関連付けする。
    ```TypeScript:src/entities/user.entity.ts
    // 省略

    // Itemエンティティと1対多の関係を定義
    @OneToMany(() => Item, (item) => item.user)
    items: Item[];
    ```

    ```TypeScript:src/entities/item.entity.ts
    // 省略

    @ManyToOne(() => User, (user) => user.items)
    user: User;

    @Column
    userId: string;
    ```
2. migrationを行う
    ```bash
    npx typeorm migration:generate -n AddRelation
    npx typeorm migration:run
    ```
3. カスタムデコレーターを実装
    ```typescript: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. コントローラーでデコレーターを記述してユーザーを取得

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

https://www.udemy.com/course/nestjs-t/