Skip to content

Commit

Permalink
Issue #1107 - access token authorization in http header
Browse files Browse the repository at this point in the history
  • Loading branch information
nbartels committed Dec 19, 2020
1 parent 01ba425 commit 11c31ef
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 15 deletions.
50 changes: 35 additions & 15 deletions src/main/java/com/restfb/DefaultFacebookClient.java
Expand Up @@ -93,13 +93,15 @@ public class DefaultFacebookClient extends BaseFacebookClient implements Faceboo
/**
* Version of API endpoint.
*/
protected Version apiVersion = Version.UNVERSIONED;
protected Version apiVersion;

/**
* By default this is <code>false</code>, so real http DELETE is used
*/
protected boolean httpDeleteFallback;

protected boolean accessTokenInHeader;

protected DefaultFacebookClient() {
this(Version.LATEST);
}
Expand Down Expand Up @@ -196,6 +198,17 @@ public DefaultFacebookClient(String accessToken, String appSecret, WebRequestor
graphFacebookExceptionGenerator = new DefaultFacebookExceptionGenerator();
}

/**
* Switch between access token in header and access token in query parameters (default)
*
* @param accessTokenInHttpHeader
* <code>true</code> use access token as header field, <code>false</code> use access token as query parameter
* (default)
*/
public void setHeaderAuthorization(boolean accessTokenInHttpHeader) {
this.accessTokenInHeader = accessTokenInHttpHeader;
}

