-
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.
- Loading branch information
Dmytro
authored
Oct 18, 2021
1 parent
cf04b8b
commit 04989bc
Showing
7 changed files
with
352 additions
and
39 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
76 changes: 76 additions & 0 deletions
76
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/AsanaOAuthFlow.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,76 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.collect.ImmutableMap; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.oauth.BaseOAuthFlow; | ||
import java.io.IOException; | ||
import java.net.URISyntaxException; | ||
import java.net.http.HttpClient; | ||
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.asana.com/docs/oauth | ||
*/ | ||
public class AsanaOAuthFlow extends BaseOAuthFlow { | ||
|
||
private static final String AUTHORIZE_URL = "https://app.asana.com/-/oauth_authorize"; | ||
private static final String ACCESS_TOKEN_URL = "https://app.asana.com/-/oauth_token"; | ||
|
||
public AsanaOAuthFlow(ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@VisibleForTesting | ||
AsanaOAuthFlow(ConfigRepository configRepository, HttpClient httpClient, Supplier<String> stateSupplier) { | ||
super(configRepository, httpClient, stateSupplier); | ||
} | ||
|
||
@Override | ||
protected String formatConsentUrl(UUID definitionId, String clientId, String redirectUrl) throws IOException { | ||
try { | ||
return new URIBuilder(AUTHORIZE_URL) | ||
.addParameter("client_id", clientId) | ||
.addParameter("redirect_uri", redirectUrl) | ||
.addParameter("response_type", "code") | ||
.addParameter("state", getState()) | ||
.build().toString(); | ||
} catch (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, String> getAccessTokenQueryParameters(String clientId, String clientSecret, String authCode, String redirectUrl) { | ||
return ImmutableMap.<String, String>builder() | ||
.putAll(super.getAccessTokenQueryParameters(clientId, clientSecret, authCode, redirectUrl)) | ||
.put("grant_type", "authorization_code") | ||
.build(); | ||
} | ||
|
||
@Override | ||
protected Map<String, Object> extractRefreshToken(JsonNode data) throws IOException { | ||
System.out.println(Jsons.serialize(data)); | ||
if (data.has("refresh_token")) { | ||
final String refreshToken = data.get("refresh_token").asText(); | ||
return Map.of("credentials", Map.of("refresh_token", refreshToken)); | ||
} else { | ||
throw new IOException(String.format("Missing 'refresh_token' in query params from %s", ACCESS_TOKEN_URL)); | ||
} | ||
} | ||
|
||
} |
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
173 changes: 173 additions & 0 deletions
173
...oauth/src/test-integration/java/io.airbyte.oauth.flows/AsanaOAuthFlowIntegrationTest.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,173 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.collect.ImmutableMap; | ||
import com.sun.net.httpserver.HttpExchange; | ||
import com.sun.net.httpserver.HttpHandler; | ||
import com.sun.net.httpserver.HttpServer; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.SourceOAuthParameter; | ||
import io.airbyte.config.persistence.ConfigNotFoundException; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.validation.json.JsonValidationException; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.net.InetSocketAddress; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class AsanaOAuthFlowIntegrationTest { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(AsanaOAuthFlowIntegrationTest.class); | ||
private static final String REDIRECT_URL = "http://localhost:8000/code"; | ||
private static final Path CREDENTIALS_PATH = Path.of("secrets/asana.json"); | ||
|
||
private ConfigRepository configRepository; | ||
private AsanaOAuthFlow asanaOAuthFlow; | ||
private HttpServer server; | ||
private ServerHandler serverHandler; | ||
|
||
@BeforeEach | ||
public void setup() throws IOException { | ||
if (!Files.exists(CREDENTIALS_PATH)) { | ||
throw new IllegalStateException( | ||
"Must provide path to a oauth credentials file."); | ||
} | ||
configRepository = mock(ConfigRepository.class); | ||
asanaOAuthFlow = new AsanaOAuthFlow(configRepository); | ||
|
||
server = HttpServer.create(new InetSocketAddress(8000), 0); | ||
server.setExecutor(null); // creates a default executor | ||
server.start(); | ||
serverHandler = new ServerHandler("code"); | ||
server.createContext("/code", serverHandler); | ||
} | ||
|
||
@AfterEach | ||
void tearDown() { | ||
server.stop(1); | ||
} | ||
|
||
@Test | ||
public void testFullAsanaOAuthFlow() throws InterruptedException, ConfigNotFoundException, IOException, JsonValidationException { | ||
int limit = 20; | ||
final UUID workspaceId = UUID.randomUUID(); | ||
final UUID definitionId = UUID.randomUUID(); | ||
final String fullConfigAsString = new String(Files.readAllBytes(CREDENTIALS_PATH)); | ||
final JsonNode credentialsJson = Jsons.deserialize(fullConfigAsString); | ||
final String clientId = credentialsJson.get("client_id").asText(); | ||
when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() | ||
.withOauthParameterId(UUID.randomUUID()) | ||
.withSourceDefinitionId(definitionId) | ||
.withWorkspaceId(workspaceId) | ||
.withConfiguration(Jsons.jsonNode(ImmutableMap.builder() | ||
.put("client_id", clientId) | ||
.put("client_secret", credentialsJson.get("client_secret").asText()) | ||
.build())))); | ||
final String url = asanaOAuthFlow.getSourceConsentUrl(workspaceId, definitionId, REDIRECT_URL); | ||
LOGGER.info("Waiting for user consent at: {}", url); | ||
// TODO: To automate, start a selenium job to navigate to the Consent URL and click on allowing | ||
// access... | ||
while (!serverHandler.isSucceeded() && limit > 0) { | ||
Thread.sleep(1000); | ||
limit -= 1; | ||
} | ||
assertTrue(serverHandler.isSucceeded(), "Failed to get User consent on time"); | ||
final Map<String, Object> params = asanaOAuthFlow.completeSourceOAuth(workspaceId, definitionId, | ||
Map.of("code", serverHandler.getParamValue()), REDIRECT_URL); | ||
LOGGER.info("Response from completing OAuth Flow is: {}", params.toString()); | ||
assertTrue(params.containsKey("credentials")); | ||
final Map creds = (Map) params.get("credentials"); | ||
assertTrue(creds.containsKey("refresh_token")); | ||
assertTrue(creds.get("refresh_token").toString().length() > 0); | ||
} | ||
|
||
static class ServerHandler implements HttpHandler { | ||
|
||
final private String expectedParam; | ||
private Map responseQuery; | ||
private String paramValue; | ||
private boolean succeeded; | ||
|
||
public ServerHandler(String expectedParam) { | ||
this.expectedParam = expectedParam; | ||
this.paramValue = ""; | ||
this.succeeded = false; | ||
} | ||
|
||
public boolean isSucceeded() { | ||
return succeeded; | ||
} | ||
|
||
public String getParamValue() { | ||
return paramValue; | ||
} | ||
|
||
public Map getResponseQuery() { | ||
return responseQuery; | ||
} | ||
|
||
@Override | ||
public void handle(HttpExchange t) { | ||
final String query = t.getRequestURI().getQuery(); | ||
LOGGER.info("Received query: '{}'", query); | ||
final Map<String, String> data; | ||
try { | ||
data = deserialize(query); | ||
final String response; | ||
if (data != null && data.containsKey(expectedParam)) { | ||
paramValue = data.get(expectedParam); | ||
response = String.format("Successfully extracted %s:\n'%s'\nTest should be continuing the OAuth Flow to retrieve the refresh_token...", | ||
expectedParam, paramValue); | ||
responseQuery = data; | ||
LOGGER.info(response); | ||
t.sendResponseHeaders(200, response.length()); | ||
succeeded = true; | ||
} else { | ||
response = String.format("Unable to parse query params from redirected url: %s", query); | ||
t.sendResponseHeaders(500, response.length()); | ||
} | ||
final OutputStream os = t.getResponseBody(); | ||
os.write(response.getBytes()); | ||
os.close(); | ||
} catch (RuntimeException | IOException e) { | ||
LOGGER.error("Failed to parse from body {}", query, e); | ||
} | ||
} | ||
|
||
private static Map<String, String> deserialize(String query) { | ||
if (query == null) { | ||
return null; | ||
} | ||
final Map<String, String> result = new HashMap<>(); | ||
for (String param : query.split("&")) { | ||
String[] entry = param.split("="); | ||
if (entry.length > 1) { | ||
result.put(entry[0], entry[1]); | ||
} else { | ||
result.put(entry[0], ""); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
} | ||
|
||
} |
Oops, something went wrong.