140 lines
7.5 KiB
Java
140 lines
7.5 KiB
Java
package com.baiyue.auth.service;
|
|
|
|
import com.baiyue.auth.client.AuthorizationLoader;
|
|
import com.baiyue.auth.client.AuthorizationSnapshot;
|
|
import com.baiyue.auth.config.OverseasJwtProperties;
|
|
import com.baiyue.auth.entity.AuthAccount;
|
|
import com.baiyue.auth.repository.AuthAccountRepository;
|
|
import jakarta.annotation.Resource;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
|
import org.springframework.security.oauth2.jwt.Jwt;
|
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|
import org.springframework.test.context.TestPropertySource;
|
|
|
|
import java.time.Duration;
|
|
import java.util.Set;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
@SpringJUnitConfig(classes = {OverseasTokenService.class, OverseasTokenServiceTest.Config.class})
|
|
@TestPropertySource(properties = "baiyue.security.overseas-jwt.enabled=true")
|
|
class OverseasTokenServiceTest {
|
|
@MockBean private AuthAccountRepository accounts;
|
|
@MockBean private AuthorizationLoader authorizationLoader;
|
|
@MockBean(name = "overseasJwtEncoder") private JwtEncoder encoder;
|
|
@Resource private OverseasTokenService service;
|
|
|
|
@Configuration(proxyBeanMethods = false)
|
|
static class Config {
|
|
@Bean OverseasJwtProperties properties() {
|
|
return new OverseasJwtProperties(true, "auth-service", "lastleg-api", "{}", "current-kid",
|
|
Duration.ofMinutes(15));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void signsShortRs256TokenWithCrossRegionClaims() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(42L); account.setAccountStatus("ACTIVE"); account.setDefaultTenantId(999L);
|
|
account.setDefaultMembershipId(84L); account.setDefaultAppCode("MERCHANT");
|
|
String authority = "platform:overseas-master-data:quote:list";
|
|
String merchantAuthority = "merchant:warehouse:delivery:list";
|
|
String merchantCrossBorderAuthority = "merchant:cross-border:procurement";
|
|
String lastlegAuthority = "lastleg:pda:operate";
|
|
String financeAuthority = "platform:finance:fee-source:edit";
|
|
String domesticAuthority = "platform:firstleg:order:list";
|
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM", "WEB",
|
|
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"),
|
|
Set.of(authority, merchantAuthority, merchantCrossBorderAuthority, lastlegAuthority,
|
|
financeAuthority, domesticAuthority), 7L, 900L);
|
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
|
when(accounts.findById(42L)).thenReturn(account);
|
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
|
when(encoder.encode(any())).thenReturn(encoded);
|
|
when(encoded.getTokenValue()).thenReturn("overseas-token");
|
|
|
|
var response = service.exchange(42L);
|
|
|
|
ArgumentCaptor<JwtEncoderParameters> captor = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
|
verify(encoder).encode(captor.capture());
|
|
var parameters = captor.getValue();
|
|
assertThat(parameters.getJwsHeader().getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
|
|
assertThat(parameters.getJwsHeader().getKeyId()).isEqualTo("current-kid");
|
|
assertThat(parameters.getClaims().getAudience()).containsExactly("lastleg-api");
|
|
assertThat(parameters.getClaims().getClaims())
|
|
.containsEntry("user_id", 42L).containsEntry("tenant_id", 0L)
|
|
.containsEntry("membership_id", 84L)
|
|
.containsEntry("app_code", "PLATFORM").containsEntry("client_type", "WEB")
|
|
.containsEntry("permission_version", 7L)
|
|
.containsEntry("token_version", 0)
|
|
.containsEntry("overseas_warehouse_id", 900L)
|
|
.doesNotContainKeys("roles", "authorities", "permissions", "userId", "tenantId", "appCode",
|
|
"permissionVersion");
|
|
assertThat(response.expiresIn()).isEqualTo(900);
|
|
assertThat(response.roles()).containsExactly("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
|
assertThat(response.authorities()).containsExactlyInAnyOrder(authority, merchantAuthority,
|
|
merchantCrossBorderAuthority, lastlegAuthority, financeAuthority, domesticAuthority);
|
|
}
|
|
|
|
@Test
|
|
void refreshedAuthorizationSnapshotReplacesWarehouseBindingAndTokenIdentity() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(43L); account.setAccountStatus("ACTIVE"); account.setDefaultTenantId(0L);
|
|
account.setDefaultMembershipId(85L); account.setDefaultAppCode("PLATFORM");
|
|
account.setTokenVersion(8);
|
|
AuthorizationSnapshot oldBinding = new AuthorizationSnapshot(43L, 0L, 85L, "PLATFORM", "PDA",
|
|
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 8L, 900L);
|
|
AuthorizationSnapshot newBinding = new AuthorizationSnapshot(43L, 0L, 85L, "PLATFORM", "PDA",
|
|
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 9L, 901L);
|
|
Jwt first = org.mockito.Mockito.mock(Jwt.class);
|
|
Jwt second = org.mockito.Mockito.mock(Jwt.class);
|
|
when(encoder.encode(any())).thenReturn(first, second);
|
|
when(first.getTokenValue()).thenReturn("overseas-old-binding");
|
|
when(second.getTokenValue()).thenReturn("overseas-new-binding");
|
|
|
|
var oldToken = service.issue(account, oldBinding);
|
|
var newToken = service.issue(account, newBinding);
|
|
|
|
ArgumentCaptor<JwtEncoderParameters> captor = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
|
verify(encoder, times(2)).encode(captor.capture());
|
|
var oldClaims = captor.getAllValues().get(0).getClaims();
|
|
var newClaims = captor.getAllValues().get(1).getClaims();
|
|
assertThat(oldClaims.getId()).isNotEqualTo(newClaims.getId());
|
|
assertThat(oldClaims.getClaims()).containsEntry("overseas_warehouse_id", 900L)
|
|
.containsEntry("permission_version", 8L);
|
|
assertThat(newClaims.getClaims()).containsEntry("overseas_warehouse_id", 901L)
|
|
.containsEntry("permission_version", 9L);
|
|
assertThat(newClaims.getClaims()).containsEntry("token_version", 8);
|
|
assertThat(oldToken.accessToken()).isNotEqualTo(newToken.accessToken());
|
|
}
|
|
|
|
@Test
|
|
void unboundWarehouseIsOmittedSoPdaIdentityMustFailClosed() {
|
|
AuthAccount account = new AuthAccount();
|
|
account.setId(44L); account.setAccountStatus("ACTIVE");
|
|
AuthorizationSnapshot unbound = new AuthorizationSnapshot(44L, 0L, 86L, "PLATFORM", "PDA",
|
|
Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR"), Set.of("lastleg:pda:operate"), 10L, null);
|
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
|
when(encoder.encode(any())).thenReturn(encoded);
|
|
when(encoded.getTokenValue()).thenReturn("overseas-unbound");
|
|
|
|
service.issue(account, unbound);
|
|
|
|
ArgumentCaptor<JwtEncoderParameters> captor = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
|
verify(encoder).encode(captor.capture());
|
|
assertThat(captor.getValue().getClaims().getClaims())
|
|
.doesNotContainKey("overseas_warehouse_id");
|
|
}
|
|
}
|