Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/(with-sidebar)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
'use client';

import Sidebar from '@/components/common/Sidebar';
import { useAuthGuard } from '@/hooks/useAuthGuard';

import '@uiw/react-md-editor/markdown-editor.css';
import '@uiw/react-markdown-preview/markdown.css';
Expand All @@ -8,6 +11,13 @@ export default function SidebarLayout({
}: {
children: React.ReactNode;
}) {
const { loading, isAuthed } = useAuthGuard({ redirectTo: '/login' });

if (loading) return <div>로딩중...</div>;
if (!isAuthed) {
return null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<div className="flex">
<Sidebar />
Expand Down
8 changes: 8 additions & 0 deletions components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ const LoginForm = ({ handleOpenModal }: LoginFormProps) => {

const router = useRouter();

// 쿠키 설정 헬퍼 함수
const setLoginCookie = () => {
// 7일간 유지되는 쿠키 설정
document.cookie = 'isLoggedIn=true; path=/; max-age=' + 60 * 60 * 24 * 7;
};

const handleGithubLogin = async () => {
if (isLoading) return;
setIsLoading(true);
setError('');
try {
await signupWithGithub();
setLoginCookie(); // 쿠키 설정
router.push('/');
} catch (err: unknown) {
setError('Github 로그인에 실패했습니다.');
Expand Down Expand Up @@ -58,6 +65,7 @@ const LoginForm = ({ handleOpenModal }: LoginFormProps) => {
password
);
// console.log('로그인 성공!', userCredential.user);
setLoginCookie(); // 쿠키 설정
router.push('/');
} catch (err) {
// console.error('로그인 에러:', err);
Expand Down
4 changes: 3 additions & 1 deletion components/common/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ const Sidebar = () => {
const handleLogout = async (): Promise<void> => {
try {
await signOut(auth);
document.cookie =
'isLoggedIn=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
alert('로그아웃 되었습니다.');
router.replace('/landing');
router.refresh(); // 서버 컴포넌트/세션 UI 갱신
} catch (err) {
console.error('로그아웃 실패:', err);
alert('로그아웃에 실패했습니다.');
Expand Down
3 changes: 0 additions & 3 deletions components/write/PostDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const Markdown = dynamic(
const PostDetail = ({ tilId }: { tilId: string }) => {
const router = useRouter();
const [data, setData] = useState<Til | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const unsub = auth.onAuthStateChanged(async (user) => {
Expand All @@ -26,13 +25,11 @@ const PostDetail = ({ tilId }: { tilId: string }) => {

const post = await fetchMyTil(user.uid, tilId);
setData(post);
setLoading(false);
});

return () => unsub();
}, [tilId, router]);

if (loading) return <div className="min-h-screen p-6">로딩중...</div>;
if (!data) return <div className="min-h-screen p-6">글이 없습니다.</div>;

return (
Expand Down
44 changes: 44 additions & 0 deletions hooks/useAuthGuard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { useEffect, useState } from 'react';
import { onAuthStateChanged, type User } from 'firebase/auth';
import { useRouter } from 'next/navigation';
import { auth } from '@/lib/firebase';

type UseAuthGuardOptions = {
/** 로그인 안 됐을 때 보낼 경로 */
redirectTo?: string;
};

type UseAuthGuardResult = {
user: User | null;
loading: boolean;
isAuthed: boolean;
};

export function useAuthGuard(
options: UseAuthGuardOptions = {}
): UseAuthGuardResult {
const { redirectTo = '/login' } = options;
Comment thread
DongEun02 marked this conversation as resolved.

const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const unsub = onAuthStateChanged(auth, (u) => {
if (!u) {
setUser(null);
setLoading(true);
router.replace(redirectTo);
return;
}
setUser(u);
setLoading(false);
});

return () => unsub();
}, [router, redirectTo]);
Comment on lines +28 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

loading 상태가 미인증 시 false로 전환되지 않는 문제

사용자가 미인증일 때 setLoading(true)를 호출하고 있어 loading이 계속 true인 채로 남습니다. 이로 인해:

  1. layout.tsx에서 if (!isAuthed) 블록의 alert가 절대 실행되지 않음 (loadingtrue이므로 먼저 반환됨)
  2. 리다이렉트 중 사용자가 "로딩중..." 화면만 계속 보게 됨

또한 Firebase onAuthStateChanged는 초기에 persisted auth state를 확인하기 전에 null로 콜백될 수 있어, 실제로 로그인된 사용자도 잠시 미인증으로 판단되어 불필요한 리다이렉트가 발생할 수 있습니다.

🔧 수정 제안
 useEffect(() => {
   const unsub = onAuthStateChanged(auth, (u) => {
     if (!u) {
       setUser(null);
-      setLoading(true);
+      setLoading(false);
       router.replace(redirectTo);
       return;
     }
     setUser(u);
     setLoading(false);
   });

   return () => unsub();
 }, [router, redirectTo]);
🤖 Prompt for AI Agents
In `@hooks/useAuthGuard.ts` around lines 28 - 41, The onAuthStateChanged handler
currently sets setLoading(true) when u is null which leaves loading stuck true
and causes premature redirects; update the effect (the onAuthStateChanged
callback in useEffect) to always setLoading(false) after processing the auth
result and avoid redirecting on the initial transient null by introducing an
initialization guard (e.g., a ref or an "initialized" state): when the callback
runs, setUser(u ?? null), setLoading(false), and if u is null only call
router.replace(redirectTo) after the initialization flag is true (set the flag
to true on the first callback) so transient nulls don’t trigger redirects (refer
to the onAuthStateChanged callback, setUser, setLoading, router.replace, and
redirectTo).


return { user, loading, isAuthed: !!user };
}
28 changes: 28 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;

// 쿠키에서 isLoggedIn 확인
const isLoggedIn = request.cookies.get('isLoggedIn')?.value === 'true';

if (pathname === '/') {
// 로그인이 안 된 경우에만 랜딩으로 보냄
if (!isLoggedIn) {
return NextResponse.redirect(new URL('/landing', request.url));
}
// 로그인 된 상태라면 그대로 루트('/')를 보여줌
return NextResponse.next();
}

if (pathname === '/landing' && isLoggedIn) {
return NextResponse.redirect(new URL('/', request.url));
}

return NextResponse.next();
}
Comment on lines +4 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the middleware file and related auth configuration
git ls-files | grep -E "(middleware|auth|cookie|firebase)" | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 428


🏁 Script executed:

# Look for where isLoggedIn cookie is set
rg "isLoggedIn" -t typescript -t javascript -B 3 -A 3

Repository: DeveloperBlog-Devflow/devflow

Length of output: 107


🏁 Script executed:

# Search for cookie configuration and HttpOnly settings
rg "cookies\." -t typescript -t javascript -B 2 -A 2 | head -50

Repository: DeveloperBlog-Devflow/devflow

Length of output: 107


🏁 Script executed:

# Check for Firebase configuration
rg -i "firebase" -t typescript -t javascript -t json | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 107


🏁 Script executed:

# Look for auth-related setup and session configuration
fd -e "ts" -e "js" -e "json" | xargs grep -l -E "(auth|session|login)" 2>/dev/null | head -10

Repository: DeveloperBlog-Devflow/devflow

Length of output: 160


🏁 Script executed:

# Read the middleware file completely
cat -n middleware.ts

Repository: DeveloperBlog-Devflow/devflow

Length of output: 936


🏁 Script executed:

# Look for where isLoggedIn cookie is set (likely in LoginForm)
cat -n "components/auth/LoginForm.tsx"

Repository: DeveloperBlog-Devflow/devflow

Length of output: 4636


🏁 Script executed:

# Check the Firebase configuration
cat -n "lib/firebase.ts"

Repository: DeveloperBlog-Devflow/devflow

Length of output: 1373


🏁 Script executed:

# Check signup service for auth handling
cat -n "services/auth/signup.service.ts"

Repository: DeveloperBlog-Devflow/devflow

Length of output: 1900


🏁 Script executed:

# Search for any server-side auth handling or API routes
fd -e ts -e js | xargs grep -l "NextRequest\|NextResponse\|route.ts" 2>/dev/null | head -10

Repository: DeveloperBlog-Devflow/devflow

Length of output: 86


🏁 Script executed:

# Check if there are any server-side auth routes or session handling
fd -e ts -e js "api.*auth\|api.*session\|route" 2>/dev/null | head -10

Repository: DeveloperBlog-Devflow/devflow

Length of output: 55


🏁 Script executed:

# Look for any logout functionality or cookie clearing
rg "isLoggedIn|cookies\|logout" --type ts --type js -B 2 -A 2 | head -40

Repository: DeveloperBlog-Devflow/devflow

Length of output: 1324


보안 취약점: 클라이언트 쿠키 기반 라우팅 인증 우회 가능

현재 미들웨어는 isLoggedIn 쿠키의 값만으로 라우팅을 결정하고 있습니다. 이 쿠키는 LoginForm.tsx의 document.cookie = 'isLoggedIn=true'로 클라이언트에서 설정되므로, 악의적인 사용자가 브라우저 콘솔에서 document.cookie = 'isLoggedIn=true'를 실행하여 인증을 우회할 수 있습니다. 미들웨어는 이 쿠키 값을 검증 없이 신뢰하고 있습니다.

이미 Firebase Auth를 통해 실제 인증을 처리하고 있으므로, 다음 중 하나의 방식으로 개선할 것을 권장합니다:

  1. Firebase Admin SDK를 사용하여 미들웨어에서 서버 측 세션 토큰 검증
  2. Firebase Auth의 createSessionCookie() 기능을 활용하여 HttpOnly + Secure 플래그가 적용된 안전한 쿠키 사용

더불어, 명시적인 NextResponse.next() 반환을 추가하면 코드의 의도가 더욱 명확해집니다.

🔒 명시적 반환 추가 제안
  if (pathname === '/landing' && isLoggedIn) {
    return NextResponse.redirect(new URL('/', request.url));
  }
+
+ return NextResponse.next();
}
🤖 Prompt for AI Agents
In `@middleware.ts` around lines 4 - 22, The middleware currently trusts the
client-set isLoggedIn cookie (set in LoginForm.tsx via document.cookie) allowing
auth bypass; update middleware(request: NextRequest) to validate authentication
server-side instead of reading this client cookie — either verify Firebase
ID/session tokens using the Firebase Admin SDK or switch to Firebase Auth
createSessionCookie() and read an HttpOnly, Secure session cookie in middleware,
and replace the simple cookie check with token verification logic before calling
NextResponse.redirect/new URL(...) or NextResponse.next(); also ensure every
code path returns explicitly (add an explicit NextResponse.next() at the end of
middleware) so intent is clear.


export const config = {
matcher: ['/', '/landing'],
};