오늘은 nest에서의 passport에 대해서 적어보려고 한다.
우선 guard파일과 strategy 파일이 존재한다.
문제가 생긴 시점은
커스텀 guard파일을 사용 중이었는데, 로직이 실행되면서 strategy파일을 경유하는 것이 정상적인 진행이었으나, strategy파일로 넘어가지 않는 것을 발견했다.
문제가 생겼던 코드 예시이다.
@Injectable()
export class AuthGuard extends NestAuthGuard('jwt') {
constructor(
private readonly authService: AuthService,
private readonly usersService: UsersService,
) {
super();
}
async canActivate(context: ExecutionContext) {
//내부 로직
return;
}
}
해당 파일을 실행 할 경우, 내부 로직은 작동을 했으나, strategy 파일로 넘어가지 않는 것을 확인했다.
이것저것 만져도 보고 했는데 해결이 안되어서 한참 헤매다가 결국 커스텀 guard를 사용하면 passport 내장 guard가 실행되지 않는 것 같다는 결론에 도달했다.
그렇게 canActivate부분을 지우고 strategy파일에서 로직을 주려고 했는데, 마지막으로 더 찾아보자고 한 글에서 guard와 strategy를 동시에 사용하고 있는 것을 찾았다.
남들도 쓰는데 포기할 수 없어서 조금 더 뜯어보다가, 생각이 passport에서 제공하는 guard와 strategy에 미쳐서 찾아보게 되었다.
결국 passport에 존재하는 내장 guard와 내장 strategy파일을 찾았는데,
혹시 필요한 사람을 위해서 경로를 남겨본다.
node_modules > passport-jwt > strategy.js > JwtStrategy(내장 전략파일)
node_modules > auth > security > jwt.guard.ts(내장 가드)
이리저리 console.log()를 해본 결과 파악한 흐름은 다음과 같다.
현재 컨트롤러에서 UseGuard(AuthGuard)를 통해서 커스텀 Guard를 실행시킨다.
그럼 커스텀 Guard의 canAtivate함수 내부를 타고 돌게 되는데 그 상태에서 return되는 것으로 로직이 종료된다.
더 찾아본 결과 커스텀 Guard의 canAtivate로직이 끝나면 내장 Guard의 canAtivate를 실행시켜주어야 했다.
일반적으로는 아래와 같은 형식을 띄었다.
canActivate(context: ExecutionContext) {
//내부 로직
return super.canActivate(context);
}
하지만 우리 코드의 경우 내부에서 비동기로 실행해야할 상황이 존재했고, async 선언을 해주어서 return 부분에서 타입 오류로 super.canActivate(context)가 들어가지 않았다. (await를 붙여보아도 마찬가지)
그래서 반환할 때 실행하는 것이 아니라, 조금 미리 실행을 시켜주기로 했다.
async canActivate(context: ExecutionContext) {
//내부 로직
await super.canActivate(context)
return;
}
그랬더니!! 커스텀 strategy의 validate 함수는 여전히 실행되지 않았다...
다만 내장 guard에서 로그가 찍힌 것을 확인 할 수 있었다.
내장 guard를 살펴보니 defultStrategy라는 것을 실행하는 부분이 존재했다. 여기서 defultStrategy를 어떻게 찾아올까 고민을 했었는데, 커스텀 strategy와 커스텀 guard에서 상속을 받는 부분에 'jwt'를 동일하게 써줌으로써 연관관계를 지어줄 수 있었다.
이어서 내장 strategy로 이동하여 로그를 찍어보았는데, 토큰 인증 부분이 정상작동하지 않는 것을 확인했다.
베어러 토큰을 사용하겠다고 선언은 했었는데, 곰곰히 생각해보니 리프레쉬 토큰을 사용하고 싶어서 쿠키에 담긴 토큰이 두개인 상황이었고 따라서 쿠키를 반환받을 때, 두개의 토큰이 같이 들어간 string을 반환받은 것 같았다.
그래서 커스텀 strategy에서 super부분에 커스텀을 조금 해주었다.
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private usersService: UsersService) {
//이 아래 부분
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: any) => {
return request?.cookies['AccessToken'].split(' ')[1];
},
]),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET_ACCESS,
passReqToCallback: true,
});
}
async validate(payload: Payload) {
const user = await this.usersService.tokenValidateUser(payload);
if (!user) {
throw new UnauthorizedException('존재하지 않는 사용자 입니다.');
}
return user;
}
}
쿠키에서 엑세스 토큰 값만 분리하여 내장 strategy에서 엑세스 토큰만 판별할 수 있도록 구성해주었다.
결과는 ....
성공적이었다.
엑세스토큰이 만료되지 않고 정상적인 상황에서 원하는 흐름인 커스텀 Guard -> 내장 Guard -> 내장 strategy -> 커스텀 strategy.validate 의 순서대로 진행되는 것을 확인 할 수 있었다.
이렇게 끝이라면 좋겠지만...
리프레쉬 토큰을 구현하고 싶었기 때문에, 엑세스 토큰이 만료된 상황에서 추가적으로 리프레쉬 토큰을 검증하여 엑세스토큰을 발급하는 과정이 필요했다.
이 부분에서 필요했던 것이 handleRequest였다.
로직의 흐름을 다시한번 기술하자면
엑세스 토큰이 유효할 경우
->
커스텀 Guard -> 내장 Guard -> 내장 strategy -> 커스텀 strategy
엑세스 토큰이 유효하지 않을 경우
커스텀 Guard -> 내장 Guard -> 내장 strategy -> 커스텀 Guard의 handleRequest
의 흐름을 가진다.
즉 엑세스 토큰이 유효하지 않을 때, 리프레쉬 토큰 관련 로직을 수행하기 위해서 handleRequest 부분에 작성을 해주어야 했다.
handleRequest는 err, user, info, context의 4개의 인자값을 받는데,
내장 strategy에서 로그를 추적해 본 결과,
엑세스 토큰이 유효하지 않을 때(만료), info에 값이 담겨서 넘어오는 것을 확인 했다.
따라서 handle안에 if(info){}를 통하여 로직을 작성해주었다.
여기서 또 문제가 발생했는데...
위에서 말한 경로대로 내장 guard를 들어가보면 알겠지만,
canActivate의 경우 async로 선언되어 있다.
그런데 handelRequest의 경우 async 처리되어있지 않으며, d.ts파일로 들어가서 확인해 본 결과 promise타입을 지원하지 않고 있었다.
문제는 토큰 검증/발급 과정에서 비동기 처리를 해야할 상황이었다는 점이었다.
당연히 커스텀 guard의 handelRequest에서 async처리를 하자 타입에러가 발생하였다.
고민을 하다가...
내장 Guard에서 함수를 추가하면 되는거아냐?!?!
라는 생각이 들어서
새로 async 메서드를 선언해주고 기존 defultStrategy를 실행한 후 검증 실패 시에handleRequest로 넘기는 부분에서 새로 생성한 메서드로 넘어가도록 해주었다.
그리고 원활한 사용을 위해 내장 guard의 d.ts파일에서 promise타입을 지원하도록 처리하였다.
그리고 커스텀 guard에서 handleRequest 대신 새로 생성한 메서드로 작성을 하자 문제 없이 성공하였다.
근데.... 또... 문제가...
혼자 성공했다고 좋아했었는데, 생각을 해보니 passport는 라이브러리고 결국 git에 올리거나 ec2에 배포를 해야하는데, npm i로 설치를 하고서 각 사용자마다 내장 guard로 찾아가서 메서드를 생성해준다는게... 사실 상 불가능하다는 것을 깨달았다.
그래서 내장 guard의 메서드 생성은 취소하고 handleRequest를 비동기적으로 사용하는 방법을 고민하다가, await가 필요한 부분에서 then으로 처리를 하면 되지 않을까 하여 시도해보았으나, 표면적인 로직은 같지만 then은 말그대로 결과가 성공했을 때 다음 로직 실행/ await의 경우 이벤트에 따로 담아 실행(node를 시작 했을 때, 관련하여 내용을 얼핏 본 기억은 나는데 깊게 알아보진 못하였다.)의 방식 차이 탓인지 특정 부분에서 원하는대로 작동하지 않는 것을 발견했다.
그래서 then도 포기하고, 결국 콜백함수로 해결했다.
handleRequest 내부에서 콜백함수를 통해 async를 선언하여 원하는 곳에 await 사용이 가능해졌고,
원하는 방식대로 작동하는 것을 확인 할 수 있었다.
handleRequest<TUser = any>(
err: any,
user: any,
info: any,
context: ExecutionContext,
): any {
return (async () => {
if (err) {
throw new UnauthorizedException('AUTH', 'JWT AUTH ERROR');
}
if (info) {
//리프레쉬 토큰 관련 로직
}
})();
}
중간에 포기하고 로직의 위치를 바꿀까 하다가,
오기가 생겨서 결국 passport에서 제공하는 함수도 파보고, guard / strategy 의 흐름에 대해서 직접 비교/체험 해보면서 좀 더 명확하게 느낄 수 있었다.
시간은 엄청 잡아먹었지만, 재밌었다 ㅋㅋ!