📌

Nest.js で Cookie 認証API を実装する

2022/03/18に公開

Nest.js で 認証用の API を実装していきます。

構成

  • Nest.js
  • Postgres

作成する 認証向けの REST API では、
SPA での利用を考慮し Cookie ベースの認証を採用しています。

作成するエンドポイント

  • GET /api/profile
  • POST /api/signup
  • POST /api/login

インストール

Nest.js のプロジェクトに以下のモジュールをインストールします。

.env を用いた環境構築のために @nestjs/config を利用します。

$ npm i @nestjs/config

Cookie 認証を利用するために express-session を利用します。

$ npm i express-session

DB に Postgres を利用するために typeorm と pg を導入します。

$ npm i @nestjs/typeorm typeorm pg

DB のセットアップ

typeorm.ts を作成してプロジェクトをセットアップします。

// need for typeorm cli
require('dotenv').config();

const config = {
    type: 'postgres',
    url: process.env.DATABASE_URL,
    entities: [
        "./dist/src/entities/*.js"
    ],
    migrations: [
        "./dist/src/migrations/*.js"
    ],
    cli: {
        migrationsDir: './src/migrations',
    },
}
module.exports = config

src/entities フォルダを作成して、user.entity.ts を作成します。

import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";

@Entity("t_users")
export class UserEntity {

    @PrimaryGeneratedColumn("uuid")
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    @Index({unique:true})
    email: string;

    @Column()
    password: string;
}

Entity を用意したあとで、migration:generate を実行すると、migration ファイルが作成されます。

$ npx ts-node ./node_modules/.bin/typeorm migration:generate -n UserTables

migration:run を実行して、データベーステーブルを作成します。

$ npx ts-node ./node_modules/.bin/typeorm migration:run 

会員登録の実装

会員登録の流れは以下のとおりです。

  • 会員データを DB に登録
  • セッションにユーザデータを格納

Nest.js におけるセッションのセットアップは以下を確認してください。

https://zenn.dev/mikakane/articles/nestjs_session_typeorm

session 管理用の session.service.ts を作成し、app.module.ts に登録します。

import {Inject, Injectable, Scope} from "@nestjs/common";
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import {UserEntity} from "./entities/user.entity";

@Injectable({ scope: Scope.REQUEST })
export class SessionService {

  constructor(@Inject(REQUEST) private request: Request) {}

  private session(): any{
    return this.request.session
  }
  
  get user():UserEntity{
    return this.session().user
  }

  set user(user:UserEntity){
    this.session().user = user
  }
}

app.service.ts にユーザ作成用の signup 関数を追加します。

import {Injectable} from '@nestjs/common';
import {Connection, getConnection} from "typeorm";
import {UserEntity} from "./entities/user.entity";

@Injectable()
export class AppService {
  // ...
  async signup(
    email: string,
    password: string,
    firstName: string,
    lastName: string,
   ): Promise<any> {

    const connection: Connection = await getConnection();
    const user = new UserEntity()
    user.password = password //TODO hashed
    user.email = email
    user.firstName = firstName
    user.lastName = lastName
    await connection.manager.save(user)
    return {user}
  }
}

app.controller.ts に signup を追加します。

import {Body, Controller, Get, Post} from '@nestjs/common';
import { AppService } from './app.service';
import {UserEntity} from "./entities/user.entity";
import {SessionService} from "./session.service";

@Controller()
export class AppController {
  constructor(
      private readonly appService: AppService,
      private readonly session: SessionService,
  ) {}
  
  // ...

  @Post("signup")
  async signup(
    @Body() body
  ): Promise<any> {
    const {user} = await this.appService.signup(
        body.email,
        body.password,
        body.first_name,
        body.last_name
    );
    this.session.user = user

    return {
      user
    };
  }
}

curl で API を実行して、DB にデータが格納されれば成功です。

プロフィールの実装

プロフィール API では、以下の処理を実装します。

  • セッションにデータが格納されているかをチェック(Guard)
  • セッションのデータを返却

まずは、guard/auth.guard.ts を作成します。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import {SessionService} from "../session.service";

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private session: SessionService) {
  }

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const user = this.session.user
    if(user){
      // セッション内のユーザデータが信用できない運用のときはここでデータを更新する
      return true
    }else{
      return false
    }
  }
}

app.controller.ts に以下の形で login を追加します。

import {Body, Controller, Get, Post, Session, UseGuards} from '@nestjs/common';
// ...
import {AuthGuard} from "./guard/auth.guard";

@Controller()
export class AppController {
  //...
  @UseGuards(AuthGuard)
  @Get("/profile")
  async profile(){
    return {
      user: this.session.user
    }
  }
}

curl で signup 後に profile を叩いて、ユーザデータが取得できるかを確認します。
curl で cookie を有効にするには、以下のように -cb で クッキーファイルを指定します。

curl http://localhost:4200/api/profile -X GET -d {} -b cookie.txt -c cookie.txt

ログインの実装

最後に ログインの実装です。ログインAPIでは以下のような実装を追加します。

  • DB からユーザの取得
  • ユーザデータの返却

app.service.ts に以下のような形で login 関数を追加します。

//...
@Injectable()
export class AppService {
  // ...
  async login(email: string, password: string): Promise<any> {
      const connection: Connection = await getConnection();
      const user = await connection.manager.findOne(
        UserEntity,
        {
          email,password
        }
      )
      return user
  }
}

app.controller.ts に login 関数を追加します。

//...
@Controller()
export class AppController {
  //...
  @Post("/login")
  async login(@Body() body){
    const {user,auth} = await this.appService.login(
        body.email,
        body.password
    );
    this.session.user = user
    return {
      user: this.session.user
    }
  }
}

注記

記載しているコードは、実装のフローを紹介するためのサンプルコードです。

Discussion