데이터베이스를 어지럽히지 않고 새로 고침 토큰 자동 재사용 감지 구현

Node.js project에서 새로 고침 토큰 회전을 구현하는 방법을 연구하는 동안 Auth0: What Are Refresh Tokens and How to Use Them Securely에서 이 블로그 게시물을 방문했습니다. 새로 고침 토큰 자동 재사용 감지에 대해 설명하는 섹션에서 다음과 같이 말합니다.

The 🚓 Auth0 Authorization Server has been keeping track of all the refresh tokens descending from the original refresh token. That is, it has created a "token family".
Refresh Token Automatic Reuse Detection section



그러나 토큰이 손상되지 않고 애플리케이션이 많은 사용자에 의해 정기적으로 사용된다면 만료되기 전에 많은 비활성 새로 고침 토큰이 데이터베이스를 복잡하게 만들 것입니다.

솔루션



데이터베이스의 갱신 토큰 모델에 가족 속성을 추가할 수 있습니다. 이것은 Prisma ORM을 사용하는 내 모델입니다.

model UserTokens {
  id String @id @default(uuid())

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId String

  refreshToken String
  family       String   @unique
  browserInfo  String? // Show the user logged devices 
  expiresAt    DateTime
  createdAt    DateTime @default(now())
}


가족은 사용자가 로그인하고 새로운 새로 고침 토큰이 생성될 때 v4 UUID를 받습니다.
향후 새로 고침을 위해 tokenFamily가 새로 고침 토큰 페이로드에 추가됩니다.

다음 코드 스니펫에서 NestJS frameworkTypeScript을 사용하고 있습니다.

  /** Creates the refresh token and saves it in the database */
  private async createRefreshToken(
    payload: {
      sub: string;
      tokenFamily?: string;
    },
    browserInfo?: string,
  ): Promise<string> {
    if (!payload.tokenFamily) { 
      payload.tokenFamily = uuidV4();
    }

    const refreshToken = await this.jwtService.signAsync(
      { ...payload },
      refreshJwtConfig,
    );

    await this.saveRefreshToken({
      userId: payload.sub,
      refreshToken,
      family: payload.tokenFamily,
      browserInfo,
    });

    return refreshToken;
  }


이제 refreshToken을 생성하고 저장했으므로 이를 사용하여 accessToken을 새로 고치고 현재 refreshToken을 회전할 수 있습니다. 그러나 먼저 유효성을 검사해야 합니다.

  /** Checks if the refresh token is valid */
  private async validateRefreshToken(
    refreshToken: string,
    refreshTokenContent: RefreshTokenPayload,
  ): Promise<boolean> {
    const userTokens = await this.prismaService.userTokens.findMany({
      where: { userId: refreshTokenContent.sub, refreshToken },
    });

    const isRefreshTokenValid = userTokens.length > 0;

    if (!isRefreshTokenValid) {
      await this.removeRefreshTokenFamilyIfCompromised(
        refreshTokenContent.sub,
        refreshTokenContent.tokenFamily,
      );

      throw new InvalidRefreshTokenException();
    }

    return true;
  }

  /** Removes a compromised refresh token family from the database
   *
   * If a token that is not in the database is used but it's family exists
   * that means the token has been compromised and the family should me removed
   *
   * Refer to https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation#automatic-reuse-detection
   */
  private async removeRefreshTokenFamilyIfCompromised(
    userId: string,
    tokenFamily: string,
  ): Promise<void> {
    const familyTokens = await this.prismaService.userTokens.findMany({
      where: { userId, family: tokenFamily },
    });

    if (familyTokens.length > 0) {
      await this.prismaService.userTokens.deleteMany({
        where: { userId, family: tokenFamily },
      });
    }
  }


토큰이 유효하지 않지만 패밀리가 존재하는 경우 이는 원래 refreshToken에서 내려오는 토큰이므로 패밀리가 손상되었으므로 제거해야 합니다.

결론



새로 고침 토큰 회전 자동 재사용 감지를 구현하려면 원본 새로 고침 토큰을 모두 저장하지 않고 데이터베이스 모델에서 tokenFamily 속성을 만들고 등록되지 않은 자손을 확인할 수 있습니다.
이 기사에서 전체 인증 프로세스를 구현한 방법에 대해 자세히 설명하지 않았지만 원하는 경우 project's repository in GitHub에서 소스 코드를 확인할 수 있습니다.

좋은 웹페이지 즐겨찾기