Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 Source Okta: OAuth2.0 authorization method #14710

Merged
merged 15 commits into from
Jul 19, 2022
2 changes: 1 addition & 1 deletion airbyte-integrations/connectors/source-okta/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ RUN pip install .
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.1.6
LABEL io.airbyte.version=0.1.7
LABEL io.airbyte.name=airbyte/source-okta
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ tests:
connection:
- config_path: "secrets/config.json"
status: "succeed"
- config_path: "secrets/config_api_token.json"
status: "succeed"
- config_path: "secrets/config_oauth.json"
status: "succeed"
- config_path: "integration_tests/invalid_config.json"
status: "failed"
discovery:
- config_path: "secrets/config.json"
basic_read:
- config_path: "secrets/config.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
- config_path: "secrets/config_oauth.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
- config_path: "secrets/config_api_token.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
full_refresh:
- config_path: "secrets/config.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
"alternateId": {
"type": ["string", "null"]
},
"detail": {
"additionalProperties": {
"type": ["object", "null"]
},
"detailEntry": {
"type": ["object", "null"]
},
"displayName": {
Expand Down Expand Up @@ -38,11 +35,10 @@
"type": ["string", "null"]
},
"authenticationStep": {
"type": ["integer", "null"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing these nulls? have we confirmed that these fields are really always set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation sets that these fields are not nullable.

"type": ["integer"]
},
"credentialProvider": {
"enum": [
"OKTA_AUTHENTICATION_PROVIDER",
"OKTA_CREDENTIAL_PROVIDER",
"RSA",
"SYMANTEC",
Expand All @@ -63,6 +59,10 @@
"EMAIL",
"OAUTH2",
"JWT",
"CERTIFICATE",
"PRE_SHARED_SYMMETRIC_KEY",
"OKTA_CLIENT_SESSION",
"DEVICE_UDID",
null
],
"type": ["string", "null"]
Expand Down Expand Up @@ -151,9 +151,6 @@
"debugContext": {
"properties": {
"debugData": {
"additionalProperties": {
"type": ["object", "null", "string"]
},
"type": ["object", "null"]
}
},
Expand All @@ -174,7 +171,16 @@
"type": ["string", "null"]
},
"result": {
"type": ["string", "null"]
"enum": [
"SUCCESS",
"FAILURE",
"SKIPPED",
"ALLOW",
"DENY",
"CHALLENGE",
"UNKNOWN"
],
"type": "string"
}
},
"type": ["object", "null"]
Expand Down Expand Up @@ -225,7 +231,7 @@
"type": ["string", "null"]
},
"version": {
"type": "string"
"type": ["string", "null"]
}
},
"type": ["object", "null"]
Expand Down Expand Up @@ -266,19 +272,16 @@
"type": ["string", "null"]
},
"detailEntry": {
"additionalProperties": {
"type": ["object", "null", "string"]
},
"type": ["object", "null"]
},
"displayName": {
"type": ["string", "null"]
},
"id": {
"type": ["string", "null"]
"type": "string"
},
"type": {
"type": ["string", "null"]
"type": "string"
}
},
"type": ["object", "null"]
Expand All @@ -288,9 +291,6 @@
"transaction": {
"properties": {
"detail": {
"additionalProperties": {
"type": ["object", "null", "string"]
},
"type": ["object", "null"]
},
"id": {
Expand Down
63 changes: 54 additions & 9 deletions airbyte-integrations/connectors/source-okta/source_okta/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from airbyte_cdk.sources import AbstractSource
from airbyte_cdk.sources.streams import Stream
from airbyte_cdk.sources.streams.http import HttpStream
from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator
from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator, Oauth2Authenticator


class OktaStream(HttpStream, ABC):
Expand Down Expand Up @@ -204,18 +204,63 @@ def parse_response(
yield from response.json()["roles"]


class OktaOauth2Authenticator(Oauth2Authenticator):
def get_refresh_request_body(self) -> Mapping[str, Any]:
return {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
}

def refresh_access_token(self) -> Tuple[str, int]:
try:
response = requests.request(method="POST", url=self.token_refresh_endpoint, data=self.get_refresh_request_body(),
auth=(self.client_id, self.client_secret))
response.raise_for_status()
response_json = response.json()
return response_json["access_token"], response_json["expires_in"]
except Exception as e:
raise Exception(f"Error while refreshing access token: {e}") from e


class SourceOkta(AbstractSource):
def initialize_authenticator(self, config: Mapping[str, Any]) -> TokenAuthenticator:
return TokenAuthenticator(config["token"], auth_method="SSWS")
def initialize_authenticator(self, config: Mapping[str, Any]):
if "token" in config:
return TokenAuthenticator(config["token"], auth_method="SSWS")

creds = config.get("credentials")
if not creds:
raise "Config validation error. `credentials` not specified."

auth_type = creds.get("auth_type")
if not auth_type:
raise "Config validation error. `auth_type` not specified."

if auth_type == "api_token":
return TokenAuthenticator(creds["api_token"], auth_method="SSWS")

if auth_type == "oauth2.0":
return OktaOauth2Authenticator(
token_refresh_endpoint=self.get_token_refresh_endpoint(config),
client_secret=creds["client_secret"],
client_id=creds["client_id"],
refresh_token=creds["refresh_token"],
)

@staticmethod
def get_url_base(config: Mapping[str, Any]) -> str:
return config.get("base_url") or f"https://{config['domain']}.okta.com"

def get_api_endpoint(self, config: Mapping[str, Any]) -> str:
return parse.urljoin(self.get_url_base(config), "/api/v1/")

def get_url_base(self, config: Mapping[str, Any]) -> str:
return parse.urljoin(config["base_url"], "/api/v1/")
def get_token_refresh_endpoint(self, config: Mapping[str, Any]) -> str:
return parse.urljoin(self.get_url_base(config), "/oauth2/v1/token")

def check_connection(self, logger, config) -> Tuple[bool, any]:
try:
auth = self.initialize_authenticator(config)
base_url = self.get_url_base(config)
url = parse.urljoin(base_url, "users")
api_endpoint = self.get_api_endpoint(config)
url = parse.urljoin(api_endpoint, "users")

response = requests.get(
url,
Expand All @@ -232,11 +277,11 @@ def check_connection(self, logger, config) -> Tuple[bool, any]:

def streams(self, config: Mapping[str, Any]) -> List[Stream]:
auth = self.initialize_authenticator(config)
url_base = self.get_url_base(config)
api_endpoint = self.get_api_endpoint(config)

initialization_params = {
"authenticator": auth,
"url_base": url_base,
"url_base": api_endpoint,
}

return [
Expand Down
123 changes: 112 additions & 11 deletions airbyte-integrations/connectors/source-okta/source_okta/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,121 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Okta Spec",
"type": "object",
"required": ["token", "base_url"],
"additionalProperties": false,
"required": [],
"additionalProperties": true,
"properties": {
"token": {
"domain": {
"type": "string",
"title": "API Token",
"description": "A Okta token. See the <a href=\"https://docs.airbyte.io/integrations/sources/okta\">docs</a> for instructions on how to generate it.",
"airbyte_secret": true
"title": "Okta domain",
"description": "The Okta domain. See the <a href=\"https://docs.airbyte.io/integrations/sources/okta\">docs</a> for instructions on how to find it.",
"airbyte_secret": false
},
"base_url": {
"type": "string",
"title": "Base URL",
"description": "The Okta base URL.",
"airbyte_secret": true
"credentials": {
"title": "Authorization Method *",
"type": "object",
"oneOf": [
{
"type": "object",
"title": "OAuth2.0",
"required": ["auth_type", "client_id", "client_secret", "refresh_token"],
"properties": {
"auth_type": {
"type": "string",
"const": "oauth2.0",
"order": 0
},
"client_id": {
"type": "string",
"title": "Client ID",
"description": "The Client ID of your OAuth application.",
"airbyte_secret": true
},
"client_secret": {
"type": "string",
"title": "Client Secret",
"description": "The Client Secret of your OAuth application.",
"airbyte_secret": true
},
"refresh_token": {
"type": "string",
"title": "Refresh Token",
"description": "Refresh Token to obtain new Access Token, when it's expired.",
"airbyte_secret": true
}
}
},
{
"type": "object",
"title": "API Token",
"required": ["auth_type", "api_token"],
"properties": {
"auth_type": {
"type": "string",
"const": "api_token",
"order": 0
},
"api_token": {
"type": "string",
"title": "Personal API Token",
"description": "An Okta token. See the <a href=\"https://docs.airbyte.io/integrations/sources/okta\">docs</a> for instructions on how to generate it.",
"airbyte_secret": true
}
}
}
]
}
}
},
"advanced_auth": {
"auth_flow_type": "oauth2.0",
"predicate_key": ["credentials", "auth_type"],
"predicate_value": "oauth2.0",
"oauth_config_specification": {
"complete_oauth_output_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"refresh_token": {
"type": "string",
"path_in_connector_config": ["credentials", "refresh_token"]
}
}
},
"complete_oauth_server_input_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"client_id": {
"type": "string"
},
"client_secret": {
"type": "string"
}
}
},
"complete_oauth_server_output_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"client_id": {
"type": "string",
"path_in_connector_config": ["credentials", "client_id"]
},
"client_secret": {
"type": "string",
"path_in_connector_config": ["credentials", "client_secret"]
}
}
},
"oauth_user_input_from_connector_config_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"domain": {
"type": "string",
"path_in_connector_config": ["domain"]
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public OAuthImplementationFactory(final ConfigRepository configRepository, final
.put("airbyte/destination-snowflake", new DestinationSnowflakeOAuthFlow(configRepository, httpClient))
.put("airbyte/destination-google-sheets", new DestinationGoogleSheetsOAuthFlow(configRepository, httpClient))
.put("airbyte/source-snowflake", new SourceSnowflakeOAuthFlow(configRepository, httpClient))
.put("airbyte/source-okta", new OktaOAuthFlow(configRepository, httpClient))
.build();
}

Expand Down
Loading