개발일지

[NestJS] PASETO로 인증/인가 구현하기 본문

NestJS, Node.js/#02 NestJS

[NestJS] PASETO로 인증/인가 구현하기

lyjin 2022. 11. 25.

지난 포스트에서 PASETO에 대해 알아봤습니다. 이를 NestJS로 직접 구현해보고자 합니다. 전반적인 구현 방식은 JWT 인증/인가와 동일합니다. 가독성을 위해 DB 저장 등의 로직은 생략하고 PASETO 생성 및 인증 부분만 설명하겠습니다.


패키지 설치

🔗 https://paseto.io/

🔗 https://github.com/panva/paseto


$ yarn add paseto

 

v4, 비대칭키 방식(public)을 사용했습니다.

 


PASETO 생성

로그인 시 access token, refresh token을 생성합니다. 비대칭키 방식이기 때문에 한 쌍의 private/public 키 값이 필요합니다. 여기서는 paseto를 생성하는 부분이기 때문에 private key를 사용합니다.

 

import { V4 as paseto } from 'paseto';  // v4 사용

@Injectable()
export class TokenService {
    async createAuthToken({ userId }: Dto): {
        const payload = { sub: userId };

        try {
            const accessToken = await paseto.sign(payload, 'access-private-key', {
                expiresIn: '3m',
            });
            const refreshToken = await paseto.sign(payload, 'refresh-private-key', {
                expiresIn: '10m',
            });

            // logic...
            return { accessToken, refreshToken };
        } catch (error) {
            switch (error.message) {
                case 'invalid key provided':
                    throw new HttpException(
                        exceptionMessagesAuth.UNVERIFIED_SECRET_KEY,
                        400
                    );
                default:
                    throw new HttpException(
                        exceptionMessagesAuth.FAILED_CREATE_TOKEN,
                        400
                    );
            }
        }
    }
}

 


Access token 재발급

// class TokenService
{
    constructor(
        private readonly pasetoService: AuthPasetoService,
    ) {}
    ...

    async recreateAccessToken({refreshToken}: Dto) {
        const verifiedRefreshToken = await this.pasetoService.verifyPaseto(
            refreshToken,
            'RefreshToken'
        );
        const payload = { sub: verifiedRefreshToken['sub'] };

        try {
            const accessToken = await paseto.sign(
                payload, this.ACCESS_PRIVATE_KEY, {
                    expiresIn: '3m',
            });
            // logic...
            return { accessToken };
        } catch (error) {
            // throw error
        }
    }
}
 


PASETO 검증

AccessTokenGuard

사용자 검증을 위한 AccessTokenGuard를 생성합니다. 여기서는 paseto에 대한 검증이 이루어져야합니다.

 

// authUser.guard.ts

@Injectable()
export class AccessTokenGuard implements CanActivate {
    constructor(private readonly pasetoService: AuthPasetoService) {}

    async canActivate(context: ExecutionContext) {
        const request = context.switchToHttp().getRequest();
        const authorization = request.headers['authorization'];
        const token = String(authorization).split(' ')[1];

        if (!token) {
            throw new HttpException(
                exceptionMessagesAuth.TOKEN_NOT_EXISTS,
                400
            );
        }

        // paseto 검증
        const verifiedUser = await this.pasetoService.verifyPaseto(token);
        request['userId'] = verifiedUser.sub;

        return true;
    }
}
 

검증 하기

.verify()로 paseto 검증를 검증합니다. return 형식은 다음과 같습니다.

 

{
  sub: 'userId',  // payload
  iat: '2019-07-02T13:36:12.380Z',
  exp: '2019-07-02T15:36:12.380Z',
  aud: 'urn:example:client',
  iss: 'https://op.example.com'
}

 

토큰 생성 시 사용했던 private 키와 대응되는 public 키로 paseto를 검증합니다. guard에서는 access token 검증이, access token 재발급할 때에는 refresh token에 대한 검증이 이루어지기 때문에 type 파라미터를 추가로 받아 구분할 수 있도록 했습니다.

 

import { V4 as paseto } from 'paseto';

@Injectable()
export class AuthPasetoService {
    async verifyPaseto(
        token: string,
        type: TokenType = 'AccessToken'
    ){
        const publicKey =
            type == 'AccessToken'
                ? this.ACCESS_PUBLIC_KEY
                : this.REFRESH_PUBLIC_KEY;

        try {
            const verifiedToken = await paseto.verify(token, publicKey);

            return {
                sub: verifiedToken['sub'],
                iat: verifiedToken['iat'],
                exp: verifiedToken['exp'],
            };
        } catch (error) {
            switch (error.message) {
                case 'invalid key provided':
                    throw new HttpException(
                        exceptionMessagesAuth.UNVERIFIED_SECRET_KEY,
                        400
                    );
                case 'token is expired':
                    throw new HttpException(
                        exceptionMessagesAuth.EXPIRED_TOKEN,
                        400
                    );
                default:
                    throw new HttpException(
                        exceptionMessagesAuth.UNVERIFIED_TOKEN,
                        400
                    );
            }
        }
    }
}

 

 



키 생성하기

paseto 에서는 local 또는 public 키를 생성하는 .generateKey()를 제공합니다.

 

async createKey() {
    const { secretKey, publicKey } = await paseto.generateKey('public', {
        format: 'paserk',
    });

    return { secretKey, publicKey };
}