아웃바운드 OAuth 토큰 현황 조회 API 추가
- GET /manage/oauth-token/status[/{어댑터그룹명}] 조회 전용
- SessionManager.peekOutboundAccessToken 추가 : 캐시만 조회해 상태 조회가 토큰 발급을 유발하지 않도록 함
- AccessTokenManagerByDB 에 조회 전용 getter 추가
- accessToken 은 앞 8자만 노출하고 clientSecret 은 응답에 미포함
- Ehcache 백엔드는 아웃바운드 토큰 캐시 미지원이므로 사유를 담아 응답
- 단위테스트 10건 추가
This commit is contained in:
@@ -384,6 +384,39 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 등록된 OAuth 인증 정보의 어댑터그룹명 목록을 반환한다.
|
||||
* 2. 처리 개요 : 상태 조회 API 에서 사용한다.
|
||||
*
|
||||
* @return 어댑터그룹명 목록
|
||||
**/
|
||||
public Set<String> getRegisteredAdapterGroupNames() {
|
||||
return new TreeSet<>(outboundOAuthCredentialVos.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 어댑터그룹의 OAuth 인증 정보를 반환한다.
|
||||
* 2. 처리 개요 : 상태 조회 API 에서 사용한다.
|
||||
* 3. 주의사항 : clientSecret 이 들어있으므로 응답 전문에 그대로 담지 말 것.
|
||||
*
|
||||
* @return OAuth 인증 정보. 등록돼 있지 않으면 null.
|
||||
**/
|
||||
public OutboundOAuthCredentialVo getOutboundOAuthCredentialVo(String adapterGroupName) {
|
||||
return outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 캐시에 있는 토큰만 조회한다.
|
||||
* 2. 처리 개요 : getAccessTokenVO() 와 달리 캐시에 없거나 만료됐어도 새로 발급하지 않는다.
|
||||
* 상태 조회가 토큰 발급을 유발하면 안 되므로 조회 API 는 이 메서드를 쓴다.
|
||||
*
|
||||
* @return 캐시에 있는 토큰. 없으면 null.
|
||||
* @exception UnsupportedOperationException 아웃바운드 토큰 캐시를 지원하지 않는 SessionManager 백엔드
|
||||
**/
|
||||
public AccessTokenVO peekAccessTokenVO(String adapterGroupName) {
|
||||
return SessionManager.getInstance().peekOutboundAccessToken(adapterGroupName);
|
||||
}
|
||||
|
||||
public boolean isOAuthCredentialRegistered(String adapterGroupName){
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
return outboundOAuthCredentialVo != null && "Y".equals(outboundOAuthCredentialVo.getUseYn());
|
||||
|
||||
@@ -214,6 +214,14 @@ public abstract class SessionManager implements Lifecycle {
|
||||
//Outbound Access Token Cache
|
||||
public abstract AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> getNewToken);
|
||||
|
||||
/**
|
||||
* 캐시에 있는 토큰만 조회한다. 없거나 만료됐어도 새로 발급하지 않는다. (상태 조회용)
|
||||
*
|
||||
* @param key 어댑터그룹명
|
||||
* @return 캐시에 있는 토큰. 없으면 null.
|
||||
*/
|
||||
public abstract AccessTokenVO peekOutboundAccessToken(String key);
|
||||
|
||||
public abstract void removeOutboundAccessToken(String key);
|
||||
|
||||
public abstract void clearOutboundAccessToken();
|
||||
|
||||
@@ -951,6 +951,11 @@ public class SessionManagerForEhcache extends SessionManager {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenVO peekOutboundAccessToken(String key) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOutboundAccessToken(String key) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@@ -823,6 +823,14 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
public CacheInfoVO getOutboundAccessTokenCache() {
|
||||
return convert(cacheOutBoundAccessToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenVO peekOutboundAccessToken(String key) {
|
||||
if (cacheOutBoundAccessToken == null) {
|
||||
return null;
|
||||
}
|
||||
return cacheOutBoundAccessToken.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putWebSocketTimeout(String key, SessionVO value) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.eactive.eai.manage.oauthtoken;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 아웃바운드 OAuth 토큰 현황 조회 API. (HsmStatusController 와 동일한 응답 형태)
|
||||
*
|
||||
* 조회 전용이며 토큰을 발급하거나 캐시를 건드리지 않는다.
|
||||
* accessToken 은 앞 8자만 남기고 마스킹하며 clientSecret 은 응답에 담지 않는다.
|
||||
*
|
||||
* GET /manage/oauth-token/status → 등록된 전체 어댑터그룹의 토큰 현황
|
||||
* GET /manage/oauth-token/status/{adapterGroupName} → 어댑터그룹 하나의 토큰 현황
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/manage/oauth-token")
|
||||
public class OAuthTokenStatusController {
|
||||
|
||||
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
|
||||
|
||||
@Autowired
|
||||
private OAuthTokenStatusService oAuthTokenStatusService;
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<?> status() {
|
||||
return respond(() -> {
|
||||
List<OAuthTokenStatusDTO> list = oAuthTokenStatusService.getStatusList();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("count", list.size());
|
||||
data.put("tokens", list);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/status/{adapterGroupName}")
|
||||
public ResponseEntity<?> status(@PathVariable("adapterGroupName") String adapterGroupName) {
|
||||
return respond(() -> oAuthTokenStatusService.getStatus(adapterGroupName));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> respond(Callable<Object> action) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
result.put("success", true);
|
||||
result.put("data", action.call());
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", e.getMessage());
|
||||
}
|
||||
return ResponseEntity.ok().contentType(APPLICATION_JSON_UTF8).body(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.eactive.eai.manage.oauthtoken;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 어댑터그룹별 아웃바운드 OAuth 토큰 현황.
|
||||
*
|
||||
* accessToken 값은 마스킹해서 담는다. clientSecret 은 담지 않는다.
|
||||
*/
|
||||
@Data
|
||||
public class OAuthTokenStatusDTO {
|
||||
|
||||
// ---- 등록 정보 ----
|
||||
|
||||
/** 어댑터그룹명 */
|
||||
String adapterGroupName;
|
||||
|
||||
/** OAuth 인증 정보 사용 여부 (Y/N) */
|
||||
String useYn;
|
||||
|
||||
/** 토큰 발급에 사용하는 구현체 클래스명 (어댑터 속성 ADAPTER_TOKEN_ISSUING_CLIENT_TYPE) */
|
||||
String tokenServiceClass;
|
||||
|
||||
/** 토큰 발급 URL */
|
||||
String tokenUrl;
|
||||
|
||||
/** 토큰 재발급 주기(초) */
|
||||
int intervalSec;
|
||||
|
||||
// ---- 캐시 상태 ----
|
||||
|
||||
/** 캐시에 토큰이 있는지 */
|
||||
boolean cached;
|
||||
|
||||
/** 캐시된 토큰이 만료됐는지 */
|
||||
boolean expired;
|
||||
|
||||
/** 만료 시각 */
|
||||
String expiration;
|
||||
|
||||
/** 만료까지 남은 초. 이미 만료됐으면 음수. */
|
||||
Long remainSec;
|
||||
|
||||
// ---- 토큰 정보 ----
|
||||
|
||||
/** 앞 8자만 남기고 마스킹한 accessToken */
|
||||
String accessTokenMasked;
|
||||
|
||||
/** accessToken 전체 길이 */
|
||||
Integer accessTokenLength;
|
||||
|
||||
String tokenType;
|
||||
|
||||
String scope;
|
||||
|
||||
String clientId;
|
||||
|
||||
/** 조회 불가 사유 등 부가 메시지 */
|
||||
String message;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.eactive.eai.manage.oauthtoken;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
|
||||
/**
|
||||
* 아웃바운드 OAuth 토큰 현황 조회 서비스.
|
||||
*
|
||||
* 캐시에 있는 토큰만 읽는다(peek). 조회 때문에 토큰이 새로 발급되지 않도록
|
||||
* AccessTokenManagerByDB.getAccessTokenVO() 가 아니라 peekAccessTokenVO() 를 쓴다.
|
||||
*
|
||||
* accessToken 은 앞 8자만 남기고 마스킹하며 clientSecret 은 담지 않는다.
|
||||
*/
|
||||
@Service
|
||||
public class OAuthTokenStatusService {
|
||||
|
||||
private static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/** 토큰 발급 구현체 클래스명을 담고 있는 어댑터 속성 키 */
|
||||
private static final String PROP_TOKEN_SERVICE_CLASS = "ADAPTER_TOKEN_ISSUING_CLIENT_TYPE";
|
||||
|
||||
/** accessToken 중 노출할 앞자리 수 */
|
||||
private static final int UNMASKED_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* 등록된 모든 어댑터그룹의 토큰 현황을 반환한다.
|
||||
*
|
||||
* @return 어댑터그룹명 순으로 정렬된 현황 목록
|
||||
*/
|
||||
public List<OAuthTokenStatusDTO> getStatusList() {
|
||||
List<OAuthTokenStatusDTO> result = new ArrayList<OAuthTokenStatusDTO>();
|
||||
for (String adapterGroupName : AccessTokenManagerByDB.getInstance().getRegisteredAdapterGroupNames()) {
|
||||
result.add(getStatus(adapterGroupName));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터그룹 하나의 토큰 현황을 반환한다.
|
||||
*
|
||||
* @param adapterGroupName 어댑터그룹명
|
||||
* @return 토큰 현황. 등록돼 있지 않으면 message 만 채워서 반환한다.
|
||||
*/
|
||||
public OAuthTokenStatusDTO getStatus(String adapterGroupName) {
|
||||
OAuthTokenStatusDTO dto = new OAuthTokenStatusDTO();
|
||||
dto.setAdapterGroupName(adapterGroupName);
|
||||
|
||||
AccessTokenManagerByDB manager = AccessTokenManagerByDB.getInstance();
|
||||
OutboundOAuthCredentialVo credential = manager.getOutboundOAuthCredentialVo(adapterGroupName);
|
||||
|
||||
if (credential == null) {
|
||||
dto.setMessage("등록된 OAuth 인증 정보가 없습니다.");
|
||||
return dto;
|
||||
}
|
||||
|
||||
dto.setUseYn(credential.getUseYn());
|
||||
dto.setTokenUrl(credential.getUrl());
|
||||
dto.setIntervalSec(credential.getIntervalSec());
|
||||
dto.setTokenServiceClass(findTokenServiceClass(adapterGroupName));
|
||||
|
||||
AccessTokenVO accessToken;
|
||||
try {
|
||||
accessToken = manager.peekAccessTokenVO(adapterGroupName);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
dto.setMessage("현재 SessionManager 백엔드는 아웃바운드 토큰 캐시를 지원하지 않습니다.");
|
||||
return dto;
|
||||
} catch (Exception e) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("토큰 캐시 조회 실패. adapterGroupName : " + adapterGroupName, e);
|
||||
}
|
||||
dto.setMessage("토큰 캐시 조회 실패 : " + e.getMessage());
|
||||
return dto;
|
||||
}
|
||||
|
||||
if (accessToken == null) {
|
||||
dto.setMessage("캐시에 토큰이 없습니다.");
|
||||
return dto;
|
||||
}
|
||||
|
||||
dto.setCached(true);
|
||||
dto.setExpired(accessToken.isExpired());
|
||||
dto.setAccessTokenMasked(mask(accessToken.getAccessToken()));
|
||||
dto.setAccessTokenLength(accessToken.getAccessToken() == null ? 0 : accessToken.getAccessToken().length());
|
||||
|
||||
Date expiration = accessToken.getExpiration();
|
||||
if (expiration != null) {
|
||||
dto.setExpiration(new SimpleDateFormat(DATE_FORMAT).format(expiration));
|
||||
dto.setRemainSec((expiration.getTime() - System.currentTimeMillis()) / 1000L);
|
||||
}
|
||||
|
||||
if (accessToken instanceof OAuth2AccessTokenVO) {
|
||||
OAuth2AccessTokenVO oauth2Token = (OAuth2AccessTokenVO) accessToken;
|
||||
dto.setTokenType(oauth2Token.getTokenType());
|
||||
dto.setScope(oauth2Token.getScope());
|
||||
dto.setClientId(oauth2Token.getClientId());
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터 속성에서 토큰 발급 구현체 클래스명을 찾는다.
|
||||
*
|
||||
* @param adapterGroupName 어댑터그룹명
|
||||
* @return 구현체 클래스명. 찾지 못하면 null.
|
||||
*/
|
||||
private String findTokenServiceClass(String adapterGroupName) {
|
||||
try {
|
||||
AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
|
||||
if (gvo == null || !gvo.getAdapters().hasNext()) {
|
||||
return null;
|
||||
}
|
||||
AdapterVO avo = gvo.getAdapters().next();
|
||||
Properties properties = AdapterPropManager.getInstance().getProperties(avo.getPropGroupName());
|
||||
return properties.getProperty(PROP_TOKEN_SERVICE_CLASS);
|
||||
} catch (Exception e) {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("토큰 발급 구현체 조회 실패. adapterGroupName : " + adapterGroupName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* accessToken 앞 8자만 남기고 마스킹한다.
|
||||
*
|
||||
* @param accessToken 토큰 값
|
||||
* @return 마스킹된 토큰 값
|
||||
*/
|
||||
private String mask(String accessToken) {
|
||||
if (StringUtils.isBlank(accessToken)) {
|
||||
return null;
|
||||
}
|
||||
if (accessToken.length() <= UNMASKED_LENGTH) {
|
||||
return StringUtils.repeat('*', accessToken.length());
|
||||
}
|
||||
return StringUtils.substring(accessToken, 0, UNMASKED_LENGTH) + "***";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.eactive.eai.manage.oauthtoken;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
|
||||
/**
|
||||
* OAuthTokenStatusService 단위테스트
|
||||
*
|
||||
* AccessTokenManagerByDB 는 ApplicationContextProvider 를 통해 조회되므로
|
||||
* mock ApplicationContext 를 리플렉션으로 주입해 격리한다.
|
||||
*/
|
||||
class OAuthTokenStatusServiceTest {
|
||||
|
||||
private static final String GROUP = "TESTGRP";
|
||||
|
||||
private OAuthTokenStatusService service;
|
||||
private AccessTokenManagerByDB mockManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
service = new OAuthTokenStatusService();
|
||||
mockManager = Mockito.mock(AccessTokenManagerByDB.class);
|
||||
|
||||
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
|
||||
Mockito.when(mockCtx.getBean(AccessTokenManagerByDB.class)).thenReturn(mockManager);
|
||||
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
|
||||
ctxField.setAccessible(true);
|
||||
ctxField.set(null, mockCtx);
|
||||
}
|
||||
|
||||
/** 인증 정보가 등록된 것으로 mock 설정한다. */
|
||||
private OutboundOAuthCredentialVo registerCredential(String adapterGroupName) {
|
||||
OutboundOAuthCredentialVo credential = new OutboundOAuthCredentialVo();
|
||||
credential.setAdapterGroupName(adapterGroupName);
|
||||
credential.setUseYn("Y");
|
||||
credential.setUrl("https://token.example.com/oauth/token");
|
||||
credential.setIntervalSec(1800);
|
||||
credential.setClientId("client-1");
|
||||
credential.setClientSecret("super-secret");
|
||||
Mockito.when(mockManager.getOutboundOAuthCredentialVo(adapterGroupName)).thenReturn(credential);
|
||||
return credential;
|
||||
}
|
||||
|
||||
/** 만료까지 expiresInSec 남은 토큰 */
|
||||
private OAuth2AccessTokenVO token(String accessToken, long expiresInSec) {
|
||||
OAuth2AccessTokenVO vo = new OAuth2AccessTokenVO();
|
||||
vo.setAccessToken(accessToken);
|
||||
vo.setTokenType("Bearer");
|
||||
vo.setScope("read");
|
||||
vo.setClientId("client-1");
|
||||
vo.setExpiration(new Date(System.currentTimeMillis() + expiresInSec * 1000L));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1. 등록되지 않은 어댑터그룹이면 사유만 반환한다")
|
||||
void 미등록그룹() {
|
||||
Mockito.when(mockManager.getOutboundOAuthCredentialVo(anyString())).thenReturn(null);
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertEquals(GROUP, dto.getAdapterGroupName());
|
||||
assertFalse(dto.isCached());
|
||||
assertNotNull(dto.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2. 캐시에 토큰이 없으면 등록 정보만 반환한다")
|
||||
void 캐시에토큰없음() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(null);
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertFalse(dto.isCached());
|
||||
assertEquals("Y", dto.getUseYn());
|
||||
assertEquals(1800, dto.getIntervalSec());
|
||||
assertEquals("https://token.example.com/oauth/token", dto.getTokenUrl());
|
||||
assertNotNull(dto.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3. 토큰 캐시를 지원하지 않는 백엔드면 사유를 담아 반환한다 (Ehcache)")
|
||||
void 백엔드미지원() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenThrow(new UnsupportedOperationException());
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertFalse(dto.isCached());
|
||||
assertTrue(dto.getMessage().contains("지원하지 않습니다"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4. 캐시된 토큰의 만료/잔여시간/부가정보를 반환한다")
|
||||
void 캐시된토큰() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijklmnop", 600));
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertTrue(dto.isCached());
|
||||
assertFalse(dto.isExpired());
|
||||
assertNotNull(dto.getExpiration());
|
||||
assertTrue(dto.getRemainSec() > 0 && dto.getRemainSec() <= 600);
|
||||
assertEquals("Bearer", dto.getTokenType());
|
||||
assertEquals("read", dto.getScope());
|
||||
assertEquals("client-1", dto.getClientId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5. accessToken 은 앞 8자만 남기고 마스킹한다")
|
||||
void 토큰마스킹() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijklmnop", 600));
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertEquals("abcdefgh***", dto.getAccessTokenMasked());
|
||||
assertEquals(16, dto.getAccessTokenLength());
|
||||
assertFalse(dto.getAccessTokenMasked().contains("ijklmnop"), "토큰 뒷부분이 노출되면 안 된다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6. 8자 이하 토큰은 전부 마스킹한다")
|
||||
void 짧은토큰마스킹() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abc", 600));
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertEquals("***", dto.getAccessTokenMasked());
|
||||
assertEquals(3, dto.getAccessTokenLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7. 만료된 토큰은 expired=true, 잔여시간 음수")
|
||||
void 만료된토큰() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", -60));
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertTrue(dto.isCached());
|
||||
assertTrue(dto.isExpired());
|
||||
assertTrue(dto.getRemainSec() < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("8. 응답에 clientSecret 은 담기지 않는다")
|
||||
void secret비노출() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", 600));
|
||||
|
||||
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||
|
||||
assertFalse(dto.toString().contains("super-secret"), "clientSecret 이 응답에 포함되면 안 된다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("9. 전체 목록은 등록된 어댑터그룹 수만큼 반환한다")
|
||||
void 전체목록() {
|
||||
Set<String> groups = new LinkedHashSet<>(Arrays.asList("GRP-A", "GRP-B"));
|
||||
Mockito.when(mockManager.getRegisteredAdapterGroupNames()).thenReturn(groups);
|
||||
registerCredential("GRP-A");
|
||||
registerCredential("GRP-B");
|
||||
Mockito.when(mockManager.peekAccessTokenVO("GRP-A")).thenReturn(token("abcdefghijkl", 600));
|
||||
Mockito.when(mockManager.peekAccessTokenVO("GRP-B")).thenReturn(null);
|
||||
|
||||
List<OAuthTokenStatusDTO> list = service.getStatusList();
|
||||
|
||||
assertEquals(2, list.size());
|
||||
assertEquals("GRP-A", list.get(0).getAdapterGroupName());
|
||||
assertTrue(list.get(0).isCached());
|
||||
assertFalse(list.get(1).isCached());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10. 조회가 토큰 발급을 유발하지 않는다 (peek 만 호출)")
|
||||
void 발급유발없음() {
|
||||
registerCredential(GROUP);
|
||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", 600));
|
||||
|
||||
service.getStatus(GROUP);
|
||||
|
||||
Mockito.verify(mockManager).peekAccessTokenVO(GROUP);
|
||||
Mockito.verify(mockManager, Mockito.never()).getAccessTokenVO(anyString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user