-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Instagram and Facebook Pages oAuth backend (#7412)
- Loading branch information
Dmytro
authored
Oct 29, 2021
1 parent
ab95855
commit cdb80f4
Showing
10 changed files
with
235 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 0 additions & 72 deletions
72
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/FacebookMarketingOAuthFlow.java
This file was deleted.
Oops, something went wrong.
30 changes: 30 additions & 0 deletions
30
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/facebook/FacebookMarketingOAuthFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows.facebook; | ||
|
||
import com.google.common.annotations.VisibleForTesting; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import java.net.http.HttpClient; | ||
import java.util.function.Supplier; | ||
|
||
public class FacebookMarketingOAuthFlow extends FacebookOAuthFlow { | ||
|
||
private static final String SCOPES = "ads_management,ads_read,read_insights"; | ||
|
||
public FacebookMarketingOAuthFlow(final ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@VisibleForTesting | ||
FacebookMarketingOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier<String> stateSupplier) { | ||
super(configRepository, httpClient, stateSupplier); | ||
} | ||
|
||
@Override | ||
protected String getScopes() { | ||
return SCOPES; | ||
} | ||
|
||
} |
123 changes: 123 additions & 0 deletions
123
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/facebook/FacebookOAuthFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows.facebook; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.base.Preconditions; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.oauth.BaseOAuthFlow; | ||
import java.io.IOException; | ||
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.util.Map; | ||
import java.util.UUID; | ||
import java.util.function.Supplier; | ||
import org.apache.http.client.utils.URIBuilder; | ||
|
||
/** | ||
* Following docs from | ||
* https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow | ||
*/ | ||
public abstract class FacebookOAuthFlow extends BaseOAuthFlow { | ||
|
||
private static final String ACCESS_TOKEN_URL = "https://graph.facebook.com/v12.0/oauth/access_token"; | ||
private static final String AUTH_CODE_TOKEN_URL = "https://www.facebook.com/v12.0/dialog/oauth"; | ||
|
||
public FacebookOAuthFlow(final ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@VisibleForTesting | ||
FacebookOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier<String> stateSupplier) { | ||
super(configRepository, httpClient, stateSupplier); | ||
} | ||
|
||
protected abstract String getScopes(); | ||
|
||
@Override | ||
protected String formatConsentUrl(final UUID definitionId, final String clientId, final String redirectUrl) throws IOException { | ||
try { | ||
return new URIBuilder(AUTH_CODE_TOKEN_URL) | ||
.addParameter("client_id", clientId) | ||
.addParameter("redirect_uri", redirectUrl) | ||
.addParameter("state", getState()) | ||
.addParameter("scope", getScopes()) | ||
.build().toString(); | ||
} catch (final URISyntaxException e) { | ||
throw new IOException("Failed to format Consent URL for OAuth flow", e); | ||
} | ||
} | ||
|
||
@Override | ||
protected String getAccessTokenUrl() { | ||
return ACCESS_TOKEN_URL; | ||
} | ||
|
||
@Override | ||
protected Map<String, Object> extractRefreshToken(final JsonNode data, String accessTokenUrl) throws IOException { | ||
// Facebook does not have refresh token but calls it "long lived access token" instead: | ||
// see https://developers.facebook.com/docs/facebook-login/access-tokens/refreshing | ||
Preconditions.checkArgument(data.has("access_token"), "Missing 'access_token' in query params from %s", ACCESS_TOKEN_URL); | ||
return Map.of("access_token", data.get("access_token").asText()); | ||
} | ||
|
||
@Override | ||
protected Map<String, Object> completeOAuthFlow(final String clientId, | ||
final String clientSecret, | ||
final String authCode, | ||
final String redirectUrl, | ||
JsonNode oAuthParamConfig) | ||
throws IOException { | ||
// Access tokens generated via web login are short-lived tokens | ||
// they arre valid for 1 hour and need to be exchanged for long-lived access token | ||
// https://developers.facebook.com/docs/facebook-login/access-tokens (Short-Term Tokens and | ||
// https://developers.facebook.com/docs/instagram-basic-display-api/overview#short-lived-access-tokens | ||
// Long-Term Tokens section) | ||
|
||
final Map<String, Object> data = super.completeOAuthFlow(clientId, clientSecret, authCode, redirectUrl, oAuthParamConfig); | ||
Preconditions.checkArgument(data.containsKey("access_token")); | ||
final String shortLivedAccessToken = (String) data.get("access_token"); | ||
final String longLivedAccessToken = getLongLivedAccessToken(clientId, clientSecret, shortLivedAccessToken); | ||
return Map.of("access_token", longLivedAccessToken); | ||
} | ||
|
||
protected URI createLongLivedTokenURI(final String clientId, final String clientSecret, final String shortLivedAccessToken) | ||
throws URISyntaxException { | ||
// Exchange Short-lived Access token for Long-lived one | ||
// https://developers.facebook.com/docs/facebook-login/access-tokens/refreshing | ||
// It's valid for 60 days and resreshed once per day if using in requests. | ||
// If no requests are made, the token will expire after about 60 days and | ||
// the person will have to go through the login flow again to get a new | ||
// token. | ||
return new URIBuilder(ACCESS_TOKEN_URL) | ||
.addParameter("client_secret", clientSecret) | ||
.addParameter("client_id", clientId) | ||
.addParameter("grant_type", "fb_exchange_token") | ||
.addParameter("fb_exchange_token", shortLivedAccessToken) | ||
.build(); | ||
} | ||
|
||
protected String getLongLivedAccessToken(final String clientId, final String clientSecret, final String shortLivedAccessToken) throws IOException { | ||
try { | ||
final URI uri = createLongLivedTokenURI(clientId, clientSecret, shortLivedAccessToken); | ||
final HttpRequest request = HttpRequest.newBuilder() | ||
.GET() | ||
.uri(uri) | ||
.build(); | ||
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | ||
final JsonNode responseJson = Jsons.deserialize(response.body()); | ||
Preconditions.checkArgument(responseJson.hasNonNull("access_token"), "%s response should have access_token", responseJson); | ||
return responseJson.get("access_token").asText(); | ||
} catch (final InterruptedException | URISyntaxException e) { | ||
throw new IOException("Failed to complete OAuth flow", e); | ||
} | ||
} | ||
|
||
} |
22 changes: 22 additions & 0 deletions
22
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/facebook/FacebookPagesOAuthFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows.facebook; | ||
|
||
import io.airbyte.config.persistence.ConfigRepository; | ||
|
||
public class FacebookPagesOAuthFlow extends FacebookOAuthFlow { | ||
|
||
private static final String SCOPES = "pages_manage_ads,pages_manage_metadata,pages_read_engagement,pages_read_user_content"; | ||
|
||
public FacebookPagesOAuthFlow(final ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@Override | ||
protected String getScopes() { | ||
return SCOPES; | ||
} | ||
|
||
} |
23 changes: 23 additions & 0 deletions
23
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/facebook/InstagramOAuthFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows.facebook; | ||
|
||
import io.airbyte.config.persistence.ConfigRepository; | ||
|
||
// Instagram Graph API require Facebook API User token | ||
public class InstagramOAuthFlow extends FacebookMarketingOAuthFlow { | ||
|
||
private static final String SCOPES = "ads_management,instagram_basic,instagram_manage_insights,read_insights"; | ||
|
||
public InstagramOAuthFlow(final ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@Override | ||
protected String getScopes() { | ||
return SCOPES; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.