Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ Optional<ProjectApiKey> findFirstByProjectAndApiKeyStatusNotOrderByCreatedAtDesc
Project project, ApiKeyStatus status);

List<ProjectApiKey> findAllByProjectAndApiKeyStatusNot(Project project, ApiKeyStatus status);

Optional<ProjectApiKey> findByApiKeyHash(String apiKeyHash);
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
package kr.java.documind.domain.member.service;

import java.util.Optional;
import java.util.UUID;
import kr.java.documind.domain.auth.model.entity.ProjectApiKey;
import kr.java.documind.domain.auth.model.repository.ProjectApiKeyRepository;
import kr.java.documind.domain.member.model.enums.ApiKeyStatus;
import kr.java.documind.global.util.HmacApiKeyUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ProjectApiKeyValidationService {

// 테스트를 위한 약속된 더미 데이터 세팅
public static final String VALID_TEST_API_KEY = "test-api-key-1234";
public static final UUID DUMMY_PROJECT_ID =
UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
private final ProjectApiKeyRepository projectApiKeyRepository;

@Value("${app.api-key.hmac-secret}")
private String hmacSecret;

/**
* [임시] API Key 검증 로직 (해싱 및 DB 연동 구현 전까지 사용)
* TODO: [godqhrenf] Api-Key 검증 후 projectId 반환 로직 구현
* API Key를 검증하고, 유효한 경우 연결된 ProjectId를 반환합니다.
*
* @param rawApiKey 사용자가 제공한 API Key (plain text)
* @return 유효한 경우 ProjectId, 그렇지 않으면 null
*/
@Transactional(readOnly = true)
public UUID getProjectIdByApiKey(String rawApiKey) {
log.info("요청된 API Key (테스트 모드): {}", rawApiKey);
if (rawApiKey == null || rawApiKey.isBlank()) {
log.warn("API Key가 제공되지 않았습니다.");
return null;
}

String requestHashedKey = HmacApiKeyUtil.computeHmac(rawApiKey, hmacSecret);
Optional<ProjectApiKey> apiKeyOptional =
projectApiKeyRepository.findByApiKeyHash(requestHashedKey);

if (apiKeyOptional.isEmpty()) {
log.warn("유효하지 않은 API Key 접근 시도: {}", HmacApiKeyUtil.maskApiKey(rawApiKey));
return null;
}

ProjectApiKey apiKey = apiKeyOptional.get();

// 1. 약속된 테스트용 API Key가 들어온 경우 -> 고정된 더미 ProjectId 반환
if (VALID_TEST_API_KEY.equals(rawApiKey)) {
log.info("API Key 검증 성공! 테스트 ProjectId 반환: {}", DUMMY_PROJECT_ID);
return DUMMY_PROJECT_ID;
if (apiKey.getApiKeyStatus() != ApiKeyStatus.ACTIVE) {
log.warn(
"비활성 API Key입니다 (상태: {}): {}",
apiKey.getApiKeyStatus(),
HmacApiKeyUtil.maskApiKey(rawApiKey));
return null;
}

// 2. 그 외의 키가 들어온 경우 -> null 반환 (Filter에서 401 에러로 튕겨냄)
log.warn("유효하지 않은 API Key 접근 시도: {}", rawApiKey);
return null;
log.info("API Key 검증 성공! Project PublicId: {}", apiKey.getProject().getPublicId());
return apiKey.getProject().getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package kr.java.documind.domain.member.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;

import java.util.Optional;
import java.util.UUID;
import kr.java.documind.domain.auth.model.entity.Project;
import kr.java.documind.domain.auth.model.entity.ProjectApiKey;
import kr.java.documind.domain.auth.model.repository.ProjectApiKeyRepository;
import kr.java.documind.global.util.HmacApiKeyUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(MockitoExtension.class)
@DisplayName("ProjectApiKeyValidationService 단위 테스트")
class ProjectApiKeyValidationServiceTest {

@Mock private ProjectApiKeyRepository projectApiKeyRepository;
@Mock private Project testProject;

@InjectMocks private ProjectApiKeyValidationService validationService;

private final String hmacSecret = "test-secret";
private final UUID projectId = UUID.randomUUID();

@BeforeEach
void setUp() {
ReflectionTestUtils.setField(validationService, "hmacSecret", hmacSecret);
lenient().when(testProject.getId()).thenReturn(projectId);
}

@Test
@DisplayName("API Key 발급부터 검증까지 전체 흐름: 유효한 키 → ProjectId 반환")
void getProjectIdByApiKey_ValidKey_ReturnsProjectId() {
// Given: HmacApiKeyUtil을 사용하여 API Key 생성
String generatedRawApiKey = HmacApiKeyUtil.generatePlainKey();
String extractedPrefix = HmacApiKeyUtil.extractPrefix(generatedRawApiKey);
String last4 = HmacApiKeyUtil.extractLast4(generatedRawApiKey);
String hashedApiKey = HmacApiKeyUtil.computeHmac(generatedRawApiKey, hmacSecret);

ProjectApiKey apiKey =
ProjectApiKey.create(testProject, hashedApiKey, extractedPrefix, last4);

// 해시 검색으로 Mocking
when(projectApiKeyRepository.findByApiKeyHash(hashedApiKey))
.thenReturn(Optional.of(apiKey));

// When: 생성된 API Key로 검증 시도
UUID resultProjectId = validationService.getProjectIdByApiKey(generatedRawApiKey);

// Then: 검증 성공 및 ProjectId 반환
assertThat(resultProjectId).isNotNull();
assertThat(resultProjectId).isEqualTo(projectId);
}

@Test
@DisplayName("API Key 검증 실패: API Key가 null인 경우 → null 반환")
void getProjectIdByApiKey_NullApiKey_ReturnsNull() {
// Given
String nullApiKey = null;

// When
UUID resultProjectId = validationService.getProjectIdByApiKey(nullApiKey);

// Then
assertThat(resultProjectId).isNull();
}

@Test
@DisplayName("API Key 검증 실패: 존재하지 않는 API Key → null 반환")
void getProjectIdByApiKey_NonExistentHash_ReturnsNull() {
// Given
String rawApiKey = HmacApiKeyUtil.generatePlainKey();
String hashedApiKey = HmacApiKeyUtil.computeHmac(rawApiKey, hmacSecret);

// DB에 해당 해시값이 없을 때 (Optional.empty 반환)
when(projectApiKeyRepository.findByApiKeyHash(hashedApiKey)).thenReturn(Optional.empty());

// When
UUID resultProjectId = validationService.getProjectIdByApiKey(rawApiKey);

// Then
assertThat(resultProjectId).isNull();
}

@Test
@DisplayName("API Key 검증 실패: 변조된 API Key 입력 시 (해시 불일치) → null 반환")
void getProjectIdByApiKey_HashMismatch_ReturnsNull() {
// Given: 정상적인 키가 발급되었으나
String originalRawApiKey = HmacApiKeyUtil.generatePlainKey();

// 클라이언트가 끝자리를 'X'로 변조하여 요청했다고 가정
String tamperedRawApiKey =
originalRawApiKey.substring(0, originalRawApiKey.length() - 1) + "X";
String tamperedHashedApiKey = HmacApiKeyUtil.computeHmac(tamperedRawApiKey, hmacSecret);

// 변조된 키로 만든 해시값은 DB에 존재하지 않음
when(projectApiKeyRepository.findByApiKeyHash(tamperedHashedApiKey))
.thenReturn(Optional.empty());

// When: 변조된 키로 검증 시도
UUID resultProjectId = validationService.getProjectIdByApiKey(tamperedRawApiKey);

// Then: 식별 실패로 null 반환
assertThat(resultProjectId).isNull();
}

@Test
@DisplayName("API Key 검증 실패: 정지된(SUSPENDED) 키 → null 반환")
void getProjectIdByApiKey_SuspendedKey_ReturnsNull() {
// Given
String generatedRawApiKey = HmacApiKeyUtil.generatePlainKey();
String extractedPrefix = HmacApiKeyUtil.extractPrefix(generatedRawApiKey);
String last4 = HmacApiKeyUtil.extractLast4(generatedRawApiKey);
String hashedApiKey = HmacApiKeyUtil.computeHmac(generatedRawApiKey, hmacSecret);

ProjectApiKey apiKey =
ProjectApiKey.create(testProject, hashedApiKey, extractedPrefix, last4);
apiKey.suspend(); // 상태 변경

when(projectApiKeyRepository.findByApiKeyHash(hashedApiKey))
.thenReturn(Optional.of(apiKey));

// When
UUID resultProjectId = validationService.getProjectIdByApiKey(generatedRawApiKey);

// Then
assertThat(resultProjectId).isNull();
}

@Test
@DisplayName("API Key 검증 실패: 폐기된(REVOKED) 키 → null 반환")
void getProjectIdByApiKey_RevokedKey_ReturnsNull() {
// Given
String generatedRawApiKey = HmacApiKeyUtil.generatePlainKey();
String extractedPrefix = HmacApiKeyUtil.extractPrefix(generatedRawApiKey);
String last4 = HmacApiKeyUtil.extractLast4(generatedRawApiKey);
String hashedApiKey = HmacApiKeyUtil.computeHmac(generatedRawApiKey, hmacSecret);

ProjectApiKey apiKey =
ProjectApiKey.create(testProject, hashedApiKey, extractedPrefix, last4);
apiKey.revoke(); // 상태 변경

when(projectApiKeyRepository.findByApiKeyHash(hashedApiKey))
.thenReturn(Optional.of(apiKey));

// When
UUID resultProjectId = validationService.getProjectIdByApiKey(generatedRawApiKey);

// Then
assertThat(resultProjectId).isNull();
}
}