优化
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
package com.baiyue.auth.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DualTrackJwtConfigurationTest {
|
||||
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(DomesticJwtConfiguration.class, OverseasJwtConfiguration.class))
|
||||
.withBean(AuthSecurityProperties.class, () -> new AuthSecurityProperties(
|
||||
"domestic-secret-at-least-32-bytes-long", "auth-service", "lastleg-api",
|
||||
java.time.Duration.ofMinutes(24), java.time.Duration.ofDays(7), 5,
|
||||
java.time.Duration.ofMinutes(15)))
|
||||
.withPropertyValues(
|
||||
"baiyue.security.overseas-jwt.enabled=false");
|
||||
|
||||
@Test
|
||||
void startsDomesticEncoderWithoutRsaConfiguration() {
|
||||
runner.run(context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
assertThat(context).hasBean("jwtEncoder");
|
||||
assertThat(context).doesNotHaveBean("overseasJwtEncoder");
|
||||
assertThat(context).hasSingleBean(JwtEncoder.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.auth.config;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class UnifiedJwtConfigurationTest {
|
||||
@Test
|
||||
void signsWithCurrentKeyAndRetainsPreviousPublicKeyForRotation() throws Exception {
|
||||
var current = new RSAKeyGenerator(2048).keyID("current").algorithm(JWSAlgorithm.RS256).generate();
|
||||
var previous = new RSAKeyGenerator(2048).keyID("previous").algorithm(JWSAlgorithm.RS256).generate();
|
||||
var properties = new UnifiedJwtProperties(new JWKSet(List.of(current, previous.toPublicJWK())).toString(false), "current");
|
||||
var encoder = new UnifiedJwtConfiguration().jwtEncoder(properties);
|
||||
var now = Instant.now();
|
||||
var token = encoder.encode(JwtEncoderParameters.from(
|
||||
JwsHeader.with(SignatureAlgorithm.RS256).keyId("current").build(),
|
||||
JwtClaimsSet.builder().subject("42").issuedAt(now).expiresAt(now.plusSeconds(60)).build()));
|
||||
var decoded = NimbusJwtDecoder.withPublicKey(current.toRSAPublicKey()).build().decode(token.getTokenValue());
|
||||
assertThat(decoded.getSubject()).isEqualTo("42");
|
||||
assertThat(decoded.getHeaders()).containsEntry("kid", "current").containsEntry("alg", "RS256");
|
||||
assertThatThrownBy(() -> NimbusJwtDecoder.withPublicKey(previous.toRSAPublicKey()).build().decode(token.getTokenValue()))
|
||||
.isInstanceOf(JwtException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingPrivateKeyAndPreviousPrivateKey() throws Exception {
|
||||
var current = new RSAKeyGenerator(2048).keyID("current").generate();
|
||||
var previous = new RSAKeyGenerator(2048).keyID("previous").generate();
|
||||
var config = new UnifiedJwtConfiguration();
|
||||
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties(new JWKSet(current.toPublicJWK()).toString(), "current")))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties(new JWKSet(List.of(current, previous)).toString(false), "current")))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
assertThatThrownBy(() -> config.jwtEncoder(new UnifiedJwtProperties("", "current")))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.baiyue.auth.controller;
|
||||
|
||||
import com.baiyue.auth.config.OverseasJwtProperties;
|
||||
import com.baiyue.auth.config.UnifiedJwtProperties;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
@@ -20,8 +20,7 @@ class JwksControllerTest {
|
||||
RSAKey current = key("current", true);
|
||||
RSAKey previous = key("previous", false);
|
||||
String json = new JWKSet(List.of(current, previous)).toString(false);
|
||||
var properties = new OverseasJwtProperties(true, "auth-service", "lastleg-api", json,
|
||||
"current", Duration.ofMinutes(15));
|
||||
var properties = new UnifiedJwtProperties(json, "current");
|
||||
|
||||
var body = new JwksController(properties).jwks().getBody();
|
||||
String published = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(body);
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
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;
|
||||
@@ -37,21 +37,24 @@ class AuthServiceSmallJwtTest {
|
||||
@MockBean private JwtEncoder jwtEncoder;
|
||||
@MockBean private AuthorizationLoader authorizationLoader;
|
||||
@MockBean private TokenSessionStore tokenSessionStore;
|
||||
@MockBean private OverseasTokenService overseasTokenService;
|
||||
@Resource private AuthService service;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
com.baiyue.auth.config.UnifiedJwtProperties signingProperties() {
|
||||
return new com.baiyue.auth.config.UnifiedJwtProperties("unused", "current");
|
||||
}
|
||||
@Bean
|
||||
AuthSecurityProperties properties() {
|
||||
return new AuthSecurityProperties("unused", "auth-service", "lastleg-api",
|
||||
return new AuthSecurityProperties("auth-service", "lastleg-api",
|
||||
Duration.ofMinutes(30),
|
||||
Duration.ofDays(7), 5, Duration.ofMinutes(10));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void signsCrossRegionClaimsAndStoresSession() {
|
||||
void signsUnifiedClaimsAndStoresSession() {
|
||||
AuthAccount account = new AuthAccount();
|
||||
account.setId(42L);
|
||||
account.setUsername("operator");
|
||||
@@ -71,9 +74,6 @@ class AuthServiceSmallJwtTest {
|
||||
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||
when(encoded.getTokenValue()).thenReturn("small-token");
|
||||
when(overseasTokenService.issue(account, snapshot)).thenReturn(
|
||||
new OverseasTokenService.OverseasTokenResponse("Bearer", "overseas-token", 900,
|
||||
snapshot.getRoles(), snapshot.getPermissions()));
|
||||
|
||||
var response = service.login(new LoginRequest("operator", "secret"));
|
||||
|
||||
@@ -89,16 +89,13 @@ class AuthServiceSmallJwtTest {
|
||||
.containsEntry("overseas_warehouse_id", 900L)
|
||||
.doesNotContainKeys("roles", "authorities", "permissions", "scope");
|
||||
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
|
||||
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(MacAlgorithm.HS256);
|
||||
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isNull();
|
||||
assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
|
||||
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isEqualTo("current");
|
||||
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("OVERSEAS_WAREHOUSE_PDA_OPERATOR")),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
||||
verify(overseasTokenService).issue(account, snapshot);
|
||||
assertThat(response.overseasAccessToken()).isEqualTo("overseas-token");
|
||||
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||
assertThat(response.roles()).containsExactly("OVERSEAS_WAREHOUSE_PDA_OPERATOR");
|
||||
assertThat(response.authorities()).containsExactly("platform:order:list");
|
||||
}
|
||||
@@ -144,7 +141,7 @@ class AuthServiceSmallJwtTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() {
|
||||
void refreshRotatesUnifiedTokenFromCurrentAuthorizationSnapshot() {
|
||||
AuthAccount account = new AuthAccount();
|
||||
account.setId(52L);
|
||||
account.setUsername("merchant");
|
||||
@@ -164,9 +161,6 @@ class AuthServiceSmallJwtTest {
|
||||
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||
when(encoded.getTokenValue()).thenReturn("domestic-refreshed");
|
||||
when(overseasTokenService.issue(account, snapshot)).thenReturn(
|
||||
new OverseasTokenService.OverseasTokenResponse("Bearer", "overseas-refreshed", 900,
|
||||
snapshot.getRoles(), snapshot.getPermissions()));
|
||||
|
||||
var response = service.refresh("refresh-old");
|
||||
|
||||
@@ -175,8 +169,6 @@ class AuthServiceSmallJwtTest {
|
||||
assertThat(response.expiresIn()).isEqualTo(1800);
|
||||
assertThat(response.refreshToken()).isNotBlank().isNotEqualTo("refresh-old");
|
||||
assertThat(response.refreshExpiresIn()).isEqualTo(604800);
|
||||
assertThat(response.overseasAccessToken()).isEqualTo("overseas-refreshed");
|
||||
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||
assertThat(response.roles()).containsExactly("MERCHANT_ADMIN");
|
||||
assertThat(response.authorities()).containsExactly("merchant:order:list");
|
||||
verify(tokenSessionStore).consume("refresh-old");
|
||||
@@ -187,7 +179,6 @@ class AuthServiceSmallJwtTest {
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
||||
verify(authorizationLoader).load(account);
|
||||
verify(overseasTokenService).issue(account, snapshot);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -200,7 +191,6 @@ class AuthServiceSmallJwtTest {
|
||||
|
||||
verify(repository, never()).findById(any());
|
||||
verify(jwtEncoder, never()).encode(any());
|
||||
verify(overseasTokenService, never()).issue(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -225,14 +215,9 @@ class AuthServiceSmallJwtTest {
|
||||
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||
when(encoded.getTokenValue()).thenReturn("small-impersonation-token");
|
||||
when(overseasTokenService.issue(account, snapshot)).thenReturn(
|
||||
new OverseasTokenService.OverseasTokenResponse("Bearer", "overseas-impersonation-token", 900,
|
||||
Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list")));
|
||||
|
||||
AuthService.ImpersonationTokenResponse response = service.exchangeImpersonationTicket("one-time-code");
|
||||
|
||||
assertThat(response.overseasAccessToken()).isEqualTo("overseas-impersonation-token");
|
||||
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||
assertThat(response.roles()).containsExactly("MERCHANT_OWNER");
|
||||
assertThat(response.authorities()).containsExactly("merchant:order:list");
|
||||
|
||||
@@ -247,6 +232,5 @@ class AuthServiceSmallJwtTest {
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("MERCHANT_OWNER")),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("merchant:order:list")),
|
||||
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(15)));
|
||||
verify(overseasTokenService).issue(account, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
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:list";
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user