Changed from JWT token to public/privat key

This commit is contained in:
Andreas Dahm committed 2026-05-12 08:57:12 +02:00
1 parent b7efe27a54
commit dbea8922d5
5 files changed
+135 -18

No files matched your search

+3 -3
View File
@@ -97,9 +97,9 @@ Currently active rules cover: cycles, controller→repository direction, reposit
### Security ### Security
- **Drop the shared `JWT_SECRET`.** Today both Auth and Core hold the same HMAC secret, which means a Core compromise lets an attacker forge tokens for Auth. Once Auth ships EdDSA signing (see Auth README roadmap), Core should hold only the **public key** (`JWT_PUBLIC_KEY`) and verify with `Jwts.parser().verifyWith(publicKey).build()`. Touches `JwtConfig` (rename `secret` → `publicKey`), `JwtService` (replace `Keys.hmacShaKeyFor` with `KeyFactory.getInstance("Ed25519")`), and the deployment env wiring. - ~~**Drop the shared `JWT_SECRET`.**~~ Done. Core now holds only `JWT_PUBLIC_KEY` and verifies with the Ed25519 public key via `Jwts.parser().verifyWith(publicKey).build()`. `JwtConfig.secret` removed.
- **Consume a JWKS endpoint** once Auth exposes one (`GET https://auth.tbp.andreas-dahm.eu/.well-known/jwks.json`). Cache keys at startup, refresh on `kid` miss. Avoids redeploying Core when keys rotate. - ~~**Add a `JwtServiceTest`.**~~ Done. `src/test/java/core_service/configuration/security/JwtServiceTest.java` covers extract / valid / wrong-key paths with an Ed25519 fixture.
- **Add a `JwtServiceTest`.** No verification tests exist for Core's JWT parser today; adding one is also a natural place to add the EdDSA key fixture. - **Consume a JWKS endpoint** once Auth exposes one (`GET https://auth.tbp.andreas-dahm.eu/.well-known/jwks.json`). Cache keys at startup, refresh on `kid` miss. Avoids redeploying Core when keys rotate. With the `kid` header now present in issued tokens (`JWT_KID`), this is unblocked.
### Testing ### Testing
@@ -11,6 +11,6 @@ import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "security.jwt") @ConfigurationProperties(prefix = "security.jwt")
public class JwtConfig { public class JwtConfig {
private String secret; /** Base64-encoded X.509 DER Ed25519 public key. Verify-only; private key lives in Auth. */
private String publicKey;
} }
@@ -1,24 +1,37 @@
package core_service.configuration.security; package core_service.configuration.security;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.crypto.SecretKey; import java.security.KeyFactory;
import java.nio.charset.StandardCharsets; import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* Verifies JWTs issued by tbp-auth-service. Verify-only: holds the Ed25519 public key.
*/
@Service @Service
public class JwtService { public class JwtService {
private static final String ED25519 = "Ed25519";
private final JwtConfig jwtConfig; private final JwtConfig jwtConfig;
private PublicKey publicKey;
public JwtService(JwtConfig jwtConfig) { public JwtService(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig; this.jwtConfig = jwtConfig;
} }
@PostConstruct
void init() {
this.publicKey = decodePublicKey(jwtConfig.getPublicKey());
}
public String extractEmail(String token) { public String extractEmail(String token) {
return Jwts.parser() return Jwts.parser()
.verifyWith(getSigningKey()) .verifyWith(publicKey)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.getPayload() .getPayload()
@@ -27,7 +40,7 @@ public class JwtService {
public String extractUserId(String token) { public String extractUserId(String token) {
return Jwts.parser() return Jwts.parser()
.verifyWith(getSigningKey()) .verifyWith(publicKey)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.getPayload() .getPayload()
@@ -36,7 +49,7 @@ public class JwtService {
public Long extractSteamId(String token) { public Long extractSteamId(String token) {
return Jwts.parser() return Jwts.parser()
.verifyWith(getSigningKey()) .verifyWith(publicKey)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.getPayload() .getPayload()
@@ -46,7 +59,7 @@ public class JwtService {
public boolean isTokenValid(String token) { public boolean isTokenValid(String token) {
try { try {
Jwts.parser() Jwts.parser()
.verifyWith(getSigningKey()) .verifyWith(publicKey)
.build() .build()
.parseSignedClaims(token); .parseSignedClaims(token);
return true; return true;
@@ -55,10 +68,12 @@ public class JwtService {
} }
} }
private SecretKey getSigningKey() { private static PublicKey decodePublicKey(String base64X509Der) {
String secretKey = jwtConfig.getSecret(); try {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); byte[] keyBytes = Base64.getDecoder().decode(base64X509Der);
return Keys.hmacShaKeyFor(keyBytes); return KeyFactory.getInstance(ED25519).generatePublic(new X509EncodedKeySpec(keyBytes));
} catch (Exception e) {
throw new IllegalStateException("Failed to decode Ed25519 public key", e);
}
} }
} }
+1 -1
View File
@@ -36,7 +36,7 @@ management:
enabled: true enabled: true
security: security:
jwt: jwt:
secret: ${JWT_SECRET} public-key: ${JWT_PUBLIC_KEY}
app: app:
cors: cors:
allowed-origins: "https://tbp.andreas-dahm.eu" allowed-origins: "https://tbp.andreas-dahm.eu"
@@ -0,0 +1,102 @@
package core_service.configuration.security;
import core_service.shared.test.AbstractUnitTest;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.Base64;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.lenient;
/**
* Tests Core's verify-only JwtService against tokens signed with a matching Ed25519 private key.
*/
class JwtServiceTest extends AbstractUnitTest {
@Mock
private JwtConfig jwtConfig;
private JwtService jwtService;
private PrivateKey signingKey;
@BeforeEach
void setUp() throws Exception {
KeyPair keyPair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
signingKey = keyPair.getPrivate();
String publicB64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
lenient().when(jwtConfig.getPublicKey()).thenReturn(publicB64);
jwtService = new JwtService(jwtConfig);
jwtService.init();
}
private String signToken(String userId, String email, Long steamId) {
var builder = Jwts.builder()
.subject(email)
.claim("userId", userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 60_000));
if (steamId != null) {
builder.claim("steamId", steamId);
}
return builder.signWith(signingKey, Jwts.SIG.EdDSA).compact();
}
@Test
void extractEmail_Success() {
var email = "test@example.com";
var token = signToken("0815", email, null);
assertEquals(email, jwtService.extractEmail(token));
}
@Test
void extractUserId_Success() {
var userId = "0815";
var token = signToken(userId, "test@example.com", null);
assertEquals(userId, jwtService.extractUserId(token));
}
@Test
void extractSteamId_Success() {
var steamId = 123456789L;
var token = signToken("0815", "test@example.com", steamId);
assertEquals(steamId, jwtService.extractSteamId(token));
}
@Test
void isTokenValid_Success() {
var token = signToken("0815", "test@example.com", null);
assertTrue(jwtService.isTokenValid(token));
}
@Test
void isTokenValid_Failure_garbage() {
assertFalse(jwtService.isTokenValid("not-a-jwt"));
}
@Test
void isTokenValid_Failure_wrongKey() throws Exception {
KeyPair other = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
String token = Jwts.builder()
.subject("test@example.com")
.expiration(new Date(System.currentTimeMillis() + 60_000))
.signWith(other.getPrivate(), Jwts.SIG.EdDSA)
.compact();
assertFalse(jwtService.isTokenValid(token));
}
}