Skip to content

Commit

Permalink
Source Zoom: Replace JWT Auth methods with server-to-server Oauth (#2…
Browse files Browse the repository at this point in the history
…5308)

* Replace JWT Auth methods with server-to-server Oauth

* Bump versions in the Dockerfile and metadata.yaml
  • Loading branch information
KqLLL authored Aug 1, 2023
1 parent 57d3daf commit 4544eb5
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 13 deletions.
2 changes: 1 addition & 1 deletion airbyte-integrations/connectors/source-zoom/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"

ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.1.1
LABEL io.airbyte.version=1.0.0
LABEL io.airbyte.name=airbyte/source-zoom
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"jwt_token": "dummy"
"client_id": "client_id",
"client_secret": "client_secret",
"authorization_endpoint": "https://zoom.us/oauth/token"
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"jwt_token": "abcd"
"account_id": "account_id",
"client_id": "client_id",
"client_secret": "client_secret",
"authorization_endpoint": "https://zoom.us/oauth/token"
}
2 changes: 1 addition & 1 deletion airbyte-integrations/connectors/source-zoom/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: cbfd9856-1322-44fb-bcf1-0b39b7a8e92e
dockerImageTag: 0.1.1
dockerImageTag: 1.0.0
dockerRepository: airbyte/source-zoom
githubIssueLabel: source-zoom
icon: zoom.svg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import base64
import requests
import time

from dataclasses import dataclass
from http import HTTPStatus
from typing import Any, Mapping, Union

from airbyte_cdk.sources.declarative.auth.declarative_authenticator import NoAuth
from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
from airbyte_cdk.sources.declarative.types import Config
from requests import HTTPError

# https://developers.zoom.us/docs/internal-apps/s2s-oauth/#successful-response
# The Bearer token generated by server-to-server token will expire in one hour
BEARER_TOKEN_EXPIRES_IN = 3590


class SingletonMeta(type):
_instances = {}

def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]


@dataclass
class ServerToServerOauthAuthenticator(NoAuth):
config: Config
account_id: Union[InterpolatedString, str]
client_id: Union[InterpolatedString, str]
client_secret: Union[InterpolatedString, str]
authorization_endpoint: Union[InterpolatedString, str]

_instance = None
_generate_token_time = 0
_access_token = None
_grant_type = "account_credentials"

def __post_init__(self, parameters: Mapping[str, Any]):
self._account_id = InterpolatedString.create(self.account_id, parameters=parameters).eval(self.config)
self._client_id = InterpolatedString.create(self.client_id, parameters=parameters).eval(self.config)
self._client_secret = InterpolatedString.create(self.client_secret, parameters=parameters).eval(self.config)
self._authorization_endpoint = InterpolatedString.create(self.authorization_endpoint, parameters=parameters).eval(self.config)

def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
"""Attach the page access token to params to authenticate on the HTTP request"""
if self._access_token is None or ((time.time() - self._generate_token_time) > BEARER_TOKEN_EXPIRES_IN):
self._generate_token_time = time.time()
self._access_token = self.generate_access_token()
headers = {
"Authorization": f"Bearer {self._access_token}",
'Content-type': 'application/json'
}
request.headers.update(headers)

return request

@property
def auth_header(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.token}",
'Content-type': 'application/json'
}

@property
def token(self) -> str:
return self._access_token

def generate_access_token(self) -> str:
self._generate_token_time = time.time()
try:
token = base64.b64encode(f'{self._client_id}:{self._client_secret}'.encode('ascii')).decode('utf-8')
headers = {'Authorization': f'Basic {token}',
'Content-type': 'application/json'}
rest = requests.post(
url=f"{self._authorization_endpoint}?grant_type={self._grant_type}&account_id={self._account_id}",
headers=headers
)
if rest.status_code != HTTPStatus.OK:
raise HTTPError(rest.text)
return rest.json().get("access_token")
except Exception as e:
raise Exception(f"Error while generating access token: {e}") from e
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
version: "0.29.0"

definitions:
# Server to Server Oauth Authenticator
requester:
url_base: "https://api.zoom.us/v2/"
url_base: "https://api.zoom.us/v2"
http_method: "GET"
authenticator:
type: BearerAuthenticator
api_token: "{{ config['jwt_token'] }}"
class_name: source_zoom.components.ServerToServerOauthAuthenticator
client_id: "{{ config['client_id'] }}"
account_id: "{{ config['account_id'] }}"
client_secret: "{{ config['client_secret'] }}"
authorization_endpoint: "{{ config['authorization_endpoint'] }}"
grant_type: "account_credentials"

