Mitigating CORS Errors With Custom Jira REST API Endpoints
If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing. Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain. CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.
Let’s talk about the example that I encountered. I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro. However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself. Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.
We can address this by creating a custom REST API endpoint in Confluence (or Jira). In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.
In other words, the custom REST API endpoint acts like a middleman that doesn’t trip the CORS errors.
Setting up a REST API Endpoint
In order to set up a custom REST API in Confluence or Jira, you’ll need ScriptRunner. You’ll also need the URL of the REST API you’d like to connect to, and the credentials to access it.
REST APIs are a function of ScriptRunner:
Add a new endpoint, and choose “Custom Endpoint” as the type. Finally, give it a name and provide the code. Here’s the basic framework of some code that will run as a REST API, courtesy of the Adaptavist Script Library:
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@BaseScript CustomEndpointDelegate delegate
doSomething(httpMethod: "GET") { MultivaluedMap queryParams, String body ->
//Define the URL of the third-party API with the dynamic dates
def apiUrl = "<API URL>"
//Define the username and password for basic authentication
def username = "<username>"
def password = "<password>"
//Encode the username and password
def encodedCredentials = Base64.getEncoder().encodeToString("$username:$password".getBytes())
//Make a request to the third-party API with basic authentication
def connection = new URL(apiUrl).openConnection() as HttpURLConnection
connection.setRequestProperty("Authorization", "Basic $encodedCredentials")
connection.setRequestProperty("Accept", "application/json")
connection.connect()
def responseCode = connection.responseCode
def headers = connection.getHeaderFields()
def responseBody = connection.inputStream.text
connection.disconnect()
//Log the response details
logResponse(responseCode, headers, responseBody)
//Process the API response
def result = processApiResponse(responseBody)
//Return the processed result as the response
return Response.ok(new JsonBuilder(result).toString())
.header("Content-Type", "application/json")
.header('Accept', 'application/json')
.build()
}
//Log the response details
def logResponse(int responseCode, Map<String, List<String>> headers, String responseBody) {
log.warn("Response Code: $responseCode")
headers.each { headerName, headerValues ->
log.warn("Header: $headerName - ${headerValues.join(', ')}")
}
log.warn("Response Body: $responseBody")
}
//Process the response from the third-party API
def processApiResponse(String apiResponse) {
def responseJson = new JsonSlurper().parseText(apiResponse)
//Extract the desired data from the API response
def extractedData = responseJson
//Perform any additional processing or transformation as needed
//Return the processed data
return [data: extractedData]
}
In this case we call the REST endpoint doSomething, and we access it by calling /rest/scriptrunner/latest/custom/doSomething. Because the credentials are stored in the code for the endpoint, the macro that calls the endpoint doesn’t need to worry about authenticating. The most basic example of a script in Confluence DC using a custom REST API endpoint is this:
import org.apache.http.HttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.util.EntityUtils
import javax.xml.bind.DatatypeConverter
// Create an HTTP client
CloseableHttpClient httpClient = HttpClientBuilder.create().build()
// Define the URL for the HTTP request
String url = "https://<Confluence DC URL>/rest/scriptrunner/latest/custom/doSomething"
try {
// Create an HTTP GET request
HttpGet httpGet = new HttpGet(url)
// Execute the request and retrieve the response
HttpResponse response = httpClient.execute(httpGet)
// Get the response body as a String
String responseBody = EntityUtils.toString(response.getEntity())
// Print the response body
log.warn "Response: $responseBody"
} finally {
// Close the HTTP client
httpClient.close()
}
This calls the REST API endpoint stored on the Confluence Server, which returns the call made to the third-party API as JSON. From there, you can do whatever you’d normally do with a blob of JSON.
Leave a Reply