---
# System prepended metadata

title: NESTJS-JWT

---

# NESTJS-JWT
https://docs.nestjs.com/security/authentication
https://www.youtube.com/watch?v=uAKzFhE3rxU

## 需要的套件
```shell=
npm install --save @nestjs/passport
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
```
@types/passport, @types/passport-jwt是為了讓TypeScript能夠好好辨認他們的type

加密法不用bcrypt，因為我們要加密token，通常它的長度會非常的長
bcrypt的最長長度為72bytes(71 characters + 1 byte null terminator)
如果在長度超過72bytes的字串使用bcrypt，超出長度的字串將不會被加密到，導致只要前面72byte的字串相符，bcrypt就會通過比對
這在jwt是非常致命的，因為jwt的secret被存在最後段，而前面兩段的payload是可以透過他們官網解出來的

我們這裡用的是argon2，他的最大輸入長度是2^32-1 bytes
[Does bcrypt have a maximum password length?](https://security.stackexchange.com/questions/39849/does-bcrypt-have-a-maximum-password-length)
[Maximum input and output length for Argon2](https://stackoverflow.com/questions/55597857/maximum-input-and-output-length-for-argon2)

## refresh token的意義
一般的access token驗證時不必經過資料庫，也因此無法把它銷毀，若在過期之前有人竊取到了token便可以拿它做任何事，只能透過設定過期時間讓他失效
但設定的時間太短會影響使用體驗，太長則提高安全性風險
因此提出了refresh token的概念，讓access token的有效時間可以設的很短，在過期之後透過refresh token的驗證換發，如果refresh token被竊取，則因為refresh token會存在資料庫的關係，直接刪掉就可以了


## 前端儲存位置
https://dotblogs.com.tw/wasichris/2020/10/25/223728
- access token
  localStorage or in-memory or HttpOnly Cookie
- refresh token
  cookie(HttpOnly, Secure, SameSite)
  要特別挑出存放的位置，才不會發出每個request都帶有他
  https://stackoverflow.com/questions/57650692/where-to-store-the-refresh-token-on-the-client
  
  
## 流程
在這裡我做了4個API
- signup
  確認無重複email之後，把email跟hash過的密碼存到資料庫
  生成access token跟refresh token
  把hash過的refresh token存到資料庫
  回傳access token跟refresh token
- signin
  確認信箱跟密碼無誤
  生成access token跟refresh token
  把hash過的refresh token存到資料庫
  回傳access token跟refresh token
- logout
  確認access token的正確性
  把資料庫裡的refresh token刪除
- refresh
  確認refresh token的正確性
  生成access token跟refresh token
  把hash過的refresh token存到資料庫
  回傳access token跟refresh token

## JwtService

### initial
因為jwt可能會在很多個地方用到，所以把它放到common裡
在register裡面可以放一些共用的payload，他會跟我們之後傳進的payload合在一起
```typescript=
//common.module.ts
@Global()
@Module({
  imports: [JwtModule.register({})],
})
export class CommonModule {}
```

### Generate Token
```typescript=
//tokens.ts
export type tokens = {
  accessToken: string;
  refreshToken: string;
};

//jwt.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { tokens } from 'src/types/tokens';

@Injectable()
export class JWTService {
  constructor(private jwtService: JwtService) {}

  async getTokens(id: number, email: string): Promise<tokens> {
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        {
          id,
          email,
        },
        {
          secret: 'VeryLongStringForAccessToken',
          expiresIn: 60 * 15,
        },
      ),
      this.jwtService.signAsync(
        {
          id,
          email,
        },
        {
          secret: 'VeryLongStringForRefreshToken',
          expiresIn: 60 * 60 * 24 * 7,
        },
      ),
    ]);
    return {
      accessToken: accessToken,
      refreshToken: refreshToken,
    };
  }
}
```
用signAsync()生成token
在這個檔案裡決定了payload裡面要放什麼以及他的secret跟過期時間

### strategy
strategy決定了我們驗證完token之後要做什麼動作

```typescript=
//access-token.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';

export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'VeryLongStringForAccessToken',
    });
  }
  
  validate(payload: any) {
    return payload;
    //req.user = payload
  }
}
```

extends PassportStrategy的第二個參數是那個strategy的名字，這樣我們才能透過名字指定要用的strategy

在constructor裡有三個property
- jwtFromRequest
  決定我們如何得到token，在這裡用的是從header裡提取出來
  headerKey:Authorization
  headerValue:bearer \<token>
  在bearer跟token中間有一個空白，我們之後可以用split來提取
- ignoreExpiration
  在nest官網裡這個選項有被拿出來講，設為false代表token會過期，而預設就是false，所以可以不用特地寫出來
- secretOrKey
  jwt的secret，用來加密訊息
在validate裡面有return payload，在這裡因為我們使用的是express來處理http request，所以它會自動幫我們做req.user = payload的動作

```typescript=
//refresh-token.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';

@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'VeryLongStringForRefreshToken',
      passReqToCallback: true,
    });
  }

  validate(req: Request, payload: any) {
    const refreshToken = req.get('authorization').replace('Bearer', '').trim();
    return { ...payload, refreshToken };
    //req.user = { ...payload, refreshToken }
  }
}
```

基本上多了一個passReqToCallback，這是為了把request傳到validate這個method裡面
這樣我們就可以取得refresh token並拿回到controller用

## USER ENTITY
```typescript=
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  password: string;

  @Column({ nullable: true })
  refreshToken: string;
}
```

## ROUTES

### signup
```typescript=
//auth.controller.ts
@Post('/signup')
@HttpCode(HttpStatus.CREATED)
signup(@Body() dto: UserDto): Promise<tokens> {
  return this.authService.signup(dto);
}

//auth.service.ts
async signup(dto: UserDto): Promise<tokens> {
  const existingUser = await this.repo.findOneBy({ email: dto.email });
  if (existingUser) {
    throw new BadRequestException();
  }

  const hashedPassword = await argon.hash(dto.password);

  const user = await this.repo.create({
    email: dto.email,
    password: hashedPassword,
    refreshToken: '',
  });

  await this.repo.save(user);

  const tokens = await this.jwtService.getTokens(user.id, user.email);
  await this.updateRefreshToken(user.id, tokens.refreshToken);

  return tokens;
}
```
```json=
{
  "email": "a@email.com",
  "password": "noToken"
}
```
傳進信箱跟密碼，nest就會把他轉成dto
這裡可以不用做`refreshToken: ''`就可以存，只是我在操作的時候是用sqlite，他不給存null，而我也沒有特別去找解決方法

小發現:在user被create出來的時候，id是undefined
但在save之後，id會存進去user這個變數
因此就可以直接把id寫進payload裡了
最後回傳access token跟refresh token

### signin
```typescript=
//auth.controller.ts
@Post('/signin')
@HttpCode(200)
signin(@Body() dto: UserDto): Promise<tokens> {
  return this.authService.signin(dto);
}

//auth.service.ts
async signin(dto: UserDto): Promise<tokens> {
  const user = await this.repo.findOneBy({ email: dto.email });
  if (!user) {
    throw new ForbiddenException();
  }

  const isMatch = await argon.verify(user.password, dto.password);
  if (!isMatch) {
    throw new ForbiddenException();
  }

  const tokens = await this.jwtService.getTokens(user.id, user.email);
  await this.updateRefreshToken(user.id, tokens.refreshToken);

  return tokens;
}
```
帳號密碼錯誤的話，丟403 forbidden
假如信箱密碼都相符，則回傳access token跟refresh token

小發現:基本上使用到POST的API他的預設HttpStatus都是201
我們可以用nest的裝飾器"@HttpCode()"來改變這個API的HttpStatus

### logout
這邊要使用guard解析payload，因為payload裡面有id
```typescript=
//auth.controller.ts
@UseGuards(AuthGuard('jwt'))
@Post('/logout')
@HttpCode(200)
logout(@Req() req: Request) {
  const user = req.user;
  return this.authService.logout(user['id']);
}

//auth.service.ts
async logout(id: number) {
  return await this.repo.update(
    { id, refreshToken: Not('') },
    { refreshToken: '' },
  );
}
```
`@UseGuards(AuthGuard('jwt'))`代表使用名字為jwt的strategy來做jwt token認證
回到strategy這邊
```typescript=
//access-token.strategy.ts
...
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
  ...
  validate(payload: any) {
    return payload;
    //req.user = payload
  }
}
```
這裡講過return payload就是把payload放到req.user裡
因此原本在jwt token裡的payload被解析出來之後會被放到裡面
其中包還了id
因此在controller裡才能把id傳進去

而在service的update中，在教學影片裡是id符合且refresh token不為空的才會把空字串寫進去，他說這樣是為了防止有人spam logout的API進而造成資料庫一直被寫入的狀況

### refresh
```typescript=
//auth.controller.ts
@UseGuards(AuthGuard('jwt-refresh'))
@Post('/refresh')
@HttpCode(200)
refresh(@Req() req: Request) {
  const user = req.user;
  return this.authService.refresh(user['id'], user['refreshToken']);
}

//auth.service.ts
async refresh(id: number, refreshToken: string) {
  const user = await this.repo.findOneBy({ id });
  if (!user || !user.refreshToken) {
    throw new ForbiddenException();
  }

  const isMatch = await argon.verify(user.refreshToken, refreshToken);
  if (!isMatch) {
    throw new ForbiddenException();
  }

  const tokens = await this.jwtService.getTokens(user.id, user.email);
  await this.updateRefreshToken(user.id, tokens.refreshToken);

  return tokens;
}
```

再次回到strategy看

```typescript=
//refresh-token.strategy.ts
...
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
  ...
  validate(req: Request, payload: any) {
    const refreshToken = req.get('authorization').replace('Bearer', '').trim();
    return { ...payload, refreshToken };
    //req.user = { ...payload, refreshToken }
  }
}
```

## Custom Decorator
上面的作法`return this.authService.logout(user['id']);`TS無法檢測user['id']的型別
因此用custom decorator的ParamDecorator稍微加強型別判定

### Param decorator
```typescript=
//get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GetUser = createParamDecorator(
  (data: string, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return data ? user[data] : user;
  },
);

//auth.controller.ts
@UseGuards(RefreshTokenGuard)
@Post('/refresh')
@HttpCode(200)
refresh(
  @GetUser('id') id: number,
  @GetUser('refreshToken') refreshToken: string,
) {
  return this.authService.refresh(id, refreshToken);
}
```

在這邊，data就是我們傳進去的值
`@GetUser('refreshToken')`這段code我們傳進去的就是refreshToken這段string
通過ParamDecorator我們可以把request裡面key為該string的value回傳回去
然後在controller裡設定型別，便可以稍微增加嚴格性

### Metadata decorator
使用這個decorator可以設置metadata
可以放在個別的handler上或整個controller上
```typescript=
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Public = () => SetMetadata('isPublic', true);

// auth.controller.ts
@Public()
@Controller('auth')
export class AuthController {
  ...
}

// or

@Public()
@Post('/signup')
@HttpCode(201)
signup(@Body() dto: UserDto): Promise<tokens> {
  return this.authService.signup(dto);
}
```

## Custom JWT Guard
我們可能會針對payload裡面的內容做進一步的權限檢查或其他處理
所以有必要用到custom jwt guard
在custom guard裡可能會使用到reflector來取得metadata來作為判斷的標準
```typescript=
@Injectable()
export class AccessTokenGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
}
```

### Global Guard
access token guard會用在很多handler上
因此可以把它設為全域的guard
有兩種設法
1. 在main.ts裡加上`app.useGlobalGuards(new AccessTokenGuard());`
但只有這樣guard不能使用dependency injection
必須new一個reflector放到參數裡面他才能使用reflector
如果還有其他要放到constructor的東西就要一直new
```typescript=
//main.ts
...
bootstrap(){
  ...
  const reflector = new reflector();
  app.useGlobalGuards(new AccessTokenGuard(reflector))
}
```

2. 在app.module裡使用APP_GUARD
```typescript=
//app.module.ts
...
@Module({
  ...
  providers: [{ provide: APP_GUARD, useClass: AccessTokenGuard }],
})
export class AppModule {}
```
這樣就可以在要用的guard裡用dependency injection了

### 在特定的handler不使用Global Guard
在上面有寫到metadata decorator
我們可以透過metadata來辨別該handler該不該使用global guard
因為所有的route都設置access token guard的話，連登入都會被擋住
```typescript=
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';

@Injectable()
export class AccessTokenGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const isPublic = this.reflector.getAllAndOverride('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;
    }

    return super.canActivate(context);
  }
}
```

getAllAndOverride()的第一個參數是想要找的metadata，第二個參數是想要找的位置
context.getHandler() -> 從handler找
context.getClass() -> 從class找(即controller)

如果有找到，回傳true直接通過guard
沒找到的話用super執行原本的access token guard(即AuthGuard('jwt'))