Working with Secure File Access
Overview
Below are a some valuable tips for working with secure file access and the Procore API.
Handling URL Schema Changes
As indicated in the “File Link URL Schema Change” announcement posted 06/28/2023, developers should not assume any particular hostnames for download links because they can change in future, both due to Procore backend changes as well as changes from our cloud service partner, Amazon Web Services.
Passing the Bearer Token for File Downloads
As described in the “Bearer Token to be Required for File Access Authorization” announcement posted 06/10/2022, we require developers to include the Procore Bearer Token for accessing any file or document URL.
One important detail is that files are not necessarily served directly by those URLs.
For example, clients currently get redirected to our cloud service partner, Amazon Web Services, to actually fetch file content.
It is therefore important to only include Procore Bearer Tokens in requests to Procore domains (*.procore.com) and not to include them in requests going elsewhere (also due to automatic redirection).
Different HTTP clients behave differently with respect to re-attaching “Authorization” HTTP header in case of redirects.
You should check the documentation for your particular client.
For convenience, we include below an explicit implementation using the standard java.net.http.HttpClient
.
Example Implementation
The following example implementation is based on java.net.http.HttpClient
from Java 17.0.2.
Downloading occurs in the downloadFile
method that accepts:
-
fileUrl
is a link for file download. You can obtain it from a Procore API response. For example, the response forGET /rest/v1.0/folders
contains this url underfiles[].file_versions[].url
(Note: most of the fields are omitted in the example below):{ "id": 1, "files": [ { "id": 12, "name": "file.pdf", "file_versions": [ { "id": 12, "notes": "These are notes about the current file version.", "url": "www.file.com", } ] } ] }
-
bearerTokenValue
is a valid Procore OAuth 2.0 Access Token.
Complete Listing
package com.procore.file_access_demo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import java.util.Set;
public class DownloaderApp {
// Can be taken from response body of
// https://developers.procore.com/reference/rest/v1/project-folders-and-files?version=1.0#list-project-folders-and-files
private static final String FILE_URL = "";
private static final String BEARER_TOKEN_VALUE = "";
public static void main(String[] args) throws Exception {
downloadFile(FILE_URL, BEARER_TOKEN_VALUE);
}
private static void downloadFile(String fileUrl, String bearerTokenValue) throws URISyntaxException, IOException, InterruptedException {
// Create an HttpClient that doesn't follow redirects
// to make sure we don't send Bearer token to non-Procore domains
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.build();
// Perform the initial request
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(fileUrl))
.header("Authorization", "Bearer %s".formatted(bearerTokenValue))
.build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
// Loop through redirects preventing infinite loops
int maxRedirects = 5;
int redirectCount = 0;
while (Set.of(301, 302, 303, 307, 308).contains(response.statusCode())
&& (redirectCount++ < maxRedirects)) {
Optional<String> location = response.headers().firstValue("Location");
if (location.isEmpty()) {
throw new RuntimeException("Redirect response did not contain Location header");
} else {
URI redirectLocation = new URI(location.get());
HttpRequest.Builder redirectRequestBuilder = HttpRequest.newBuilder()
.uri(redirectLocation);
// Only include Bearer token for Procore domains
String procoreDomain = ".procore.com";
if (redirectLocation.getHost().endsWith(procoreDomain)) {
redirectRequestBuilder.header("Authorization", "Bearer %s".formatted(bearerTokenValue));
}
// Follow the redirect
response = client.send(redirectRequestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
}
}
if (response.statusCode() == 200) {
// Save the file to disk
Files.copy(response.body(), Path.of("/tmp/file.bin"), StandardCopyOption.REPLACE_EXISTING);
}
}
}
Testing Your Integration
We are currently working on enabling this new functionality in Monthly and Developer Sandboxes where you will be able to test your integrations.