Next.js + BFF 인증 흐름 총정리

임기호·2025년 8월 27일

TL;DR

  • Access: 응답 헤더 Authorization: Bearer <access>로 수신 → 클라이언트 메모리 only 보관
  • Refresh: HttpOnly 쿠키(SameSite, Secure, Path, Max-Age)로만 보관
  • 부팅 가드: 앱 시작 시 /api/auth/refresh 1회로 Access 복원
  • 401 처리: 싱글 플라이트로 refresh → 성공 시 1회 재시도, 실패 시 /login?next=...
  • BFF: 업스트림의 Set-Cookie / Authorization그대로 전달, Cache-Control: no-store

(참고) NestJS 백엔드 예시는 글 맨 아래 부록에 첨부


① 로그인

  • BFF가 /auth/login 포워딩
  • 응답의 Authorization에서 access 추출, refresh는 쿠키로 저장

② 하드 리프레시 후 access 복원

  • 부팅 시 /api/auth/refresh 호출 → Authorization 헤더로 access 복원
  • Access-Control-Expose-Headers: authorization 필요

③ 데이터 호출(성공)

  • axios 인터셉터가 Authorization: Bearer <access> 자동 부착
  • BFF 프록시는 외부로 쿠키 전파하지 않음

④ 401 → 리프레시 → 재시도

  • 응답 401 → 싱글 플라이트로 /api/auth/refresh 1회만
  • 성공 시 원요청 1회 재시도, 실패 시 로그인 이동

⑤ 라우팅 가드(미들웨어)

  • refresh_token 존재 여부로 보호 구역 접근 제어
  • /login은 허용, 그 외는 세션 없으면 /login으로

⑥ 로그아웃

  • /auth/logout → 서버가 refresh_token 삭제 Set-Cookie 반환
  • 클라에서 access 메모리 삭제 후 /login 이동

부록 A — 클라이언트 핵심 스니펫(간단 버전)

// ensureAccessBool(): 토큰 보장 여부만 반환
let inFlight: Promise<boolean> | null = null;

async function refresh(): Promise<string | null> {
  const r = await internalClient.post('/api/auth/refresh', null, { withCredentials: true });
  const auth = r.headers?.['authorization'];
  const t = auth?.startsWith('Bearer ') ? auth.slice(7) : null;
  if (t) AuthToken.set(t);
  return t ?? null;
}

export async function ensureAccessBool(): Promise<boolean> {
  if (AuthToken.get()) return true;
  if (!inFlight) {
    inFlight = (async () => !!(await refresh()))().finally(() => { inFlight = null; });
  }
  return await inFlight;
}

// axios 인터셉터 예시
api.interceptors.request.use(async (cfg) => {
  if (needsBearer(cfg.url, cfg.baseURL)) {
    const ok = await ensureAccessBool();
    if (!ok) {
      AuthToken.clear();
      window.location.replace('/login');
      throw new axios.CanceledError('redirecting to login');
    }
    (cfg.headers ??= {}).Authorization = `Bearer ${AuthToken.get()!}`;
  }
  return cfg;
});

부록 B — NestJS 백엔드(핵심 패턴)

@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const { access, refresh } = await this.authService.login(dto);
  res.setHeader('Authorization', 'Bearer ' + access);
  res.cookie('refresh_token', refresh, {
    httpOnly: true, sameSite: 'lax', secure: isProd, path: '/', maxAge: 604800000,
  });
  return { ok: true };
}

@Post('refresh')
async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
  const rt = req.cookies?.['refresh_token'];
  const { access, refresh } = await this.authService.refresh(rt);
  res.setHeader('Authorization', 'Bearer ' + access);
  if (refresh) res.cookie('refresh_token', refresh, { httpOnly: true, sameSite: 'lax', secure: isProd, path: '/', maxAge: 604800000 });
  return { ok: true };
}

@Post('logout')
async logout(@Res({ passthrough: true }) res: Response) {
  res.clearCookie('refresh_token', { httpOnly: true, sameSite: 'lax', secure: isProd, path: '/' });
  return { ok: true };
}

0개의 댓글