zoom_paginator:
type: DefaultPaginator
Expand Down
22 changes: 19 additions & 3 deletions airbyte-integrations/connectors/source-zoom/source_zoom/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@ connectionSpecification:
title: Zoom Spec
type: object
required:
- jwt_token
- account_id
- client_id
- client_secret
- authorization_endpoint
additionalProperties: true
properties:
jwt_token:
account_id:
type: string
description: JWT Token
order: 0
description: "The account ID for your Zoom account. You can find this in the Zoom Marketplace under the \"Manage\" tab for your app."
client_id:
type: string
order: 1
description: "The client ID for your Zoom app. You can find this in the Zoom Marketplace under the \"Manage\" tab for your app."
client_secret:
type: string
order: 2
description: "The client secret for your Zoom app. You can find this in the Zoom Marketplace under the \"Manage\" tab for your app."
airbyte_secret: true
authorization_endpoint:
type: string
order: 3
default: "https://zoom.us/oauth/token"
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import base64
from http import HTTPStatus
import unittest
import requests
import requests_mock
from source_zoom.components import ServerToServerOauthAuthenticator


class TestOAuthClient(unittest.TestCase):
def test_generate_access_token(self):
except_access_token = "rc-test-token"
except_token_response = {"access_token": except_access_token}

config = {
"account_id": "rc-asdfghjkl",
"client_id": "rc-123456789",
"client_secret": "rc-test-secret",
"authorization_endpoint": "https://example.zoom.com/oauth/token",
"grant_type": "account_credentials"
}
parameters = config
client = ServerToServerOauthAuthenticator(config=config,
account_id=config["account_id"],
client_id=config["client_id"],
client_secret=config["client_secret"],
grant_type=config["grant_type"],
authorization_endpoint=config["authorization_endpoint"],
parameters=parameters)

# Encode the client credentials in base64
token = base64.b64encode(f'{config.get("client_id")}:{config.get("client_secret")}'.encode('ascii')).decode('utf-8')

# Define the headers that should be sent in the request
headers = {'Authorization': f'Basic {token}',
'Content-type': 'application/json'}

# Define the URL containing the grant_type and account_id as query parameters
url = f'{config.get("authorization_endpoint")}?grant_type={config.get("grant_type")}&account_id={config.get("account_id")}'

with requests_mock.Mocker() as m:
# Mock the requests.post call with the expected URL, headers and token response
m.post(url, json=except_token_response, request_headers=headers, status_code=HTTPStatus.OK)

# Call the generate_access_token function and assert it returns the expected access token
self.assertEqual(client.generate_access_token(), except_access_token)

# Test case when the endpoint has some error, like a timeout
with requests_mock.Mocker() as m:
m.post(url, exc=requests.exceptions.RequestException)
with self.assertRaises(Exception) as cm:
client.generate_access_token()
self.assertIn("Error while generating access token", str(cm.exception))


if __name__ == "__main__":
unittest.main()
13 changes: 10 additions & 3 deletions docs/integrations/sources/zoom.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,22 @@ Please [create an issue](https://github.com/airbytehq/airbyte/issues) if you see

### Requirements

* Zoom JWT Token
* Zoom Server-to-Server Oauth App

### Setup guide
Please read [How to generate your Server-to-Server OAuth app ](https://developers.zoom.us/docs/internal-apps/s2s-oauth/).

:::info

JWT Tokens are deprecated, only Server-to-Server works now. [link to Zoom](https://developers.zoom.us/docs/internal-apps/jwt-faq/)

:::

Please read [How to generate your JWT Token](https://marketplace.zoom.us/docs/guides/build/jwt-app).

## Changelog

| Version | Date | Pull Request | Subject |
| :------ | :--------- | :------------------------------------------------------- | :--------------------------------------------------------------------- |
|:--------|:-----------|:---------------------------------------------------------| :--------------------------------------------------------------------- |
| 1.0.0 | 2023-7-28 | [25308](https://github.com/airbytehq/airbyte/pull/25308) | Replace JWT Auth methods with server-to-server Oauth |
| 0.1.1 | 2022-11-30 | [19939](https://github.com/airbytehq/airbyte/pull/19939) | Upgrade CDK version to fix bugs with SubStreamSlicer |
| 0.1.0 | 2022-10-25 | [18179](https://github.com/airbytehq/airbyte/pull/18179) | Initial Release |

0 comments on commit 4544eb5

Please sign in to comment.