DEV/TypeScript

JavaScript 서버에 TypeScript 적용하기 - 3-2. Express 유저 인증시스템 TypeScript로 마이그레이션(middleware 타입 적용)

develop-ch94 2026. 8. 5. 10:58

auth.ts

import { expressjwt }  from "express-jwt";
import reviewRepository from "../repositories/reviewRepository";
import {
  AuthenticationError,
  ForbiddenError,
  NotfoundError,
  ValidationError,
} from "../types/errors";
import { NextFunction, Request, Response } from "express";

function throwUnauthorizedError() {
  // 인증되지 않은 경우 401 에러를 발생시키는 함수
  const error = new AuthenticationError("Unauthorized");
  throw error;
}

async function verifySessionLogin(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  try {
    if (!req.isAuthenticated()) {
      throwUnauthorizedError();
    }
    next();
  } catch (error) {
    next(error);
  }
}

const verifyAccessToken = expressjwt({
  secret: process.env.JWT_SECRET!,
  algorithms: ["HS256"],
});

const verifyRefreshToken = expressjwt({
  secret: process.env.JWT_SECRET!,
  algorithms: ["HS256"],
  getToken: (req) => req.cookies.refreshToken,
});

async function verifyReviewAuth(
  req: Request<{ id: string }>,
  res: Response,
  next: NextFunction,
) {
  const { id: reviewId } = req.params;
  try {
    const review = await reviewRepository.getById(+reviewId);
    if (!review) {
      const error = new NotFoundError("Review not found");
      throw error;
    }
    
    // expressjJwt와 passport-jwt 둘 다 지원
    const userId = req.user?.id || req.auth?.userId;
    if (!userId) {
      const error new AuthenticationError("User not authenticated");
      throw error;
    }
    
    if (review.authorId !== userId) {
      const error = new ForbiddenError("Forbidden");
      throw error;
    }
    // 인증 성공 시 다음 미들웨어로 이동
    next();
  } catch (error) {
    // 에러 발생 시 에러 핸들러로 전파
    return next(error);
  }
}

function validateEmailAndPassword(
  req: Request<{}, {}, { email: string; password: string }>,
  res: Response,
  next: NextFunction,
) {
  const { email, password } = req.body;
  if (!email || !password) {
    const error = new ValidationError("email, password가 모두 필요합니다.");
    throw error;
  }
  next();
}
export default {
  verifySessionLogin,
  verifyAccessToken,
  verifyReviewAuth,
  verifyRefreshToken,
  validateEmailAndPassword,
};

errorHandler.ts

import { ErrorRequestHandler } from "express";

const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
  if (error.name === "UnauthorizedError") {
    res.status(401).send("invalid token...");
  }
  
  const status = error.code ?? 500;
  
  console.error(error);
  res.status(status).json({
    path: req.path,
    method: req.method,
    message: error.message ?? "Internal Server Error",
    data: error.data ?? undefined,
    date: new Date();
  });
};

export default errorHandler;

middlewares/passport/jwtStrategy.ts

import {
  Strategy as JwtStrategy,
  ExtractJwt,
  VerifyCallback,
} from "passport-jwt";
import userService from "../../services/userService";
import { Request } from "express";
import { DoneCallback } from "passport";

const accessTokenOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET!,
};

const cookieExtractor = function (req: Request) {
  var token = null;
  if (req && req.cookies) {
    token = req.cookies["refreshToken"];
  }
  return token;
};

const refreshTokenOptions = {
  jwtFromRequest: cookieExtractor,
  secretOrKey: process.env.JWT_SECRET!,
};

type JwtPayload = {
  userId: string;
};

async function jwtVerify(payload: JwtPayload, done: DoneCallback) {
  try {
    const user = await userService.getUserById(+payload.userId);
    if (!user) {
      return done(null, false);
    }
    return done(null, user);
  } catch (error) {
    return done(error);
  }
}

const accessTokenStrategy = new JwtStrategy(accessTokenOptions, jwtVerify);
const refreshTokenStrategy = new JwtStrategy(refreshTokenOptions, jwtVerify);

export default {
  accessTokenStrategy,
  refreshTokenStrategy,
};

middlewares/passport/localStrategy.ts

import { Strategy as LocalStrategy } from "passport-local";
import userService from "../../services/userService";

const localStrategy = new LocalStrategy(
  {
    usernameField: "email",
  },
  async (email, password, done) => {
    try {
      const user = await userService.getUser(email, password);
      if (!user) {
        return done(null, false);
      }
      return done(null, user);
    } catch (error) {
      return done(error); 
    }
  },
};

export default localStrategy;