mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
Merge pull request #607 from gale-popai/fix/device-auth-redis-typing
fix(auth): read device-code state via ObjectMapper conversion, not cast
This commit is contained in:
commit
19c3070291
3 changed files with 125 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.auth.device;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenService;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
|
@ -33,14 +34,17 @@ public class DeviceAuthService {
|
|||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final ApiTokenService apiTokenService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String verificationUri;
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
public DeviceAuthService(RedisTemplate<String, Object> redisTemplate,
|
||||
ApiTokenService apiTokenService,
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${skillhub.device-auth.verification-uri:/cli/auth}") String verificationUri) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.apiTokenService = apiTokenService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.verificationUri = verificationUri;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +75,7 @@ public class DeviceAuthService {
|
|||
throw new DomainBadRequestException("error.deviceAuth.userCode.invalid");
|
||||
}
|
||||
|
||||
DeviceCodeData data = (DeviceCodeData) redisTemplate.opsForValue().get(DEVICE_CODE_PREFIX + deviceCode);
|
||||
DeviceCodeData data = readDeviceCodeData(deviceCode);
|
||||
if (data == null) {
|
||||
throw new DomainBadRequestException("error.deviceAuth.deviceCode.expired");
|
||||
}
|
||||
|
|
@ -97,7 +101,7 @@ public class DeviceAuthService {
|
|||
* into an API token exactly once.
|
||||
*/
|
||||
public DeviceTokenResponse pollToken(String deviceCode) {
|
||||
DeviceCodeData data = (DeviceCodeData) redisTemplate.opsForValue().get(DEVICE_CODE_PREFIX + deviceCode);
|
||||
DeviceCodeData data = readDeviceCodeData(deviceCode);
|
||||
|
||||
if (data == null) {
|
||||
throw new DomainBadRequestException("error.deviceAuth.deviceCode.invalid");
|
||||
|
|
@ -147,6 +151,17 @@ public class DeviceAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads device-code state from Redis. The shared template's JSON value
|
||||
* serializer carries no type information, so values deserialize as plain
|
||||
* maps; convert explicitly instead of casting (a direct cast throws
|
||||
* {@code ClassCastException} on every read).
|
||||
*/
|
||||
private DeviceCodeData readDeviceCodeData(String deviceCode) {
|
||||
Object raw = redisTemplate.opsForValue().get(DEVICE_CODE_PREFIX + deviceCode);
|
||||
return raw == null ? null : objectMapper.convertValue(raw, DeviceCodeData.class);
|
||||
}
|
||||
|
||||
private String generateRandomDeviceCode() {
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ public class DeviceCodeData implements Serializable {
|
|||
}
|
||||
|
||||
public String getDeviceCode() { return deviceCode; }
|
||||
public void setDeviceCode(String deviceCode) { this.deviceCode = deviceCode; }
|
||||
public String getUserCode() { return userCode; }
|
||||
public void setUserCode(String userCode) { this.userCode = userCode; }
|
||||
public DeviceCodeStatus getStatus() { return status; }
|
||||
public void setStatus(DeviceCodeStatus status) { this.status = status; }
|
||||
public String getUserId() { return userId; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.iflytek.skillhub.auth.device;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iflytek.skillhub.auth.token.ApiTokenService;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DeviceAuthServiceTest {
|
||||
|
||||
private static final String DEVICE_CODE = "device-code-1";
|
||||
private static final String USER_CODE = "ABCD-2345";
|
||||
|
||||
@Mock
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private ValueOperations<String, Object> valueOperations;
|
||||
|
||||
@Mock
|
||||
private ApiTokenService apiTokenService;
|
||||
|
||||
private DeviceAuthService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
service = new DeviceAuthService(redisTemplate, apiTokenService, new ObjectMapper(), "/cli/auth");
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared RedisTemplate's JSON serializer keeps no type information, so
|
||||
* stored DeviceCodeData comes back as a plain map. A typed cast used to
|
||||
* throw ClassCastException on every poll; the service must convert instead.
|
||||
*/
|
||||
private static Map<String, Object> storedDeviceCode(DeviceCodeStatus status, String userId) {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
raw.put("deviceCode", DEVICE_CODE);
|
||||
raw.put("userCode", USER_CODE);
|
||||
raw.put("status", status.name());
|
||||
raw.put("userId", userId);
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollTokenReturnsPendingWhenRedisValueIsUntypedMap() {
|
||||
when(valueOperations.get("device:code:" + DEVICE_CODE))
|
||||
.thenReturn(storedDeviceCode(DeviceCodeStatus.PENDING, null));
|
||||
|
||||
DeviceTokenResponse response = service.pollToken(DEVICE_CODE);
|
||||
|
||||
assertThat(response.error()).isEqualTo("authorization_pending");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollTokenRedeemsAuthorizedCodeFromUntypedMap() {
|
||||
when(valueOperations.get("device:code:" + DEVICE_CODE))
|
||||
.thenReturn(storedDeviceCode(DeviceCodeStatus.AUTHORIZED, "usr_1"));
|
||||
when(valueOperations.setIfAbsent(eq("device:claim:" + DEVICE_CODE), any(), anyLong(), any()))
|
||||
.thenReturn(Boolean.TRUE);
|
||||
when(apiTokenService.rotateToken(eq("usr_1"), any(), any()))
|
||||
.thenReturn(new ApiTokenService.TokenCreateResult("sk_test_token", null));
|
||||
|
||||
DeviceTokenResponse response = service.pollToken(DEVICE_CODE);
|
||||
|
||||
assertThat(response.accessToken()).isEqualTo("sk_test_token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollTokenRejectsUnknownDeviceCode() {
|
||||
when(valueOperations.get("device:code:" + DEVICE_CODE)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.pollToken(DEVICE_CODE))
|
||||
.isInstanceOf(DomainBadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizeDeviceCodeMarksPendingCodeFromUntypedMap() {
|
||||
when(valueOperations.get("device:usercode:" + USER_CODE)).thenReturn(DEVICE_CODE);
|
||||
when(valueOperations.get("device:code:" + DEVICE_CODE))
|
||||
.thenReturn(storedDeviceCode(DeviceCodeStatus.PENDING, null));
|
||||
|
||||
service.authorizeDeviceCode(USER_CODE, "usr_1");
|
||||
|
||||
verify(valueOperations).set(startsWith("device:code:"), any(DeviceCodeData.class), anyLong(), any());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue