diff --git a/src/main/java/com/clerk/backend_api/helpers/security/AuthenticateRequest.java b/src/main/java/com/clerk/backend_api/helpers/security/AuthenticateRequest.java index bc824a95..53d1c9ea 100644 --- a/src/main/java/com/clerk/backend_api/helpers/security/AuthenticateRequest.java +++ b/src/main/java/com/clerk/backend_api/helpers/security/AuthenticateRequest.java @@ -114,7 +114,16 @@ private static VerifyTokenOptions buildVerifyTokenOptions(AuthenticateRequestOpt .authorizedParties(options.authorizedParties()) // .clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) // .build(); - } else { + } + else if (options.machineSecretKey().isPresent()) { + return VerifyTokenOptions + .machineSecretKey(options.machineSecretKey().get()) + .audience(options.audience()) + .authorizedParties(options.authorizedParties()) + .clockSkew(options.clockSkewInMs(), TimeUnit.MILLISECONDS) + .build(); + } + else { return new VerifyTokenOptions.Builder().build(); } } diff --git a/src/main/java/com/clerk/backend_api/helpers/security/models/AuthenticateRequestOptions.java b/src/main/java/com/clerk/backend_api/helpers/security/models/AuthenticateRequestOptions.java index ffe70de2..d4dd3a24 100644 --- a/src/main/java/com/clerk/backend_api/helpers/security/models/AuthenticateRequestOptions.java +++ b/src/main/java/com/clerk/backend_api/helpers/security/models/AuthenticateRequestOptions.java @@ -19,6 +19,7 @@ public final class AuthenticateRequestOptions { private final Optional secretKey; private final Optional jwtKey; + private final Optional machineSecretKey; private final Optional audience; private final Set authorizedParties; private final long clockSkewInMs; @@ -42,6 +43,7 @@ public final class AuthenticateRequestOptions { public AuthenticateRequestOptions( Optional secretKey, Optional jwtKey, + Optional machineSecretKey, Optional audience, Set authorizedParties, Optional clockSkewInMs, @@ -50,12 +52,14 @@ public AuthenticateRequestOptions( Utils.checkNotNull(secretKey, "secretKey"); Utils.checkNotNull(jwtKey, "jwtKey"); + Utils.checkNotNull(machineSecretKey, "machineSecretKey"); Utils.checkNotNull(audience, "audience"); Utils.checkNotNull(authorizedParties, "authorizedParties"); Utils.checkNotNull(clockSkewInMs, "clockSkewInMs"); this.secretKey = secretKey; this.jwtKey = jwtKey; + this.machineSecretKey = machineSecretKey; this.audience = audience; this.authorizedParties = authorizedParties; this.clockSkewInMs = clockSkewInMs.orElse(DEFAULT_CLOCK_SKEW_MS); @@ -72,6 +76,10 @@ public Optional jwtKey() { return jwtKey; } + public Optional machineSecretKey() { + return machineSecretKey; + } + public Optional audience() { return audience; } @@ -105,6 +113,7 @@ public static final class Builder { private Optional secretKey = Optional.empty(); private Optional jwtKey = Optional.empty(); + private Optional machineSecretKey = Optional.empty(); private Optional audience = Optional.empty(); private Set authorizedParties = new HashSet<>(); private Optional> acceptsToken = Optional.empty(); @@ -126,6 +135,13 @@ public static Builder withJwtKey(String jwtKey) { return builder; } + public static Builder withMachineSecretKey(String machineSecretKey) { + Utils.checkNotNull(machineSecretKey, "machineSecretKey"); + Builder builder = new Builder(); + builder.machineSecretKey = Optional.of(machineSecretKey); + return builder; + } + public Builder audience(String audience) { Utils.checkNotNull(audience, "audience"); return audience(Optional.of(audience)); @@ -171,6 +187,7 @@ public Builder acceptsTokens(List acceptsToken) { public AuthenticateRequestOptions build() { return new AuthenticateRequestOptions(secretKey, jwtKey, + machineSecretKey, audience, authorizedParties, Optional.of(clockSkewInMs),acceptsToken); diff --git a/src/main/java/com/clerk/backend_api/helpers/security/models/VerifyTokenOptions.java b/src/main/java/com/clerk/backend_api/helpers/security/models/VerifyTokenOptions.java index bbea9500..d1718909 100644 --- a/src/main/java/com/clerk/backend_api/helpers/security/models/VerifyTokenOptions.java +++ b/src/main/java/com/clerk/backend_api/helpers/security/models/VerifyTokenOptions.java @@ -19,6 +19,7 @@ public final class VerifyTokenOptions { private final Optional secretKey; private final Optional jwtKey; + private final Optional machineSecretKey; private final Optional audience; private final Set authorizedParties; private final long clockSkewInMs; @@ -47,6 +48,7 @@ public final class VerifyTokenOptions { public VerifyTokenOptions( Optional secretKey, Optional jwtKey, + Optional machineSecretKey, Optional audience, Set authorizedParties, Optional clockSkewInMs, @@ -58,6 +60,7 @@ public VerifyTokenOptions( Utils.checkNotNull(clockSkewInMs, "clockSkewInMs"); Utils.checkNotNull(jwtKey, "jwtKey"); Utils.checkNotNull(secretKey, "secretKey"); + Utils.checkNotNull(machineSecretKey, "machineSecretKey"); Utils.checkNotNull(apiUrl, "apiUrl"); Utils.checkNotNull(apiVersion, "apiVersion"); @@ -66,6 +69,7 @@ public VerifyTokenOptions( this.clockSkewInMs = clockSkewInMs.orElse(DEFAULT_CLOCK_SKEW_MS); this.jwtKey = jwtKey; this.secretKey = secretKey; + this.machineSecretKey = machineSecretKey; this.apiUrl = apiUrl.orElse(DEFAULT_API_URL); this.apiVersion = apiVersion.orElse(DEFAULT_API_VERSION); } @@ -90,6 +94,10 @@ public Optional secretKey() { return secretKey; } + public Optional machineSecretKey() { + return machineSecretKey; + } + public String apiUrl() { return apiUrl; } @@ -106,11 +114,15 @@ public static Builder jwtKey(String jwtKey) { return Builder.withJwtKey(jwtKey); } + public static Builder machineSecretKey(String machineSecretKey) { + return Builder.withMachineSecretKey(machineSecretKey); + } + public static final class Builder { private Optional secretKey = Optional.empty(); private Optional jwtKey = Optional.empty(); - + private Optional machineSecretKey = Optional.empty(); private Optional audience = Optional.empty(); private Set authorizedParties = new HashSet<>(); private long clockSkewInMs = DEFAULT_CLOCK_SKEW_MS; @@ -131,6 +143,13 @@ public static Builder withJwtKey(String jwtKey) { return builder; } + public static Builder withMachineSecretKey(String machineSecretKey) { + Utils.checkNotNull(machineSecretKey, "machineSecretKey"); + Builder builder = new Builder(); + builder.machineSecretKey = Optional.of(machineSecretKey); + return builder; + } + public Builder audience(String audience) { Utils.checkNotNull(audience, "audience"); return audience(Optional.of(audience)); @@ -194,6 +213,7 @@ public Builder apiVersion(Optional apiVersion) { public VerifyTokenOptions build() { return new VerifyTokenOptions(secretKey, jwtKey, + machineSecretKey, audience, authorizedParties, Optional.of(clockSkewInMs), diff --git a/src/main/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifier.java b/src/main/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifier.java index 88469c6f..59e07611 100644 --- a/src/main/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifier.java +++ b/src/main/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifier.java @@ -41,7 +41,7 @@ public TokenVerificationResponse verify(String toke HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(options.apiUrl() + MACHINE_TOKEN_VERIFICATION_URL)) .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + options.secretKey().get()) + .header("Authorization", "Bearer " + ( options.secretKey().isPresent() ? options.secretKey().get() : options.machineSecretKey().get())) .timeout(Duration.ofSeconds(30)) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); diff --git a/src/test/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifierTests.java b/src/test/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifierTests.java index bd4eb6e7..707e6483 100644 --- a/src/test/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifierTests.java +++ b/src/test/java/com/clerk/backend_api/helpers/security/token_verifiers/impl/MachineTokenVerifierTests.java @@ -21,7 +21,9 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.argThat; class MachineTokenVerifierTests { @@ -70,6 +72,62 @@ void verify_shouldReturnValidResponse_whenHttpStatusIs200() throws Exception { } } + @Test + void verify_shouldUseSecretKeyInAuthorizationHeader_whenSecretKeyIsPresent() throws Exception { + String token = "mt_test_token"; + String secretKey = "sk_test_secret_key"; + + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(getMockResponse()); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + try (MockedStatic staticHttpClient = mockStatic(HttpClient.class)) { + staticHttpClient.when(HttpClient::newHttpClient).thenReturn(mockClient); + + MachineTokenVerifier verifier = new MachineTokenVerifier(); + VerifyTokenOptions options = VerifyTokenOptions.secretKey(secretKey).build(); + + verifier.verify(token, options); + + // Verify the authorization header contains the secret key + verify(mockClient).send(argThat(request -> request.headers().firstValue("Authorization") + .map(auth -> auth.equals("Bearer " + secretKey)) + .orElse(false)), any(HttpResponse.BodyHandler.class)); + } + } + + @Test + void verify_shouldUseMachineSecretKeyInAuthorizationHeader_whenMachineSecretKeyIsPresent() throws Exception { + String token = "mt_test_token"; + String machineSecretKey = "msk_test_machine_secret_key"; + + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn(getMockResponse()); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + try (MockedStatic staticHttpClient = mockStatic(HttpClient.class)) { + staticHttpClient.when(HttpClient::newHttpClient).thenReturn(mockClient); + + MachineTokenVerifier verifier = new MachineTokenVerifier(); + VerifyTokenOptions options = VerifyTokenOptions.machineSecretKey(machineSecretKey).build(); + + verifier.verify(token, options); + + // Verify the authorization header contains the machine secret key + verify(mockClient).send(argThat(request -> request.headers().firstValue("Authorization") + .map(auth -> auth.equals("Bearer " + machineSecretKey)) + .orElse(false)), any(HttpResponse.BodyHandler.class)); + } + } + @Test void verify_shouldThrowTokenVerificationException_whenHttpStatusIsNot200() throws Exception { HttpClient mockClient = mock(HttpClient.class);