# 인증 / 로그인 / 보안 리팩토링 감사 리포트

- **작성자:** gbpark
- **작성일:** 2026-04-08
- **범위:** v2 컴포넌트 영역 (`frontend/components/v2`, `frontend/lib/v2-core`, `frontend/lib/registry/components/v2-*`) **제외** 한 인증·로그인·보안 스택
- **결론:** 리팩토링 필요. 일부 항목은 보안 패치 수준으로 시급함.

---

## 점검 대상 파일

| 영역 | 파일 |
|---|---|
| Frontend 로그인 페이지 | `frontend/app/(auth)/login/page.tsx` |
| Frontend 로그인 훅 | `frontend/hooks/useLogin.ts` |
| Frontend 인증 훅 | `frontend/hooks/useAuth.ts` |
| Frontend API 클라이언트 (토큰/인터셉터) | `frontend/lib/api/client.ts` |
| Frontend 미들웨어 | `frontend/middleware.ts` |
| Backend 보안 설정 | `backend-spring/src/main/java/com/erp/security/SecurityConfig.java` |
| Backend JWT 발급/검증 | `backend-spring/.../security/JwtTokenProvider.java` |
| Backend JWT 필터 | `backend-spring/.../security/JwtAuthenticationFilter.java` |
| Backend 인증 컨트롤러 | `backend-spring/.../controller/AuthController.java` |
| Backend 인증 서비스 (386줄) | `backend-spring/.../service/AuthService.java` |
| Backend 환경설정 (JWT secret) | `backend-spring/src/main/resources/application.yml` |

---

## 🔴 시급 (보안 결함)

### 1. JWT secret 평문 커밋
**파일:** `backend-spring/src/main/resources/application.yml:35`
```yaml
jwt:
  secret: ilshin-plm-super-secret-jwt-key-2024
  expiration: 86400000
```
git 에 그대로 올라가 있음. 누구든 토큰 위조 가능.
**조치:** 환경변수 / Vault 로 즉시 분리. `${JWT_SECRET}` 형태로 외부 주입.

### 2. 마스터 패스워드 하드코딩
**파일:** `backend-spring/.../service/AuthService.java:29`
```java
@Value("${auth.master-password:qlalfqjsgh11}")
private String masterPassword;
```
`AuthService.java:52` 의 `masterPassword.equals(password)` 한 줄로 **모든 사용자 계정에 임의 로그인 가능**. application.yml 에 override 도 없어서 디폴트가 그대로 활성.
**조치:** 운영 환경에서 즉시 제거. 디버깅이 필요하면 별도 admin impersonation 엔드포인트 + 감사 로그 + 권한 체크로 분리.

### 3. MD5 패스워드 해싱
**파일:** `AuthService.java:55, 359`
```java
} else if (storedPw != null && md5(password).equals(storedPw)) {
```
회원가입도 MD5 저장. SecurityConfig 에 `BCryptPasswordEncoder` Bean 은 등록만 해놨지 **사용처 0건**.
**조치:** BCrypt 또는 Argon2 마이그레이션. 점진 재해시 (로그인 성공 시 BCrypt 로 재저장) 로직 도입.

### 4. SecurityConfig 가 모든 API 무방비
**파일:** `SecurityConfig.java:34`
```java
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated()
```
**전체 API 가 permitAll**. JwtAuthenticationFilter 는 SecurityContext 채워주는 단순 정보 전달자에 불과하고, 실제 보호는 컨트롤러 안에서 `validateToken()` 호출로만 됨. 컨트롤러에서 직접 검증하지 않는 엔드포인트는 토큰 없이 호출해도 통과될 위험.
**조치:** `permitAll` 을 `/api/auth/login`, `/api/auth/signup`, `/api/auth/refresh` 정도로 한정하고 나머지는 `authenticated()` 로 변경.

### 5. CORS 와일드카드 + Credentials 동시 허용
**파일:** `SecurityConfig.java:51`
```java
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowCredentials(true);
```
`*` + `allowCredentials=true` 조합은 모든 출처에서 쿠키 포함 요청을 허용.
**조치:** 운영용 화이트리스트 (`v1.invyone.com`, 사내 IP 등) 로 좁힐 것.

### 6. JWT 를 localStorage 저장 + 비-HttpOnly 쿠키 중복
**파일:** `frontend/lib/api/client.ts:83-87`, `frontend/hooks/useAuth.ts:58-64`
```js
localStorage.setItem("authToken", token);
document.cookie = `authToken=${token}; path=/; max-age=86400; SameSite=Lax`;
```
- `localStorage` → XSS 한 방에 토큰 탈취
- 쿠키도 **HttpOnly 미설정 + Secure 미설정 + SameSite=Lax**
- 미들웨어는 쿠키만 읽고 훅은 localStorage 만 읽어 **동일 토큰을 두 곳에 중복 저장**, 정합성 깨질 여지 큼

**조치:** HttpOnly + Secure + SameSite=Strict 쿠키 단일 소스. 프론트가 토큰을 직접 만지지 않게 변경.

### 7. 사용자 enumeration
**파일:** `AuthService.java:48-60`
- 사용자 없음 → "사용자가 존재하지 않습니다."
- 비번 틀림 → "패스워드가 일치하지 않습니다."

ID 존재 여부가 메시지로 노출됨.
**조치:** 통일 메시지 ("아이디 또는 비밀번호를 확인해주세요") 로 변경.

### 8. 로그인 시도 제한 / 락아웃 없음
brute-force 무방비. `auth.insertLoginLog` 로 실패 기록은 남기는데 카운터/잠금은 없음.
**조치:** IP/계정별 카운터 + N회 이상 실패 시 일시 잠금 (예: Redis or DB 카운터).

