OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 32 / 90
stack-analysis
CHAPTER 32 / 90
읽기 약 2
FUNCTION

인증 시스템: Passport + JWT + OAuth2


핵심 개념

JWT 발급·refresh token·OAuth2(Google/GitHub) — 프로덕션 인증 플로우.

본문

JWT 인증 — 액세스/리프레시 토큰

TYPESCRIPT📋 코드 (48줄)
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
const ACCESS_TTL = '15m';
const REFRESH_TTL = '7d';

export class AuthService {
  async login(email: string, password: string) {
    const user = await this.userRepo.findByEmail(email);
    if (!user) throw new UnauthorizedError('Invalid credentials');

    const ok = await bcrypt.compare(password, user.passwordHash);
    if (!ok) throw new UnauthorizedError('Invalid credentials');

    return this.issueTokens(user.id);
  }

  async signup(email: string, password: string, name: string) {
    const existing = await this.userRepo.findByEmail(email);
    if (existing) throw new ConflictError('Email exists');

    const passwordHash = await bcrypt.hash(password, 12);
    const user = await this.userRepo.create({ email, passwordHash, name });
    return this.issueTokens(user.id);
  }

  private issueTokens(userId: string) {
    const accessToken = jwt.sign({ sub: userId, type: 'access' }, ACCESS_SECRET, {
      expiresIn: ACCESS_TTL,
    });
    const refreshToken = jwt.sign({ sub: userId, type: 'refresh' }, REFRESH_SECRET, {
      expiresIn: REFRESH_TTL,
    });
    // refresh는 DB에 저장 (revoke 가능)
    this.refreshRepo.save(userId, refreshToken);
    return { accessToken, refreshToken };
  }

  async refresh(refreshToken: string) {
    const payload = jwt.verify(refreshToken, REFRESH_SECRET) as { sub: string };
    const exists = await this.refreshRepo.exists(payload.sub, refreshToken);
    if (!exists) throw new UnauthorizedError('Refresh token revoked');

    return this.issueTokens(payload.sub);
  }
}

인증 미들웨어

TYPESCRIPT📋 코드 (35줄)
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
    }
  }
}

export function authenticate(req: Request, res: Response, next: NextFunction) {
  const auth = req.headers.authorization;
  if (!auth?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  const token = auth.slice(7);

  try {
    const payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as any;
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

export function requireRole(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

OAuth2 (Passport - Google)

TYPESCRIPT📋 코드 (38줄)
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
  try {
    let user = await userRepo.findByGoogleId(profile.id);
    if (!user) {
      user = await userRepo.create({
        email: profile.emails![0].value,
        name: profile.displayName,
        googleId: profile.id,
      });
    }
    done(null, user);
  } catch (err) {
    done(err as Error);
  }
}));


// 라우트
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    const { accessToken, refreshToken } = authService.issueTokens(req.user!.id);
    // 쿠키에 설정 또는 프론트로 리다이렉트
    res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true });
    res.redirect(`/?token=${accessToken}`);
  }
);

httpOnly 쿠키 vs localStorage

📋 코드 (9줄)
| 저장소 | XSS 위험 | CSRF 위험 | 모바일 |
|---|---|---|---|
| localStorage | 높음 (JS 접근) | 없음 | 가능 |
| httpOnly Cookie | 낮음 | 있음 | 제한 |


권장:
- accessToken: 메모리 (페이지 새로고침 시 refresh로 재발급)
- refreshToken: httpOnly Secure SameSite=Strict 쿠키

다음 챕터

CH.33 "파일 업로드: Multer + S3 + presigned URL".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 인증 시스템 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

인증 시스템 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 인증 시스템
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 백엔드 시장의
인증 시스템 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
인증 시스템: Passport + JWT + OAuth2 이 3가지만 확실히 잡으세요
1.JWT는 액세스(15분) + 리프레시(7일) 분리 — 보안과 UX 균형
2.리프레시 토큰은 DB 저장 → 로그아웃 시 revoke 가능
3.OAuth2는 Passport 전략으로 — Google/GitHub/Kakao 모두 동일 패턴


공유하기
진행도 32 / 90