/**
* override the default facebook exception generator to provide a custom handling for the facebook error objects
*
Expand Down Expand Up @@ -258,8 +271,8 @@ public <T> Connection<T> fetchConnection(String connection, Class<T> connectionT
public <T> Connection<T> fetchConnectionPage(final String connectionPageUrl, Class<T> connectionType) {
String connectionJson;
if (!isBlank(accessToken) && !isBlank(appSecret)) {
connectionJson = makeRequestAndProcessResponse(() -> webRequestor.executeGet(String.format("%s&%s=%s", connectionPageUrl,
urlEncode(APP_SECRET_PROOF_PARAM_NAME), obtainAppSecretProof(accessToken, appSecret))));
connectionJson = makeRequestAndProcessResponse(() -> webRequestor.executeGet(String.format("%s&%s=%s",
connectionPageUrl, urlEncode(APP_SECRET_PROOF_PARAM_NAME), obtainAppSecretProof(accessToken, appSecret))));
} else {
connectionJson = makeRequestAndProcessResponse(() -> webRequestor.executeGet(connectionPageUrl));
}
Expand Down Expand Up @@ -343,7 +356,8 @@ public <T> T publish(String connection, Class<T> objectType, List<BinaryAttachme
@Override
public <T> T publish(String connection, Class<T> objectType, BinaryAttachment binaryAttachment,
Parameter... parameters) {
List<BinaryAttachment> attachments = Optional.ofNullable(binaryAttachment).map(Collections::singletonList).orElse(null);
List<BinaryAttachment> attachments =
Optional.ofNullable(binaryAttachment).map(Collections::singletonList).orElse(null);
return publish(connection, objectType, attachments, parameters);
}

Expand Down Expand Up @@ -441,7 +455,8 @@ public AccessToken obtainAppAccessToken(String appId, String appSecret) {
@Override
public DeviceCode fetchDeviceCode(ScopeBuilder scope) {
verifyParameterPresence(SCOPE, scope);
ObjectUtil.requireNotNull(accessToken, () -> new IllegalStateException("access token is required to fetch a device access token"));
ObjectUtil.requireNotNull(accessToken,
() -> new IllegalStateException("access token is required to fetch a device access token"));

String response = makeRequest("device/login", true, false, null, Parameter.with("type", "device_code"),
Parameter.with(SCOPE, scope.toString()));
Expand All @@ -453,7 +468,8 @@ public AccessToken obtainDeviceAccessToken(String code) throws FacebookDeviceTok
FacebookDeviceTokenPendingException, FacebookDeviceTokenDeclinedException, FacebookDeviceTokenSlowdownException {
verifyParameterPresence("code", code);

ObjectUtil.requireNotNull(accessToken, () -> new IllegalStateException("access token is required to fetch a device access token"));
ObjectUtil.requireNotNull(accessToken,
() -> new IllegalStateException("access token is required to fetch a device access token"));

try {
String response = makeRequest("device/login_status", true, false, null, Parameter.with("type", "device_token"),
Expand Down Expand Up @@ -724,15 +740,19 @@ protected String makeRequest(String endpoint, final boolean executeAsPost, final
final String parameterString = toParameterString(parameters);

return makeRequestAndProcessResponse(() -> {
if (executeAsDelete && !isHttpDeleteFallback()) {
return webRequestor.executeDelete(fullEndpoint + "?" + parameterString);
}
if (accessTokenInHeader) {
webRequestor.setAccessToken(this.accessToken);
}

if (executeAsPost) {
return webRequestor.executePost(fullEndpoint, parameterString, binaryAttachments);
}
if (executeAsDelete && !isHttpDeleteFallback()) {
return webRequestor.executeDelete(fullEndpoint + "?" + parameterString);
}

if (executeAsPost) {
return webRequestor.executePost(fullEndpoint, parameterString, binaryAttachments);
}

return webRequestor.executeGet(fullEndpoint + "?" + parameterString);
return webRequestor.executeGet(fullEndpoint + "?" + parameterString);
});
}

Expand All @@ -749,7 +769,7 @@ public String obtainAppSecretProof(String accessToken, String appSecret) {
/**
* returns if the fallback post method (<code>true</code>) is used or the http delete (<code>false</code>)
*
* @return
* @return {@code true} if POST is used instead of HTTP DELETE (default)
*/
public boolean isHttpDeleteFallback() {
return httpDeleteFallback;
Expand Down Expand Up @@ -836,7 +856,7 @@ protected String toParameterString(Parameter... parameters) {
* If an error occurs when building the parameter string.
*/
protected String toParameterString(boolean withJsonParameter, Parameter... parameters) {
if (!isBlank(accessToken)) {
if (!isBlank(accessToken) && !accessTokenInHeader) {
parameters = parametersWithAdditionalParameter(Parameter.with(ACCESS_TOKEN_PARAM_NAME, accessToken), parameters);
}

Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/restfb/DefaultWebRequestor.java
Expand Up @@ -70,6 +70,8 @@ public class DefaultWebRequestor implements WebRequestor {

private DebugHeaderInfo debugHeaderInfo;

private ThreadLocal<String> accessToken = new ThreadLocal<>();

/**
* By default this is true, to prevent breaking existing usage
*/
Expand All @@ -79,6 +81,11 @@ protected enum HttpMethod {
GET, DELETE, POST
}

@Override
public void setAccessToken(final String accessToken) {
this.accessToken = ThreadLocal.withInitial(() -> accessToken);
}

@Override
public Response executeGet(String url) throws IOException {
return execute(url, HttpMethod.GET);
Expand Down Expand Up @@ -116,6 +123,8 @@ public Response executePost(String url, String parameters, List<BinaryAttachment
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setUseCaches(false);

initHeaderAccessToken(httpUrlConnection);

if (!binaryAttachments.isEmpty()) {
httpUrlConnection.setRequestProperty("Connection", "Keep-Alive");
httpUrlConnection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + MULTIPART_BOUNDARY);
Expand Down Expand Up @@ -170,6 +179,12 @@ public Response executePost(String url, String parameters, List<BinaryAttachment
}
}

protected void initHeaderAccessToken(HttpURLConnection httpUrlConnection) {
if (accessToken.get() != null) {
httpUrlConnection.setRequestProperty("Authorization", "Bearer " + accessToken.get());
}
}

/**
* Given a {@code url}, opens and returns a connection to it.
* <p>
Expand Down Expand Up @@ -333,6 +348,8 @@ private Response execute(String url, HttpMethod httpMethod) throws IOException {
httpUrlConnection.setUseCaches(false);
httpUrlConnection.setRequestMethod(httpMethod.name());

initHeaderAccessToken(httpUrlConnection);

// Allow subclasses to customize the connection if they'd like to - set
// their own headers, timeouts, etc.
customizeConnection(httpUrlConnection);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/restfb/WebRequestor.java
Expand Up @@ -93,6 +93,8 @@ public String toString() {
}
}

void setAccessToken(String accessToken);

/**
* Given a Facebook API endpoint URL, execute a {@code GET} against it.
*
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/com/restfb/ClasspathWebRequestor.java
Expand Up @@ -61,6 +61,11 @@ public Response executePost(String url, String parameters) {
return response;
}

@Override
public void setAccessToken(String accessToken) {

}

/**
* @see com.restfb.WebRequestor#executeGet(java.lang.String)
*/
Expand Down
29 changes: 29 additions & 0 deletions src/test/java/com/restfb/DefaultWebRequestorTest.java
Expand Up @@ -57,6 +57,19 @@ class DefaultWebRequestorTest {
@BeforeEach
void setup() throws IOException {
doReturn(mockUrlConnection).when(requestor).openConnection(any(URL.class));
requestor.setAccessToken(null);
}

@Test
void checkGet_withAccessToken() throws IOException {
requestor.setAccessToken("accesstoken");
String resultString = "This is just a simple Test";
when(mockUrlConnection.getResponseCode()).thenReturn(200);
InputStream stream = new ByteArrayInputStream(resultString.getBytes(StandardCharsets.UTF_8));
when(mockUrlConnection.getInputStream()).thenReturn(stream);
WebRequestor.Response response = requestor.executeGet("http://www.example.org");

verify(mockUrlConnection).setRequestProperty("Authorization", "Bearer accesstoken");
}

@Test
Expand All @@ -75,6 +88,7 @@ void checkGet() throws IOException {
verify(mockUrlConnection).setReadTimeout(180000);
verify(mockUrlConnection).setUseCaches(false);
verify(mockUrlConnection).setRequestMethod(DefaultWebRequestor.HttpMethod.GET.name());
verify(mockUrlConnection, never()).setRequestProperty(eq("Authorization"), anyString());
verify(mockUrlConnection).connect();
verify(requestor).executeGet(anyString());
verify(requestor, never()).executePost(anyString(), anyString());
Expand All @@ -83,6 +97,19 @@ void checkGet() throws IOException {
verify(requestor).fetchResponse(mockUrlConnection);
}

@Test
void checkPost_withAccessToken() throws IOException {
requestor.setAccessToken("accesstoken");
when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
String resultString = "This is just a simple Test";
when(mockUrlConnection.getResponseCode()).thenReturn(200);
InputStream stream = new ByteArrayInputStream(resultString.getBytes(StandardCharsets.UTF_8));
when(mockUrlConnection.getInputStream()).thenReturn(stream);
WebRequestor.Response response = requestor.executePost(exampleUrl, "");

verify(mockUrlConnection).setRequestProperty("Authorization", "Bearer accesstoken");
}

@Test
void checkPost_NoBinary() throws IOException {
when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
Expand All @@ -101,6 +128,7 @@ void checkPost_NoBinary() throws IOException {
verify(mockUrlConnection).setUseCaches(false);
verify(mockUrlConnection).setDoOutput(true);
verify(mockUrlConnection).setRequestMethod(DefaultWebRequestor.HttpMethod.POST.name());
verify(mockUrlConnection, never()).setRequestProperty(eq("Authorization"), anyString());
verify(mockUrlConnection).connect();
verify(requestor).executePost(same(exampleUrl), anyString());
verify(requestor, never()).executeGet(anyString());
Expand Down Expand Up @@ -132,6 +160,7 @@ void checkPost_WithBinary() throws IOException {
verify(mockUrlConnection).setUseCaches(false);
verify(mockUrlConnection).setDoOutput(true);
verify(mockUrlConnection).setRequestProperty("Connection", "Keep-Alive");
verify(mockUrlConnection, never()).setRequestProperty(eq("Authorization"), anyString());
verify(mockUrlConnection).setRequestMethod(DefaultWebRequestor.HttpMethod.POST.name());
verify(mockUrlConnection).connect();
verify(requestor).executePost(same(exampleUrl), anyString(), anyList());
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/com/restfb/FakeWebRequestor.java
Expand Up @@ -41,6 +41,8 @@ public class FakeWebRequestor implements WebRequestor {

private Response predefinedResponse;

private String accessToken;

public FakeWebRequestor() {
this(null);
}
Expand All @@ -49,6 +51,11 @@ public FakeWebRequestor() {
this.predefinedResponse = predefinedResponse;
}

@Override
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

@Override
public Response executeGet(String url) throws IOException {
this.savedUrl = url;
Expand Down

0 comments on commit 11c31ef

Please sign in to comment.