Public Access
Changed from JWT token to public/privat key
This commit is contained in:
1 parent
50366eb6bd
commit
f8d39bcda6
6 files changed
+137
-19
No files matched your search
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,4 +16,5 @@ spring:
|
|||||||
|
|
||||||
security:
|
security:
|
||||||
jwt:
|
jwt:
|
||||||
secret: "NzC9tQP1eTp496SxoVYslH1bqn81Tbfz74NjkFqdcVe7wayjcRN8IeZvJWd1s6NEWgwEXe9rdccvOn2HtJ4fdq"
|
# Dev-only Ed25519 public key. Must match Auth's dev private key.
|
||||||
|
public-key: "MCowBQYDK2VwAyEAPSmUISg0IUO1LrRPhF8RZAT/Yjh9HEVf5hzR84AL1Zw="
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user