diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 2cc40b7..cdc7730 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -16,6 +16,7 @@ RUN apt-get update --fix-missing && apt-get install --no-install-recommends --no
docker-buildx \
git \
docker.io \
+ nodejs \
maven \
libz-dev
diff --git a/account-service/pom.xml b/account-service/pom.xml
index 8c3738b..0081ef6 100644
--- a/account-service/pom.xml
+++ b/account-service/pom.xml
@@ -17,6 +17,9 @@
quarkus
+ false
+ false
+ true
3.15.0
25
UTF-8
@@ -158,20 +161,4 @@
-
-
-
- native
-
-
- native
-
-
-
- false
- false
- true
-
-
-
diff --git a/account-service/src/main/java/com/drinkool/services/AuthenticationService.java b/account-service/src/main/java/com/drinkool/services/AuthenticationService.java
index 1dde771..490bcb4 100644
--- a/account-service/src/main/java/com/drinkool/services/AuthenticationService.java
+++ b/account-service/src/main/java/com/drinkool/services/AuthenticationService.java
@@ -1,6 +1,6 @@
package com.drinkool.services;
-import com.drinkool.Jwt;
+import com.drinkool.InternalValue;
import com.drinkool.Role;
import com.drinkool.Url;
import com.drinkool.dtos.*;
@@ -25,8 +25,8 @@ public class AuthenticationService {
newCustomer.signup(input.name, input.phone, input.password);
return Response.status(Response.Status.CREATED)
- .header(Jwt.userId, newCustomer.id.toString())
- .header(Jwt.role, Role.Customer)
+ .header(InternalValue.userId, newCustomer.id.toString())
+ .header(InternalValue.role, Role.Customer)
.build();
}
@@ -39,8 +39,8 @@ public class AuthenticationService {
newCustomer.signup(input.phone);
return Response.status(Response.Status.CREATED)
- .header(Jwt.userId, newCustomer.id.toString())
- .header(Jwt.role, Role.Customer)
+ .header(InternalValue.userId, newCustomer.id.toString())
+ .header(InternalValue.role, Role.Customer)
.build();
}
@@ -63,8 +63,8 @@ public class AuthenticationService {
.build();
return Response.accepted()
- .header(Jwt.userId, customer.id.toString())
- .header(Jwt.role, Role.Customer)
+ .header(InternalValue.userId, customer.id.toString())
+ .header(InternalValue.role, Role.Customer)
.build();
}
@@ -83,8 +83,8 @@ public class AuthenticationService {
).firstResult();
return Response.accepted()
- .header(Jwt.userId, customer.id.toString())
- .header(Jwt.role, Role.Customer)
+ .header(InternalValue.userId, customer.id.toString())
+ .header(InternalValue.role, Role.Customer)
.build();
}
diff --git a/account-service/src/main/java/com/drinkool/services/UserService.java b/account-service/src/main/java/com/drinkool/services/UserService.java
index bcb8cb0..2cd2280 100644
--- a/account-service/src/main/java/com/drinkool/services/UserService.java
+++ b/account-service/src/main/java/com/drinkool/services/UserService.java
@@ -1,6 +1,6 @@
package com.drinkool.services;
-import com.drinkool.Jwt;
+import com.drinkool.InternalValue;
import com.drinkool.Url;
import com.drinkool.models.UserModel;
import jakarta.transaction.Transactional;
@@ -14,7 +14,7 @@ public class UserService {
@POST
@Path(Url.Me)
@Transactional
- public Response me(@HeaderParam(Jwt.userId) UUID id) {
+ public Response me(@HeaderParam(InternalValue.userId) UUID id) {
if (id == null) return Response.status(Response.Status.BAD_REQUEST)
.entity("InvalidUserId")
.build();
diff --git a/account-service/src/main/resources/application.properties b/account-service/src/main/resources/application.properties
index ade9388..18cb5a4 100644
--- a/account-service/src/main/resources/application.properties
+++ b/account-service/src/main/resources/application.properties
@@ -21,4 +21,8 @@ quarkus.redis.hosts=redis://localhost:6379
# Dependency
quarkus.index-dependency.lib.group-id=com.drinkool
-quarkus.index-dependency.lib.artifact-id=lib
\ No newline at end of file
+quarkus.index-dependency.lib.artifact-id=lib
+
+# Build
+quarkus.native.container-build=false
+quarkus.native.additional-build-args=--initialize-at-run-time=com.password4j.AlgorithmFinder
\ No newline at end of file
diff --git a/account-service/src/test/java/com/drinkool/services/AuthenticationServiceTest.java b/account-service/src/test/java/com/drinkool/services/AuthenticationServiceTest.java
index 61d5e50..e89655e 100644
--- a/account-service/src/test/java/com/drinkool/services/AuthenticationServiceTest.java
+++ b/account-service/src/test/java/com/drinkool/services/AuthenticationServiceTest.java
@@ -4,7 +4,7 @@ import static io.restassured.RestAssured.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.jupiter.api.Assertions.*;
-import com.drinkool.Jwt;
+import com.drinkool.InternalValue;
import com.drinkool.Role;
import com.drinkool.Url;
import com.drinkool.dtos.*;
@@ -55,8 +55,8 @@ public class AuthenticationServiceTest {
.when()
.post(Url.QuickSignup)
.then()
- .header(Jwt.userId, notNullValue())
- .header(Jwt.role, is(Role.Customer))
+ .header(InternalValue.userId, notNullValue())
+ .header(InternalValue.role, is(Role.Customer))
.statusCode(201);
assertEquals(1, CustomerEntity.find("phone", phone).count());
diff --git a/account-service/src/test/java/com/drinkool/services/UserServiceTest.java b/account-service/src/test/java/com/drinkool/services/UserServiceTest.java
index cfc6f19..eb8c5ea 100644
--- a/account-service/src/test/java/com/drinkool/services/UserServiceTest.java
+++ b/account-service/src/test/java/com/drinkool/services/UserServiceTest.java
@@ -3,7 +3,7 @@ package com.drinkool.services;
import static io.restassured.RestAssured.*;
import static org.hamcrest.CoreMatchers.*;
-import com.drinkool.Jwt;
+import com.drinkool.InternalValue;
import com.drinkool.Url;
import com.drinkool.dtos.QuickSignup;
import io.quarkus.test.junit.QuarkusTest;
@@ -26,10 +26,10 @@ public class UserServiceTest {
.post(Url.QuickSignup)
.then()
.extract()
- .header(Jwt.userId);
+ .header(InternalValue.userId);
given()
- .header(Jwt.userId, testUserId)
+ .header(InternalValue.userId, testUserId)
.contentType(ContentType.JSON)
.when()
.post(Url.Me)
diff --git a/gateway-service/pom.xml b/gateway-service/pom.xml
index 9cb7cb3..6493ca5 100644
--- a/gateway-service/pom.xml
+++ b/gateway-service/pom.xml
@@ -1,8 +1,8 @@
-
+
4.0.0
@@ -23,11 +23,9 @@
3.15.0
25
UTF-8
- UTF-8
+ UTF-8
quarkus-bom
- io.quarkus.platform
+ io.quarkus.platform
3.32.4
true
3.5.4
@@ -46,6 +44,11 @@
+
+ org.bitbucket.b_c
+ jose4j
+ 0.9.6
+
com.drinkool
lib
@@ -104,8 +107,7 @@
@{argLine}
- org.jboss.logmanager.LogManager
+ org.jboss.logmanager.LogManager
${maven.home}
@@ -126,12 +128,11 @@
${project.build.directory}/${project.build.finalName}-runner
- org.jboss.logmanager.LogManager
+ org.jboss.logmanager.LogManager
${maven.home}
-
+
\ No newline at end of file
diff --git a/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java b/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java
new file mode 100644
index 0000000..99afa06
--- /dev/null
+++ b/gateway-service/src/main/java/com/drinkool/filters/InternalHeaderGatewayFilter.java
@@ -0,0 +1,131 @@
+package com.drinkool.filters;
+
+import com.drinkool.InternalValue;
+import io.smallrye.mutiny.Uni;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.container.*;
+import jakarta.ws.rs.core.*;
+import java.util.*;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.resteasy.reactive.server.ServerRequestFilter;
+import org.jboss.resteasy.reactive.server.ServerResponseFilter;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.keys.HmacKey;
+
+public class InternalHeaderGatewayFilter {
+
+ @ConfigProperty(name = "gateway.jwt.secret")
+ String SECRET_KEY;
+
+ private boolean isSystemClaim(String key) {
+ return List.of("iss", "sub", "aud", "exp", "nbf", "iat", "jti").contains(
+ key
+ );
+ }
+
+ @ServerRequestFilter(preMatching = true)
+ public Uni handleRequestHeaders(
+ ContainerRequestContext requestContext
+ ) {
+ requestContext
+ .getHeaders()
+ .keySet()
+ .removeIf(
+ key ->
+ key.equalsIgnoreCase(InternalValue.headerId) ||
+ key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())
+ );
+
+ Map cookies = requestContext.getCookies();
+ Cookie authCookie = cookies.get(InternalValue.cookieName);
+
+ if (authCookie == null) return Uni.createFrom().nullItem();
+
+ return Uni.createFrom().item(() -> {
+ try {
+ String token = authCookie.getValue();
+
+ JwtConsumer jwtConsumer = new JwtConsumerBuilder()
+ .setRequireExpirationTime()
+ .setVerificationKey(new HmacKey(SECRET_KEY.getBytes()))
+ .build();
+
+ JwtClaims claims = jwtConsumer.processToClaims(token);
+
+ Map allClaims = claims.getClaimsMap();
+
+ allClaims.forEach((key, value) -> {
+ if (!isSystemClaim(key) && value != null) {
+ String headerName = InternalValue.headerId + (key);
+
+ requestContext.getHeaders().add(headerName, value.toString());
+ }
+ });
+ } catch (Exception e) {
+ return Response.status(Response.Status.UNAUTHORIZED)
+ .entity("InvalidToken")
+ .build();
+ }
+
+ return null;
+ });
+ }
+
+ @ServerResponseFilter(priority = Priorities.USER + 100)
+ public void handleResponseHeaders(ContainerResponseContext responseContext) {
+ JwtClaims claims = new JwtClaims();
+ boolean hasInternalData = false;
+
+ MultivaluedMap headers = responseContext.getHeaders();
+
+ List keysToRemove = new ArrayList<>();
+
+ for (String key : headers.keySet()) {
+ if (key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())) {
+ Object value = headers.getFirst(key);
+ if (value != null) {
+ String claimKey = key.substring(InternalValue.headerId.length());
+ claims.setClaim(claimKey, value.toString());
+ hasInternalData = true;
+ }
+ keysToRemove.add(key);
+ }
+ }
+
+ for (String key : keysToRemove) {
+ headers.remove(key);
+ }
+
+ if (hasInternalData) {
+ try {
+ claims.setExpirationTimeMinutesInTheFuture(60);
+ claims.setGeneratedJwtId();
+ claims.setIssuedAtToNow();
+
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(new HmacKey(SECRET_KEY.getBytes()));
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
+
+ String jwt = jws.getCompactSerialization();
+
+ NewCookie jwtCookie = new NewCookie.Builder(InternalValue.cookieName)
+ .value(jwt)
+ .path("/")
+ .httpOnly(true)
+ .secure(true)
+ .sameSite(NewCookie.SameSite.STRICT)
+ .maxAge(3600)
+ .build();
+
+ headers.add(HttpHeaders.SET_COOKIE, jwtCookie);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/gateway-service/src/main/resources/application.properties b/gateway-service/src/main/resources/application.properties
index 43aa152..dffba85 100644
--- a/gateway-service/src/main/resources/application.properties
+++ b/gateway-service/src/main/resources/application.properties
@@ -8,4 +8,4 @@ quarkus.container-image.builder=docker
quarkus.container-image.additional-tags=latest
# Build
-quarkus.native.container-build=false
+quarkus.native.container-build=false
\ No newline at end of file
diff --git a/gateway-service/src/test/java/com/drinkool/filters/InternalHeaderGatewayFilterTest.java b/gateway-service/src/test/java/com/drinkool/filters/InternalHeaderGatewayFilterTest.java
new file mode 100644
index 0000000..205c5b8
--- /dev/null
+++ b/gateway-service/src/test/java/com/drinkool/filters/InternalHeaderGatewayFilterTest.java
@@ -0,0 +1,97 @@
+package com.drinkool.filters;
+
+import static io.restassured.RestAssured.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.drinkool.InternalValue;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.response.Response;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.keys.HmacKey;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+public class InternalHeaderGatewayFilterTest {
+
+ @ConfigProperty(name = "gateway.jwt.secret")
+ String TEST_SECRET;
+
+ private String createTestToken(String userId) throws Exception {
+ JwtClaims claims = new JwtClaims();
+ claims.setExpirationTimeMinutesInTheFuture(10);
+ claims.setClaim("UserId", userId);
+ claims.setClaim("Role", "user");
+
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(new HmacKey(TEST_SECRET.getBytes()));
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
+ return jws.getCompactSerialization();
+ }
+
+ @Test
+ @DisplayName("Nên xóa header X-Internal nếu client tự ý gửi lên")
+ public void testShouldRemoveClentInternalHeaders() {
+ given()
+ .header("X-Internal-userId", "hacker-id")
+ .when()
+ .get("/test-gateway")
+ .then()
+ .statusCode(200)
+ .body(is("not-found")); // Filter phải xóa sạch trước khi vào Resource
+ }
+
+ @Test
+ @DisplayName("Nên trích xuất JWT từ Cookie và gắn vào Header nội bộ")
+ public void testValidJwtInCookie() throws Exception {
+ String token = createTestToken("user123");
+
+ given()
+ .cookie(InternalValue.cookieName, token)
+ .when()
+ .get("/test-gateway")
+ .then()
+ .statusCode(200)
+ .body(is("user123")); // Resource nhận được userId từ Header do Filter gắn vào
+ }
+
+ @Test
+ @DisplayName("Nên trả về 401 nếu JWT sai chữ ký")
+ public void testInvalidSignature() {
+ given()
+ .cookie(InternalValue.cookieName, "invalid.token.string")
+ .when()
+ .get("/test-gateway")
+ .then()
+ .statusCode(401)
+ .body(containsString("InvalidToken"));
+ }
+
+ @Test
+ @DisplayName(
+ "Nên đóng gói X-Internal headers từ service thành JWT gửi về client"
+ )
+ public void testResponseFilterConvertsHeadersToJwt() throws Exception {
+ String token = createTestToken("user123");
+
+ Response response = given()
+ .cookie(InternalValue.cookieName, token)
+ .when()
+ .post("/test-gateway/update")
+ .then()
+ .statusCode(200)
+ .cookie(InternalValue.cookieName) // Kiểm tra có Set-Cookie trả về không
+ .header("X-Internal-newStatus", nullValue()) // Header nội bộ phải bị xóa khỏi response
+ .extract()
+ .response();
+
+ String setCookie = response.getHeader("Set-Cookie");
+ assertTrue(setCookie.contains("HttpOnly"));
+ assertTrue(setCookie.contains("SameSite=Strict"));
+ }
+}
diff --git a/gateway-service/src/test/java/com/drinkool/utils/TestResource.java b/gateway-service/src/test/java/com/drinkool/utils/TestResource.java
new file mode 100644
index 0000000..2e98cc9
--- /dev/null
+++ b/gateway-service/src/test/java/com/drinkool/utils/TestResource.java
@@ -0,0 +1,28 @@
+package com.drinkool.utils;
+
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+
+@Path("/test-gateway")
+public class TestResource {
+
+ @GET
+ public Response getHeaders(@Context HttpHeaders headers) {
+ // Trả về header nội bộ để chúng ta kiểm tra xem Filter có gắn đúng không
+ String userId = headers.getHeaderString("X-Internal-userId");
+ return Response.ok().entity(userId != null ? userId : "not-found").build();
+ }
+
+ @POST
+ @Path("/update")
+ public Response updateInternal(
+ @HeaderParam("X-Internal-userId") String userId
+ ) {
+ // Giả lập service trả về header nội bộ để Gateway đóng gói vào JWT
+ return Response.ok("updated")
+ .header("X-Internal-newStatus", "active")
+ .build();
+ }
+}
diff --git a/gateway-service/src/test/resources/application.properties b/gateway-service/src/test/resources/application.properties
new file mode 100644
index 0000000..bf4225d
--- /dev/null
+++ b/gateway-service/src/test/resources/application.properties
@@ -0,0 +1,5 @@
+# Quarkus
+quarkus.http.host=0.0.0.0
+
+# JWT
+gateway.jwt.secret=1uZk07Hqu1316z9YqrcSGPTTJDhMWU1ZhFSLBrDQvmU=
\ No newline at end of file
diff --git a/k8s.yaml b/k8s.yaml
index c275470..4b2fde8 100644
--- a/k8s.yaml
+++ b/k8s.yaml
@@ -44,14 +44,6 @@ spec:
requests:
cpu: "100m"
memory: "256Mi"
- volumeMounts:
- - name: keys-volume
- mountPath: "/etc/keys"
- readOnly: true
- volumes:
- - name: keys-volume
- secret:
- secretName: jwt-keys
---
apiVersion: v1
kind: Service
@@ -88,6 +80,11 @@ spec:
env:
- name: QUARKUS_REST_CLIENT_ACCOUNT_URL
value: "http://account-service"
+ - name: GATEWAY_JWT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: jwt-secret
+ key: secret
resources:
limits:
cpu: "500m"
@@ -95,14 +92,6 @@ spec:
requests:
cpu: "100m"
memory: "256Mi"
- volumeMounts:
- - name: keys-volume
- mountPath: "/etc/keys"
- readOnly: true
- volumes:
- - name: keys-volume
- secret:
- secretName: jwt-keys
---
apiVersion: v1
kind: Service
diff --git a/lib/src/main/java/com/drinkool/Jwt.java b/lib/src/main/java/com/drinkool/InternalValue.java
similarity index 71%
rename from lib/src/main/java/com/drinkool/Jwt.java
rename to lib/src/main/java/com/drinkool/InternalValue.java
index 3847fee..f0865a4 100644
--- a/lib/src/main/java/com/drinkool/Jwt.java
+++ b/lib/src/main/java/com/drinkool/InternalValue.java
@@ -1,7 +1,8 @@
package com.drinkool;
-public class Jwt {
+public class InternalValue {
+ public static final String cookieName = "auth";
public static final String headerId = "X-Internal-";
public static final String userId = headerId + "UserId";
public static final String role = headerId + "Role";