diff --git a/.github/workflows/on_pull_request.yml b/.github/workflows/on_pull_request.yml
index 428f7bd1c..af490ab50 100644
--- a/.github/workflows/on_pull_request.yml
+++ b/.github/workflows/on_pull_request.yml
@@ -27,7 +27,7 @@ jobs:
complete:
if: always()
needs: [ gradle_build, essential_tests, extended_tests, rust_build ]
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
diff --git a/.github/workflows/sub_extended_tests.yml b/.github/workflows/sub_extended_tests.yml
index f9e1a694c..1d782f703 100644
--- a/.github/workflows/sub_extended_tests.yml
+++ b/.github/workflows/sub_extended_tests.yml
@@ -157,7 +157,7 @@ jobs:
uses: mydea/action-wait-for-api@v1
with:
url: "http://localhost:8085/health"
- expected-status: "403"
+ expected-status: "200"
interval: "1"
timeout: "600"
@@ -183,7 +183,7 @@ jobs:
uses: mydea/action-wait-for-api@v1
with:
url: "http://localhost:8085/health"
- expected-status: "403"
+ expected-status: "200"
interval: "1"
timeout: "600"
diff --git a/README.md b/README.md
index 0dcde9fce..2139bc0df 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
[](https://github.com/stellar/anchor-platform/blob/develop/LICENSE)
[](https://github.com/stellar/anchor-platform/releases)
-[](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=4.1.6)
+[](https://hub.docker.com/r/stellar/anchor-platform/tags?page=1&name=4.1.7)

diff --git a/build.gradle.kts b/build.gradle.kts
index e926a428f..2b02bd2c8 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -213,7 +213,7 @@ subprojects {
allprojects {
group = "org.stellar.anchor-sdk"
- version = "4.1.6"
+ version = "4.1.7"
tasks.jar {
manifest {
diff --git a/core/src/main/java/org/stellar/anchor/filter/AbstractJwtFilter.java b/core/src/main/java/org/stellar/anchor/filter/AbstractJwtFilter.java
index 533579f76..89f4bea1a 100644
--- a/core/src/main/java/org/stellar/anchor/filter/AbstractJwtFilter.java
+++ b/core/src/main/java/org/stellar/anchor/filter/AbstractJwtFilter.java
@@ -49,6 +49,11 @@ public void doFilter(
request.getRequestURL().toString(),
request.getQueryString());
+ if (shouldSkip(request)) {
+ filterChain.doFilter(servletRequest, servletResponse);
+ return;
+ }
+
if (request.getMethod().equals("OPTIONS")) {
filterChain.doFilter(servletRequest, servletResponse);
return;
@@ -88,6 +93,10 @@ public abstract void check(
String jwtCipher, HttpServletRequest request, ServletResponse servletResponse)
throws Exception;
+ protected boolean shouldSkip(HttpServletRequest request) {
+ return false;
+ }
+
private static void sendForbiddenError(HttpServletResponse response) throws IOException {
error("Forbidden: JwtTokenFilter failed to authenticate the request.");
response.setStatus(HttpStatus.SC_FORBIDDEN);
diff --git a/core/src/main/java/org/stellar/anchor/filter/ApiKeyFilter.java b/core/src/main/java/org/stellar/anchor/filter/ApiKeyFilter.java
index d303e34b1..5918e9e94 100644
--- a/core/src/main/java/org/stellar/anchor/filter/ApiKeyFilter.java
+++ b/core/src/main/java/org/stellar/anchor/filter/ApiKeyFilter.java
@@ -47,6 +47,11 @@ public void doFilter(
request.getRequestURL().toString(),
request.getQueryString());
+ if (shouldSkip(request)) {
+ filterChain.doFilter(servletRequest, servletResponse);
+ return;
+ }
+
if (request.getMethod().equals(OPTIONS)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
@@ -63,6 +68,10 @@ public void doFilter(
filterChain.doFilter(servletRequest, servletResponse);
}
+ protected boolean shouldSkip(HttpServletRequest request) {
+ return "/health".equals(FilterUtils.getRequestPath(request));
+ }
+
private static void sendForbiddenError(HttpServletResponse response) throws IOException {
error("Forbidden: ApiKeyFilter failed to authenticate the request.");
response.setStatus(HttpStatus.SC_FORBIDDEN);
diff --git a/core/src/main/java/org/stellar/anchor/filter/FilterUtils.java b/core/src/main/java/org/stellar/anchor/filter/FilterUtils.java
new file mode 100644
index 000000000..9f7e499af
--- /dev/null
+++ b/core/src/main/java/org/stellar/anchor/filter/FilterUtils.java
@@ -0,0 +1,31 @@
+package org.stellar.anchor.filter;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+final class FilterUtils {
+ static String getRequestPath(HttpServletRequest request) {
+ String servletPath = request.getServletPath();
+ if (servletPath != null && !servletPath.isEmpty()) {
+ return servletPath;
+ }
+
+ String pathInfo = request.getPathInfo();
+ if (pathInfo != null && !pathInfo.isEmpty()) {
+ return pathInfo;
+ }
+
+ String requestUri = request.getRequestURI();
+ if (requestUri == null) {
+ return "";
+ }
+
+ String contextPath = request.getContextPath();
+ if (contextPath != null && !contextPath.isEmpty() && requestUri.startsWith(contextPath)) {
+ return requestUri.substring(contextPath.length());
+ }
+
+ return requestUri;
+ }
+
+ private FilterUtils() {}
+}
diff --git a/core/src/main/java/org/stellar/anchor/filter/PlatformAuthJwtFilter.java b/core/src/main/java/org/stellar/anchor/filter/PlatformAuthJwtFilter.java
index 21c89ac6f..62a0f8bd4 100644
--- a/core/src/main/java/org/stellar/anchor/filter/PlatformAuthJwtFilter.java
+++ b/core/src/main/java/org/stellar/anchor/filter/PlatformAuthJwtFilter.java
@@ -12,6 +12,11 @@ public PlatformAuthJwtFilter(JwtService jwtService, String authorizationHeader)
super(jwtService, authorizationHeader);
}
+ @Override
+ protected boolean shouldSkip(HttpServletRequest request) {
+ return "/health".equals(FilterUtils.getRequestPath(request));
+ }
+
@Override
public void check(String jwtCipher, HttpServletRequest request, ServletResponse servletResponse)
throws Exception {
diff --git a/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java b/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
index 9e41f8076..75e6350c6 100644
--- a/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
+++ b/core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java
@@ -70,7 +70,7 @@ public Sep12GetCustomerResponse getCustomer(WebAuthJwt token, Sep12GetCustomerRe
populateRequestFromTransactionId(request);
validateGetOrPutRequest(request, token);
- if (request.getId() == null && request.getAccount() == null && token.getAccount() != null) {
+ if (request.getAccount() == null && token.getAccount() != null) {
request.setAccount(token.getAccount());
}
diff --git a/core/src/test/kotlin/org/stellar/anchor/filter/ApiKeyFilterTest.kt b/core/src/test/kotlin/org/stellar/anchor/filter/ApiKeyFilterTest.kt
index daad797e0..c1bf4d97e 100644
--- a/core/src/test/kotlin/org/stellar/anchor/filter/ApiKeyFilterTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/filter/ApiKeyFilterTest.kt
@@ -111,6 +111,18 @@ internal class ApiKeyFilterTest {
verify { mockFilterChain.doFilter(request, response) }
}
+ @Test
+ fun `health endpoint bypasses api key auth`() {
+ every { request.method } returns "GET"
+ every { request.servletPath } returns "/health"
+ every { request.getHeader("X-Api-Key") } returns null
+
+ apiKeyFilter.doFilter(request, response, mockFilterChain)
+
+ verify { mockFilterChain.doFilter(request, response) }
+ verify(exactly = 0) { response.setStatus(HttpStatus.SC_FORBIDDEN) }
+ }
+
@ParameterizedTest
@ValueSource(strings = ["GET", "PUT", "POST", "DELETE"])
fun `make sure FORBIDDEN is returned when the filter requires header names other than X-Api-Key`(
diff --git a/core/src/test/kotlin/org/stellar/anchor/filter/PlatformAuthJwtFilterTest.kt b/core/src/test/kotlin/org/stellar/anchor/filter/PlatformAuthJwtFilterTest.kt
new file mode 100644
index 000000000..b4d1bb5ab
--- /dev/null
+++ b/core/src/test/kotlin/org/stellar/anchor/filter/PlatformAuthJwtFilterTest.kt
@@ -0,0 +1,42 @@
+package org.stellar.anchor.filter
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.apache.hc.core5.http.HttpStatus
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.stellar.anchor.auth.JwtService
+
+internal class PlatformAuthJwtFilterTest {
+ private lateinit var jwtService: JwtService
+ private lateinit var request: HttpServletRequest
+ private lateinit var response: HttpServletResponse
+ private lateinit var mockFilterChain: FilterChain
+
+ @BeforeEach
+ fun setup() {
+ this.jwtService = mockk(relaxed = true)
+ this.request = mockk(relaxed = true)
+ this.response = mockk(relaxed = true)
+ this.mockFilterChain = mockk(relaxed = true)
+ }
+
+ @Test
+ fun `health endpoint bypasses jwt auth`() {
+ every { request.method } returns "GET"
+ every { request.servletPath } returns "/health"
+ every { request.getHeader("Authorization") } returns null
+ val filter = spyk(PlatformAuthJwtFilter(jwtService, "Authorization"))
+
+ filter.doFilter(request, response, mockFilterChain)
+
+ verify { mockFilterChain.doFilter(request, response) }
+ verify(exactly = 0) { response.setStatus(HttpStatus.SC_FORBIDDEN) }
+ verify(exactly = 0) { filter.check(any(), any(), any()) }
+ }
+}
diff --git a/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt b/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
index 40cb57dd7..51ac0de60 100644
--- a/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
+++ b/core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt
@@ -516,6 +516,43 @@ class Sep12ServiceTest {
assertEquals(TEST_ACCOUNT, mockGetRequest.account)
}
+ @Test
+ fun `Test get customer request with id injects account`() {
+ val callbackApiGetRequestSlot = slot
()
+ val mockCallbackApiGetCustomerResponse = GetCustomerResponse()
+ mockCallbackApiGetCustomerResponse.id = "customer-id"
+ every { customerIntegration.getCustomer(capture(callbackApiGetRequestSlot)) } returns
+ mockCallbackApiGetCustomerResponse
+
+ val mockGetRequest = Sep12GetCustomerRequest.builder().id("customer-id").build()
+ val jwtToken = createJwtToken(TEST_ACCOUNT)
+
+ assertDoesNotThrow { sep12Service.getCustomer(jwtToken, mockGetRequest) }
+
+ val wantCallbackApiGetRequest =
+ GetCustomerRequest.builder().id("customer-id").account(TEST_ACCOUNT).build()
+ assertEquals(wantCallbackApiGetRequest, callbackApiGetRequestSlot.captured)
+ assertEquals(TEST_ACCOUNT, mockGetRequest.account)
+ }
+
+ @Test
+ fun `Test put customer request with id injects account`() {
+ val callbackApiPutRequestSlot = slot()
+ val mockCallbackApiPutCustomerResponse = PutCustomerResponse.builder().id("customer-id").build()
+ every { customerIntegration.putCustomer(capture(callbackApiPutRequestSlot)) } returns
+ mockCallbackApiPutCustomerResponse
+
+ val mockPutRequest = Sep12PutCustomerRequest.builder().id("customer-id").build()
+ val jwtToken = createJwtToken(TEST_ACCOUNT)
+
+ assertDoesNotThrow { sep12Service.putCustomer(jwtToken, mockPutRequest) }
+
+ val wantCallbackApiPutRequest =
+ PutCustomerRequest.builder().id("customer-id").account(TEST_ACCOUNT).build()
+ assertEquals(wantCallbackApiPutRequest, callbackApiPutRequestSlot.captured)
+ assertEquals(TEST_ACCOUNT, mockPutRequest.account)
+ }
+
@Test
fun `test delete customer validation`() {
every { customerIntegration.deleteCustomer(any()) } just Runs
diff --git a/docs/resources/deployment-examples/example-fargate/Dockerfile b/docs/resources/deployment-examples/example-fargate/Dockerfile
index eb73d28fb..e906fa784 100644
--- a/docs/resources/deployment-examples/example-fargate/Dockerfile
+++ b/docs/resources/deployment-examples/example-fargate/Dockerfile
@@ -1,4 +1,4 @@
-FROM ubuntu:22.04
+FROM ubuntu:24.04
RUN mkdir /config_files
ENV STELLAR_ANCHOR_CONFIG=file:/config/anchor-config.yaml
ENV REFERENCE_SERVER_CONFIG_ENV=file:/config/reference-config.yaml
diff --git a/helm-charts/reference-server/templates/configmap.yaml b/helm-charts/reference-server/templates/configmap.yaml
index 28cb5a7ae..8358aa9c5 100644
--- a/helm-charts/reference-server/templates/configmap.yaml
+++ b/helm-charts/reference-server/templates/configmap.yaml
@@ -16,7 +16,7 @@ data:
custodyEnabled: false
auth:
- type: NONE
+ type: {{ .Values.config.auth.type | default "NONE" }}
data:
- database: postgres
\ No newline at end of file
+ database: postgres
diff --git a/helm-charts/reference-server/values.yaml b/helm-charts/reference-server/values.yaml
index 34185dc4b..cb79d6ae5 100644
--- a/helm-charts/reference-server/values.yaml
+++ b/helm-charts/reference-server/values.yaml
@@ -13,6 +13,8 @@ config:
rpcEndpoint: https://soroban-testnet.stellar.org
platformApiEndpoint: http://anchor-platform-svc-platform:8085
rpcEnabled: false
+ auth:
+ type: NONE
data:
url: postgresql-ref:5432
@@ -37,4 +39,4 @@ ingress:
host: reference-server.local
className: nginx
annotations:
- nginx.ingress.kubernetes.io/rewrite-target: /
\ No newline at end of file
+ nginx.ingress.kubernetes.io/rewrite-target: /
diff --git a/helm-charts/secret-store/templates/secretstore.yaml b/helm-charts/secret-store/templates/secretstore.yaml
index c43bc0c45..714fe819e 100644
--- a/helm-charts/secret-store/templates/secretstore.yaml
+++ b/helm-charts/secret-store/templates/secretstore.yaml
@@ -21,8 +21,10 @@ spec:
"SEP10_SIGNING_SEED": "{{ .Values.sep10_signing_seed }}",
"EVENTS_QUEUE_KAFKA_USERNAME": "user1",
"EVENTS_QUEUE_KAFKA_PASSWORD": "123456789",
- "SENTRY_AUTH_TOKEN": {{ default "\"\"" .Values.sentry_auth_token }}
- "SEP45_JWT_SECRET": "1e5ba4a1da18dc6462b21bb720a5aceddb34a421e732e0f6615ad30b4a4aa50f"
+ "SENTRY_AUTH_TOKEN": {{ default "\"\"" .Values.sentry_auth_token }},
+ "SEP45_JWT_SECRET": "1e5ba4a1da18dc6462b21bb720a5aceddb34a421e732e0f6615ad30b4a4aa50f",
+ "CALLBACK_API_AUTH_SECRET": "myPlatformToAnchorSecret",
+ "PLATFORM_API_AUTH_SECRET": "myAnchorToPlatformSecret"
}
- key: {{ .Values.namespace }}/reference-server-secrets
value: |
@@ -32,5 +34,5 @@ spec:
"PAYMENT_SIGNING_SEED": "{{ .Values.payment_signing_seed }}",
"SEP24_INTERACTIVE_JWT_KEY": "c5457e3a349df9002117543efa7e316dd89e666a5ce6f33a0deb13e90f3f1e9d",
"PLATFORM_ANCHOR_SECRET": "myPlatformToAnchorSecret",
- "ANCHOR_PLATFORM_SECRET": "myAnchorToPlatformSecret",
- }
\ No newline at end of file
+ "ANCHOR_PLATFORM_SECRET": "myAnchorToPlatformSecret"
+ }
diff --git a/helm-charts/sep-service/templates/eventprocessor-deployment.yaml b/helm-charts/sep-service/templates/eventprocessor-deployment.yaml
index e31e587b0..4c5127f07 100644
--- a/helm-charts/sep-service/templates/eventprocessor-deployment.yaml
+++ b/helm-charts/sep-service/templates/eventprocessor-deployment.yaml
@@ -68,6 +68,16 @@ spec:
secretKeyRef:
name: {{ .Values.fullName }}-secrets
key: SEP6_MORE_INFO_URL_JWT_SECRET
+ - name: SECRET_CALLBACK_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: CALLBACK_API_AUTH_SECRET
+ - name: SECRET_PLATFORM_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: PLATFORM_API_AUTH_SECRET
- name: SECRET_EVENTS_QUEUE_KAFKA_USERNAME
valueFrom:
secretKeyRef:
diff --git a/helm-charts/sep-service/templates/externalsecrets.yaml b/helm-charts/sep-service/templates/externalsecrets.yaml
index 22da08dbd..02b27e52e 100644
--- a/helm-charts/sep-service/templates/externalsecrets.yaml
+++ b/helm-charts/sep-service/templates/externalsecrets.yaml
@@ -54,3 +54,11 @@ spec:
remoteRef:
key: {{ .Values.namespace }}/{{ .Values.fullName }}-secrets
property: SEP45_JWT_SECRET
+ - secretKey: CALLBACK_API_AUTH_SECRET
+ remoteRef:
+ key: {{ .Values.namespace }}/{{ .Values.fullName }}-secrets
+ property: CALLBACK_API_AUTH_SECRET
+ - secretKey: PLATFORM_API_AUTH_SECRET
+ remoteRef:
+ key: {{ .Values.namespace }}/{{ .Values.fullName }}-secrets
+ property: PLATFORM_API_AUTH_SECRET
diff --git a/helm-charts/sep-service/templates/observer-deployment.yaml b/helm-charts/sep-service/templates/observer-deployment.yaml
index 2b03c729e..fd975a9bf 100644
--- a/helm-charts/sep-service/templates/observer-deployment.yaml
+++ b/helm-charts/sep-service/templates/observer-deployment.yaml
@@ -88,6 +88,16 @@ spec:
secretKeyRef:
name: {{ .Values.fullName }}-secrets
key: SEP6_MORE_INFO_URL_JWT_SECRET
+ - name: SECRET_CALLBACK_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: CALLBACK_API_AUTH_SECRET
+ - name: SECRET_PLATFORM_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: PLATFORM_API_AUTH_SECRET
- name: SENTRY_AUTH_TOKEN
valueFrom:
secretKeyRef:
@@ -103,4 +113,4 @@ spec:
volumes:
- name: config-volume
configMap:
- name: anchor-config
\ No newline at end of file
+ name: anchor-config
diff --git a/helm-charts/sep-service/templates/platform-deployment.yaml b/helm-charts/sep-service/templates/platform-deployment.yaml
index 1c4e9f685..228e14b3e 100644
--- a/helm-charts/sep-service/templates/platform-deployment.yaml
+++ b/helm-charts/sep-service/templates/platform-deployment.yaml
@@ -118,6 +118,16 @@ spec:
name: {{ .Values.fullName }}-secrets
key: SEP45_JWT_SECRET
optional: true
+ - name: SECRET_CALLBACK_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: CALLBACK_API_AUTH_SECRET
+ - name: SECRET_PLATFORM_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: PLATFORM_API_AUTH_SECRET
resources:
requests:
memory: {{ .Values.services.platform.deployment.resources.requests.memory }}
diff --git a/helm-charts/sep-service/templates/sepserver-deployment.yaml b/helm-charts/sep-service/templates/sepserver-deployment.yaml
index a40c01f52..b79ade549 100644
--- a/helm-charts/sep-service/templates/sepserver-deployment.yaml
+++ b/helm-charts/sep-service/templates/sepserver-deployment.yaml
@@ -119,6 +119,16 @@ spec:
name: {{ .Values.fullName }}-secrets
key: SEP45_JWT_SECRET
optional: true
+ - name: SECRET_CALLBACK_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: CALLBACK_API_AUTH_SECRET
+ - name: SECRET_PLATFORM_API_AUTH_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.fullName }}-secrets
+ key: PLATFORM_API_AUTH_SECRET
resources:
requests:
memory: {{ .Values.services.sep.deployment.resources.requests.memory }}
diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/customer/CustomerService.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/customer/CustomerService.kt
index 7db355c1e..2fb1d2294 100644
--- a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/customer/CustomerService.kt
+++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/callbacks/customer/CustomerService.kt
@@ -55,8 +55,17 @@ class CustomerService(
customer
}
request.id != null -> {
- customerRepository.get(request.id)
- ?: throw NotFoundException("customer for 'id' '${request.id}' not found", request.id)
+ val customer =
+ customerRepository.get(request.id)
+ ?: throw NotFoundException("customer for 'id' '${request.id}' not found", request.id)
+ assertCustomerMatchesRequest(
+ customer,
+ request.id,
+ request.account,
+ request.memo,
+ request.memoType
+ )
+ customer
}
request.account != null -> {
customerRepository.get(request.account, request.memo, request.memoType)
@@ -80,7 +89,19 @@ class CustomerService(
request.transactionId != null -> {
getCustomerFromTransaction(request.transactionId, request.type)
}
- request.id != null -> customerRepository.get(request.id)
+ request.id != null -> {
+ val customer = customerRepository.get(request.id)
+ if (customer != null) {
+ assertCustomerMatchesRequest(
+ customer,
+ request.id,
+ request.account,
+ request.memo,
+ request.memoType
+ )
+ }
+ customer
+ }
request.account != null ->
customerRepository.get(request.account, request.memo, request.memoType)
else -> {
@@ -251,6 +272,24 @@ class CustomerService(
}
}
+ private fun assertCustomerMatchesRequest(
+ customer: Customer,
+ requestId: String,
+ requestAccount: String?,
+ requestMemo: String?,
+ requestMemoType: String?,
+ ) {
+ if (requestAccount != null && requestAccount != customer.stellarAccount) {
+ throw NotFoundException("customer for 'id' '$requestId' not found", requestId)
+ }
+ if (requestMemo != null && requestMemo != customer.memo) {
+ throw NotFoundException("customer for 'id' '$requestId' not found", requestId)
+ }
+ if (requestMemoType != null && requestMemoType != customer.memoType) {
+ throw NotFoundException("customer for 'id' '$requestId' not found", requestId)
+ }
+ }
+
private fun convertCustomerToResponse(
customer: Customer,
type: String?,
diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/AuthHeaderUtil.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/AuthHeaderUtil.kt
new file mode 100644
index 000000000..28ae33af3
--- /dev/null
+++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/AuthHeaderUtil.kt
@@ -0,0 +1,19 @@
+package org.stellar.reference.client
+
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.http.HttpHeaders
+import org.stellar.reference.data.AuthSettings
+
+object AuthHeaderUtil {
+ fun addAuthHeaderIfNeeded(builder: HttpRequestBuilder, authSettings: AuthSettings) {
+ if (authSettings.type != AuthSettings.Type.JWT) {
+ return
+ }
+ val token =
+ JwtTokenProvider.createJwt(
+ authSettings.anchorToPlatformSecret,
+ authSettings.expirationMilliseconds,
+ )
+ builder.headers.append(HttpHeaders.Authorization, "Bearer $token")
+ }
+}
diff --git a/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/JwtTokenProvider.kt b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/JwtTokenProvider.kt
new file mode 100644
index 000000000..96e313603
--- /dev/null
+++ b/kotlin-reference-server/src/main/kotlin/org/stellar/reference/client/JwtTokenProvider.kt
@@ -0,0 +1,33 @@
+package org.stellar.reference.client
+
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.security.Keys
+import java.nio.charset.StandardCharsets
+import java.util.Date
+import org.stellar.anchor.util.GsonUtils
+
+object JwtTokenProvider {
+ fun createJwt(secret: String, expirationMilliseconds: Long): String {
+ val issuedAt = Date()
+ val expiresAt = Date(issuedAt.time + expirationMilliseconds)
+ val key = Keys.hmacShaKeyFor(secret.toByteArray(StandardCharsets.UTF_8))
+ return Jwts.builder()
+ .json(GsonJwtSerializer())
+ .issuedAt(issuedAt)
+ .expiration(expiresAt)
+ .signWith(key, Jwts.SIG.HS256)
+ .compact()
+ }
+}
+
+private class GsonJwtSerializer : io.jsonwebtoken.io.Serializer