---

## 🟡 중요 (구조 / 일관성 결함)

### 9. `useAuth.login()` 의 필드명 버그 (죽은 코드 가능성)
**파일:** `frontend/hooks/useAuth.ts:268-271`
```ts
const response = await apiCall<any>("POST", "/auth/login", {
  userId,        // ❌ 백엔드는 user_id 를 기대
  password,
});
```
백엔드 `AuthController.login()` 은 `body.get("user_id")` (`AuthController.java:32`) 로 받음. **이 훅으로 로그인하면 항상 실패**. 실제 로그인 페이지는 `useLogin` 만 쓰지만, 훅 자체가 죽은 코드 또는 잠재 버그.
**조치:** `useAuth.login()` 제거 or 필드명 수정. `useLogin` 과 통합 검토.

### 10. TokenManager 가 두 곳에 중복 정의
- `frontend/lib/api/client.ts:75-127`
- `frontend/hooks/useAuth.ts:50-81`

거의 동일한 로직을 두 번 작성. DRY 위반.
**조치:** `lib/auth/tokenManager.ts` 로 단일화.

### 11. `useAuth.refreshUserData()` 의 "임시 인증 유지" fallback
**파일:** `frontend/hooks/useAuth.ts:204-248`

`/auth/me` 가 실패하면 **클라이언트에서 토큰 페이로드를 디코드해서 그대로 신뢰**. 백엔드가 401 을 줘도 프론트는 "로그인 상태"로 우김. 디버깅용 안전장치치곤 위험.
**조치:** 운영에서는 제거 또는 명확한 "오프라인 모드" 분기로 이동.

### 12. `getUserInfo()` 의 중복 put 버그
**파일:** `AuthService.java:246-247`
```java
result.put("company_code", companyCode);
result.put("company_code", companyCode);   // ← 같은 키 두 번 — 원래 다른 키였을 듯
```
**조치:** 의도 확인 후 수정. (예: 두번째 줄이 `is_admin` 또는 다른 키였을 가능성)

### 13. Refresh token 모델 부재
**파일:** `AuthService.java:191`

`refreshToken()` 이 같은 access token 으로 새 access token 발급. **Refresh token 분리 / 회전 / revoke 없음**. 토큰 탈취 시 만료까지 계속 갱신 가능.
**조치:** JTI + 블랙리스트 또는 별도 refresh token 도입.

### 14. AuthService 386줄 단일 책임 위반
login / refresh / status / switchCompany / signup / logout / firstMenuPath 계산 / popLandingPath 계산 모두 한 클래스.
**조치:** 메뉴 경로 계산 → `MenuService`, signup → `DriverSignupService` 분리.

### 15. 미들웨어 보호 경로가 `/admin` 한 곳뿐
**파일:** `frontend/middleware.ts:30`

```ts
const strictProtectedPaths = ["/admin"];
```
나머지는 전부 클라이언트 AuthGuard 에 위임. 페이지가 한 번 렌더된 후 클라이언트에서 리다이렉트되는 깜빡임 발생 + 서버 사이드 보호 거의 없음. localStorage 정책 유지하는 한 어쩔 수 없는 한계인데, **HttpOnly 쿠키로 옮기면 미들웨어에서 전체 경로 보호 가능**.

---

## 🟢 자잘한 정리

- **`frontend/lib/api/client.ts:14-36`** — API base URL 결정 로직이 환경변수 + hostname 분기 + 포트 분기로 길어짐. hostname 하드코딩 (`v1.invyone.com`) 은 환경변수로 분리 권장.
- **`useAuth.ts:196`, `AuthService.java:273`** — `"plm_admin" === userId` 식의 매직 ID 가 프론트/백 양쪽에 박힘. role 기반으로 일원화.
- **`JwtAuthenticationFilter`** — 토큰 만료/위조 시 401 응답 없이 그냥 다음 필터로 넘김 (#4 결함과 맞물려 동작 불일치).
- **`frontend/hooks/useLogin.ts:81-95`** — `checkExistingAuth` 가 401 받으면 인터셉터의 자동 redirectToLogin 과 충돌 여지. 현재는 `pathname === "/login"` 가드로 막혀있긴 함.

---

## 권장 우선순위

| 단계 | 항목 | 비고 |
|---|---|---|
| **즉시** | #1 JWT secret 환경변수화 | 운영 토큰 위조 차단 |
| **즉시** | #2 마스터 패스워드 제거 | 백도어 차단 |
| **즉시** | #3 MD5 → BCrypt 마이그레이션 | 점진 재해시 |
| **즉시** | #4 SecurityConfig permitAll 좁히기 | 실제 보호 활성화 |
| **즉시** | #5 CORS 화이트리스트 | |
| **단기** | #6 HttpOnly 쿠키 단일 소스 | + #10 TokenManager 통합 + #15 미들웨어 확장 |
| **단기** | #7 사용자 enumeration 메시지 통일 | |
| **단기** | #8 로그인 락아웃 | |
| **단기** | #9 useAuth.login 필드명 버그 | 또는 죽은 코드 제거 |
| **단기** | #12 getUserInfo 중복 put 버그 | |
| **중기** | #11 임시 인증 fallback 정리 | |
| **중기** | #13 Refresh token 분리 | |
| **중기** | #14 AuthService 책임 분할 | |

---

## 종합

전체적으로 **"동작은 하지만 보안 모델이 절반만 완성된" 상태**.
특히 #1 ~ #5 는 보안 사고로 직결되는 항목이라 v2 작업과 무관하게 별도 리팩토링 트랙으로 빼서 처리하는 걸 권장함.
