Arquitetura

A estratégia mais segura combina dois tokens:

  • Access Token: JWT de curta duração (15min), enviado no header Authorization
  • Refresh Token: Token opaco de longa duração (7 dias), armazenado em HTTP-only cookie

Quando o access token expira, o frontend faz uma requisição silenciosa com o refresh token para obter um novo par de tokens.

Backend: Gerar Tokens

class AuthService {
  generateTokens(userId: string) {
    const accessToken = jwt.sign(
      { sub: userId },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    const refreshToken = crypto.randomBytes(64).toString('hex');

    // Salvar hash do refresh token no banco
    await this.prisma.refreshToken.create({
      data: {
        token: await bcrypt.hash(refreshToken, 10),
        userId,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      },
    });

    return { accessToken, refreshToken };
  }
}

Backend: Endpoint de Refresh

@Post('refresh')
async refresh(@Req() req, @Res() res) {
  const refreshToken = req.cookies['refresh_token'];
  if (!refreshToken) throw new UnauthorizedException();

  const stored = await this.prisma.refreshToken.findFirst({
    where: { userId: req.user?.id, expiresAt: { gt: new Date() } },
  });

  if (!stored || !(await bcrypt.compare(refreshToken, stored.token))) {
    throw new UnauthorizedException();
  }

  // Rotation: invalidar token antigo, gerar novo par
  await this.prisma.refreshToken.delete({ where: { id: stored.id } });
  const tokens = await this.generateTokens(stored.userId);

  res.cookie('refresh_token', tokens.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000,
    path: '/api/auth/refresh',
  });

  return res.json({ access_token: tokens.accessToken });
}

Frontend: Interceptor Angular

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.accessToken();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req).pipe(
    catchError(error => {
      if (error.status === 401 && !req.url.includes('refresh')) {
        return authService.refreshToken().pipe(
          switchMap(newToken => {
            const retryReq = req.clone({
              setHeaders: { Authorization: `Bearer ${newToken}` },
            });
            return next(retryReq);
          }),
        );
      }
      return throwError(() => error);
    }),
  );
};

Checklist de Segurança

  • Access token curto (15min max)
  • Refresh token em HTTP-only cookie (não acessível via JS)
  • Flag Secure em produção (HTTPS only)
  • SameSite=Strict para prevenir CSRF
  • Rotation de refresh token a cada uso
  • Limitar path do cookie ao endpoint de refresh
  • Salvar apenas hash do refresh token no banco