import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
import { User} from 'src/entities/user.entity';
import { Role } from 'src/common/common.enum';
import { Repository } from 'typeorm';
import { ROLES_KEY } from '../roles/back-office-roles.decorator';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
private reflector: Reflector,
@InjectRepository(User) private readonly userRepository: Repository<User>,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
const [user] = await this.userRepository.find({
where: { id: user.id },
take: 1,
});
return user.isAdmin;
}
}
일반적으로 Local과 JWT를 사용하여 전략을 수립하고, 그에 따라 Guard를 만든다. JWT 자체는 쉽게 decode가 가능하기는 하지만, 그걸 대비해 내부의 식별 값을 하나 더 둔다. 임의로 값을 바꿀 경우, 이 식별 값도 함께 바뀌기 때문에, Token을 만들 당시 사용한 secret 값을 모른다면 변조하는 것은 의미가 없다. 변조 자체는 막을 수 없지만, 변조가 의심되는 경우 기존 Token을 폐기처분하기 때문이다. 그래서 일단 안전하다.
하지만 변조가 불가능하다는 것은 아니고, 뚫릴 수 있는 여지가 있는 방법이기에, 애초에 JWT에는 노출되면 위험한 값을 두지 않고, 편의 상 제공한 값이라고 하더라도 핵심 로직에서는 반드시 검증을 해주어야 한다.
이런 경우를 대비해 직접 Custom한 Guard의 코드를 올린다.
CanActive를 구현하여 만든 AdminGuard는 canActivate를 필수적으로 구현해야 하는데, 이 안에서 인증 로직이 동작하게 된다. Passport를 사용하는 것이 아니기 때문에 컨트롤러에서 지정한 metadata와 비교해, 해당 API를 동작시키기 위해 필요한 권한을 체크하는 방식이다. 여기서는 user의 id를 DB에서 다시 확인해 데이터를 가져오는 간단한 동작이지만, 더욱 복잡하게 하여, 이 유저가 정말 관리자가 맞는지, 이 유저가 검증하는 로직을 만들 수 있다.
사실, 이렇게 Promise를 반환하는 함수를 만들기보다 refresh token을 구현하는 게 서버 입장에서는 더 유리할 거라고 생각하지만, 구현의 난이도를 볼 때는 이게 더 간편하기는 하다. 필요에 따라, 우선순위에 따라, 일단은 이렇게 구현해두고 넘어가는 것도 나쁘지 않겠다.
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
참고로 컨트롤러에서 API에 metadata를 넣는 것은 SetMetadata라는 직관적인 이름의 데코레이터를 사용한다. 축약을 위해 Role 데코레이터를 만들기도 한다.