Feature/ext validation/request validation #15
+18
-53
@@ -2,16 +2,10 @@ package dev.relism.flash.ext.mcp;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import dev.relism.flash.extension.FlashApp;
|
import dev.relism.flash.testing.FlashResponse;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import dev.relism.flash.testing.FlashTest;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
@@ -22,34 +16,15 @@ class McpExtensionIntegrationTest {
|
|||||||
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private FlashApp app;
|
// Every test here is a stateless JSON-RPC call against the same server, so one boot for
|
||||||
private String mcpUrl;
|
// the class rather than one per test.
|
||||||
private HttpClient client;
|
@RegisterExtension
|
||||||
|
static FlashTest mcp = FlashTest.of(app -> app.install(new McpExtension(
|
||||||
@BeforeEach
|
McpConfig.builder("test-server")
|
||||||
void setUp() throws Exception {
|
.version("9.9.9")
|
||||||
int port;
|
.toolsPackage("dev.relism.flash.ext.mcp.fixtures")
|
||||||
try (ServerSocket s = new ServerSocket(0)) {
|
.security(McpSecurity.NONE)
|
||||||
port = s.getLocalPort();
|
.build())));
|
||||||
}
|
|
||||||
mcpUrl = "http://127.0.0.1:" + port + "/mcp";
|
|
||||||
client = HttpClient.newHttpClient();
|
|
||||||
|
|
||||||
McpConfig config = McpConfig.builder("test-server")
|
|
||||||
.version("9.9.9")
|
|
||||||
.toolsPackage("dev.relism.flash.ext.mcp.fixtures")
|
|
||||||
.security(McpSecurity.NONE)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new McpExtension(config));
|
|
||||||
app.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void tearDown() {
|
|
||||||
if (app != null) app.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void initialize_returnsProtocolVersionCapabilitiesAndServerInfo() throws Exception {
|
void initialize_returnsProtocolVersionCapabilitiesAndServerInfo() throws Exception {
|
||||||
@@ -104,17 +79,13 @@ class McpExtensionIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void notification_returns202WithEmptyBody() throws Exception {
|
void notification_returns202WithEmptyBody() {
|
||||||
String body = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}";
|
post("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}").expectStatus(202);
|
||||||
HttpResponse<String> resp = post(body);
|
|
||||||
assertEquals(202, resp.statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void malformedJson_returns400ParseError() throws Exception {
|
void malformedJson_returns400ParseError() throws Exception {
|
||||||
HttpResponse<String> resp = post("not json");
|
JsonNode json = MAPPER.readTree(post("not json").expectStatus(400).body());
|
||||||
assertEquals(400, resp.statusCode());
|
|
||||||
JsonNode json = MAPPER.readTree(resp.body());
|
|
||||||
assertEquals(-32700, json.get("error").get("code").asInt());
|
assertEquals(-32700, json.get("error").get("code").asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,16 +99,10 @@ class McpExtensionIntegrationTest {
|
|||||||
|
|
||||||
private JsonNode call(int id, String method, String paramsJson) throws Exception {
|
private JsonNode call(int id, String method, String paramsJson) throws Exception {
|
||||||
String body = "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"method\":\"" + method + "\",\"params\":" + paramsJson + "}";
|
String body = "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"method\":\"" + method + "\",\"params\":" + paramsJson + "}";
|
||||||
HttpResponse<String> resp = post(body);
|
return MAPPER.readTree(post(body).expectStatus(200).body());
|
||||||
assertEquals(200, resp.statusCode());
|
|
||||||
return MAPPER.readTree(resp.body());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpResponse<String> post(String body) throws Exception {
|
private FlashResponse post(String body) {
|
||||||
HttpRequest req = HttpRequest.newBuilder(URI.create(mcpUrl))
|
return mcp.request().json(body).post("/mcp");
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
|
||||||
.build();
|
|
||||||
return client.send(req, HttpResponse.BodyHandlers.ofString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-118
@@ -3,16 +3,15 @@ package dev.relism.flash.ext.mcp;
|
|||||||
import dev.relism.flash.ext.oidc.OidcConfig;
|
import dev.relism.flash.ext.oidc.OidcConfig;
|
||||||
import dev.relism.flash.ext.oidc.OidcExtension;
|
import dev.relism.flash.ext.oidc.OidcExtension;
|
||||||
import dev.relism.flash.extension.FlashApp;
|
import dev.relism.flash.extension.FlashApp;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import dev.relism.flash.extension.FlashApplication;
|
||||||
|
import dev.relism.flash.extension.FlashConfiguration;
|
||||||
|
import dev.relism.flash.testing.FlashRequest;
|
||||||
|
import dev.relism.flash.testing.FlashResponse;
|
||||||
|
import dev.relism.flash.testing.FlashTest;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@@ -20,175 +19,161 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
* Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-oidc}
|
* Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-oidc}
|
||||||
* installation backed by {@link FakeOidcProvider} — real discovery, real JWKS, real RS256
|
* installation backed by {@link FakeOidcProvider} — real discovery, real JWKS, real RS256
|
||||||
* tokens — plus the fail-fast/degrade behavior when oidc is absent.
|
* tokens — plus the fail-fast/degrade behavior when oidc is absent.
|
||||||
|
*
|
||||||
|
* <p>Four server configurations differ only in how MCP security is declared, so each gets its
|
||||||
|
* own {@link FlashTest} and they share one provider.
|
||||||
*/
|
*/
|
||||||
class McpExtensionSecurityTest {
|
class McpExtensionSecurityTest {
|
||||||
|
|
||||||
private static final String TOOLS_PACKAGE = "dev.relism.flash.ext.mcp.fixtures";
|
private static final String TOOLS_PACKAGE = "dev.relism.flash.ext.mcp.fixtures";
|
||||||
|
private static final String EXPLICIT_RESOURCE_ID = "https://mcp.example.com/mcp";
|
||||||
|
|
||||||
private FlashApp app;
|
private static final FakeOidcProvider provider = newProvider();
|
||||||
private FakeOidcProvider provider;
|
|
||||||
|
|
||||||
@AfterEach
|
/** MCP asked for AUTO security with no oidc installed — should degrade to public. */
|
||||||
void tearDown() {
|
@RegisterExtension
|
||||||
if (app != null) app.stop();
|
static FlashTest degraded = FlashTest.of(app -> app.install(new McpExtension(
|
||||||
if (provider != null) provider.close();
|
McpConfig.builder("auto-server")
|
||||||
|
.toolsPackage(TOOLS_PACKAGE)
|
||||||
|
.security(McpSecurity.AUTO)
|
||||||
|
.build())));
|
||||||
|
|
||||||
|
/** REQUIRED with oidc, resource identifier derived from the request. */
|
||||||
|
@RegisterExtension
|
||||||
|
static FlashTest secured = FlashTest.of(securedApp(null, null));
|
||||||
|
|
||||||
|
/** REQUIRED with oidc and an explicitly declared resource identifier. */
|
||||||
|
@RegisterExtension
|
||||||
|
static FlashTest securedWithResourceId = FlashTest.of(securedApp(EXPLICIT_RESOURCE_ID, null));
|
||||||
|
|
||||||
|
/** REQUIRED with oidc and advertised scopes. */
|
||||||
|
@RegisterExtension
|
||||||
|
static FlashTest securedWithScopes =
|
||||||
|
FlashTest.of(securedApp(null, new String[] {"openid", "profile", "email"}));
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void closeProvider() {
|
||||||
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── No oidc installed ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withoutOidc_throwsAtBoot() throws Exception {
|
void required_withoutOidc_throwsAtBoot() {
|
||||||
int port = freePort();
|
// Asserting that boot fails, so this one builds its app directly rather than through
|
||||||
app = FlashApp.create(port);
|
// the harness; port(0) still removes the old free-port dance.
|
||||||
|
FlashApp app = FlashApp.create(FlashConfiguration.builder()
|
||||||
|
.port(0).host("127.0.0.1").shutdownDrainTimeoutMs(250).build());
|
||||||
app.install(new McpExtension(McpConfig.builder("secure-server")
|
app.install(new McpExtension(McpConfig.builder("secure-server")
|
||||||
.toolsPackage(TOOLS_PACKAGE)
|
.toolsPackage(TOOLS_PACKAGE)
|
||||||
.security(McpSecurity.REQUIRED)
|
.security(McpSecurity.REQUIRED)
|
||||||
.build()));
|
.build()));
|
||||||
|
try {
|
||||||
assertThrows(IllegalStateException.class, () -> app.start());
|
assertThrows(IllegalStateException.class, app::start);
|
||||||
|
} finally {
|
||||||
|
app.stop().join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void auto_withoutOidc_degradesToPublic() throws Exception {
|
void auto_withoutOidc_degradesToPublic() {
|
||||||
int port = freePort();
|
post(degraded, initializeBody(), null).expectStatus(200);
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new McpExtension(McpConfig.builder("auto-server")
|
|
||||||
.toolsPackage(TOOLS_PACKAGE)
|
|
||||||
.security(McpSecurity.AUTO)
|
|
||||||
.build()));
|
|
||||||
app.start();
|
|
||||||
|
|
||||||
HttpResponse<String> resp = post(port, initializeBody(), null);
|
|
||||||
assertEquals(200, resp.statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// ── REQUIRED with oidc ───────────────────────────────────────────────────
|
||||||
void required_withOidc_rejectsMissingToken() throws Exception {
|
|
||||||
int port = bootSecuredApp(null);
|
|
||||||
|
|
||||||
HttpResponse<String> resp = post(port, initializeBody(), null);
|
@Test
|
||||||
assertEquals(401, resp.statusCode());
|
void required_withOidc_rejectsMissingToken() {
|
||||||
|
post(secured, initializeBody(), null).expectStatus(401);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withOidc_rejectsWrongAudience() throws Exception {
|
void required_withOidc_rejectsWrongAudience() throws Exception {
|
||||||
int port = bootSecuredApp("https://mcp.example.com/mcp");
|
|
||||||
String token = provider.signToken("user-1", "https://someone-else.example.com/resource");
|
String token = provider.signToken("user-1", "https://someone-else.example.com/resource");
|
||||||
|
|
||||||
HttpResponse<String> resp = post(port, initializeBody(), token);
|
post(securedWithResourceId, initializeBody(), token).expectStatus(403);
|
||||||
assertEquals(403, resp.statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withOidc_acceptsValidAudience() throws Exception {
|
void required_withOidc_acceptsValidAudience() throws Exception {
|
||||||
String resourceId = "https://mcp.example.com/mcp";
|
String token = provider.signToken("user-1", EXPLICIT_RESOURCE_ID);
|
||||||
int port = bootSecuredApp(resourceId);
|
|
||||||
String token = provider.signToken("user-1", resourceId);
|
|
||||||
|
|
||||||
HttpResponse<String> resp = post(port, initializeBody(), token);
|
post(securedWithResourceId, initializeBody(), token)
|
||||||
assertEquals(200, resp.statusCode());
|
.expectStatus(200)
|
||||||
assertTrue(resp.body().contains("\"protocolVersion\""));
|
.expectBodyContains("\"protocolVersion\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withOidc_noExplicitResourceIdentifier_derivesFromRequestAndEnforcesAudience() throws Exception {
|
void required_withOidc_noExplicitResourceIdentifier_derivesFromRequestAndEnforcesAudience() throws Exception {
|
||||||
int port = bootSecuredApp(null);
|
String derivedResourceId = "http://127.0.0.1:" + secured.port() + "/mcp";
|
||||||
String derivedResourceId = "http://127.0.0.1:" + port + "/mcp";
|
|
||||||
|
|
||||||
String matching = provider.signToken("user-1", derivedResourceId);
|
post(secured, initializeBody(), provider.signToken("user-1", derivedResourceId))
|
||||||
assertEquals(200, post(port, initializeBody(), matching).statusCode());
|
.expectStatus(200);
|
||||||
|
post(secured, initializeBody(), provider.signToken("user-1", "https://someone-else.example.com/resource"))
|
||||||
String mismatched = provider.signToken("user-1", "https://someone-else.example.com/resource");
|
.expectStatus(403);
|
||||||
assertEquals(403, post(port, initializeBody(), mismatched).statusCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withOidc_missingToken_challengeIncludesResourceMetadata() throws Exception {
|
void required_withOidc_missingToken_challengeIncludesResourceMetadata() {
|
||||||
int port = bootSecuredApp(null);
|
FlashResponse response = post(secured, initializeBody(), null).expectStatus(401);
|
||||||
|
|
||||||
HttpResponse<String> resp = post(port, initializeBody(), null);
|
String challenge = response.header("WWW-Authenticate");
|
||||||
assertEquals(401, resp.statusCode());
|
assertTrue(challenge != null && challenge.contains("resource_metadata=\"http://127.0.0.1:"
|
||||||
String challenge = resp.headers().firstValue("WWW-Authenticate").orElse("");
|
+ secured.port() + "/.well-known/oauth-protected-resource/mcp\""),
|
||||||
assertTrue(challenge.contains(
|
|
||||||
"resource_metadata=\"http://127.0.0.1:" + port + "/.well-known/oauth-protected-resource/mcp\""),
|
|
||||||
"WWW-Authenticate: " + challenge);
|
"WWW-Authenticate: " + challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Protected resource metadata ──────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void required_withOidc_noExplicitConfig_publishesProtectedResourceMetadata() throws Exception {
|
void required_withOidc_noExplicitConfig_publishesProtectedResourceMetadata() {
|
||||||
int port = bootSecuredApp(null);
|
FlashResponse response = secured.get("/.well-known/oauth-protected-resource/mcp")
|
||||||
|
.expectStatus(200)
|
||||||
|
.expectBodyContains("\"resource\":\"http://127.0.0.1:" + secured.port() + "/mcp\"")
|
||||||
|
.expectBodyContains("\"authorization_servers\":[\"" + provider.issuer() + "\"]");
|
||||||
|
|
||||||
HttpResponse<String> resp = HttpClient.newHttpClient().send(
|
assertTrue(!response.body().contains("scopes_supported"),
|
||||||
HttpRequest.newBuilder(URI.create(
|
"scopes_supported must be omitted when unset: " + response.body());
|
||||||
"http://127.0.0.1:" + port + "/.well-known/oauth-protected-resource/mcp")).GET().build(),
|
|
||||||
HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
assertEquals(200, resp.statusCode());
|
|
||||||
assertTrue(resp.body().contains("\"resource\":\"http://127.0.0.1:" + port + "/mcp\""), resp.body());
|
|
||||||
assertTrue(resp.body().contains("\"authorization_servers\":[\"" + provider.issuer() + "\"]"), resp.body());
|
|
||||||
assertTrue(!resp.body().contains("scopes_supported"), "scopes_supported must be omitted when unset: " + resp.body());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void scopesSupported_published_inProtectedResourceMetadata() throws Exception {
|
void scopesSupported_published_inProtectedResourceMetadata() {
|
||||||
provider = new FakeOidcProvider();
|
securedWithScopes.get("/.well-known/oauth-protected-resource/mcp")
|
||||||
int port = freePort();
|
.expectStatus(200)
|
||||||
|
.expectBodyContains("\"scopes_supported\":[\"openid\",\"profile\",\"email\"]");
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new OidcExtension(OidcConfig.builder(
|
|
||||||
provider.issuer(), "mcp-client", "secret", "/auth/callback").build()));
|
|
||||||
app.install(new McpExtension(McpConfig.builder("secure-server")
|
|
||||||
.toolsPackage(TOOLS_PACKAGE)
|
|
||||||
.security(McpSecurity.REQUIRED)
|
|
||||||
.scopesSupported("openid", "profile", "email")
|
|
||||||
.build()));
|
|
||||||
app.start();
|
|
||||||
|
|
||||||
HttpResponse<String> resp = HttpClient.newHttpClient().send(
|
|
||||||
HttpRequest.newBuilder(URI.create(
|
|
||||||
"http://127.0.0.1:" + port + "/.well-known/oauth-protected-resource/mcp")).GET().build(),
|
|
||||||
HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
assertEquals(200, resp.statusCode());
|
|
||||||
assertTrue(resp.body().contains("\"scopes_supported\":[\"openid\",\"profile\",\"email\"]"), resp.body());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private int bootSecuredApp(String resourceIdentifier) throws Exception {
|
private static FlashApplication securedApp(String resourceIdentifier, String[] scopesSupported) {
|
||||||
provider = new FakeOidcProvider();
|
return app -> {
|
||||||
int port = freePort();
|
app.install(new OidcExtension(OidcConfig.builder(
|
||||||
|
provider.issuer(), "mcp-client", "secret", "/auth/callback").build()));
|
||||||
|
|
||||||
OidcConfig oidcConfig = OidcConfig.builder(
|
McpConfig.Builder mcp = McpConfig.builder("secure-server")
|
||||||
provider.issuer(), "mcp-client", "secret", "/auth/callback")
|
.toolsPackage(TOOLS_PACKAGE)
|
||||||
.build();
|
.security(McpSecurity.REQUIRED);
|
||||||
|
if (resourceIdentifier != null) mcp.resourceIdentifier(resourceIdentifier);
|
||||||
|
if (scopesSupported != null) mcp.scopesSupported(scopesSupported);
|
||||||
|
app.install(new McpExtension(mcp.build()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var mcpBuilder = McpConfig.builder("secure-server")
|
private static FlashResponse post(FlashTest server, String body, String bearerToken) {
|
||||||
.toolsPackage(TOOLS_PACKAGE)
|
FlashRequest request = server.request().header("Accept", "application/json").json(body);
|
||||||
.security(McpSecurity.REQUIRED);
|
if (bearerToken != null) request.header("Authorization", "Bearer " + bearerToken);
|
||||||
if (resourceIdentifier != null) mcpBuilder.resourceIdentifier(resourceIdentifier);
|
return request.post("/mcp");
|
||||||
|
|
||||||
app = FlashApp.create(port);
|
|
||||||
app.install(new OidcExtension(oidcConfig));
|
|
||||||
app.install(new McpExtension(mcpBuilder.build()));
|
|
||||||
app.start();
|
|
||||||
return port;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String initializeBody() {
|
private static String initializeBody() {
|
||||||
return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}";
|
return "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int freePort() throws Exception {
|
private static FakeOidcProvider newProvider() {
|
||||||
try (ServerSocket s = new ServerSocket(0)) {
|
try {
|
||||||
return s.getLocalPort();
|
return new FakeOidcProvider();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException("Could not start the fake OIDC provider", failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpResponse<String> post(int port, String body, String bearerToken) throws Exception {
|
|
||||||
HttpRequest.Builder req = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/mcp"))
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body));
|
|
||||||
if (bearerToken != null) req.header("Authorization", "Bearer " + bearerToken);
|
|
||||||
return HttpClient.newHttpClient().send(req.build(), HttpResponse.BodyHandlers.ofString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user