Skip to content

Commit

Permalink
Feat: Add Audience + Scopes Param to PKCE and DeviceCode Auth Flows (f…
Browse files Browse the repository at this point in the history
…lyteorg#1876)

This PR adds the audience parameter into the PKCE/AuthorizationClient and DeviceCodeAuthenticator auth flows. This param is derived from auth_helper.RemoteClientConfigStore.get_client_config()

Also, the scopes configuration - derived from config.yaml by way of configuration.PlatformConfig is now exposed to the aforementioned auth flows.

Changes are also made to the way the scopes parameter is parsed in the token_client module to bring it in line with the AuthorizationClient class flow.

This now makes it so that Auth0 can use the PKCE and DeviceCode Auth flows without errors.

Signed-off-by: tnam <tommy.nam@aiq-index.com>
  • Loading branch information
PudgyPigeon authored and ringohoffman committed Nov 24, 2023
1 parent 5832277 commit 126bbfa
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 7 deletions.
7 changes: 7 additions & 0 deletions flytekit/clients/auth/auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def __init__(
endpoint: str,
auth_endpoint: str,
token_endpoint: str,
audience: typing.Optional[str] = None,
scopes: typing.Optional[typing.List[str]] = None,
client_id: typing.Optional[str] = None,
redirect_uri: typing.Optional[str] = None,
Expand All @@ -196,6 +197,7 @@ def __init__(
:param endpoint: str endpoint to connect to
:param auth_endpoint: str endpoint where auth metadata can be found
:param token_endpoint: str endpoint to retrieve token from
:param audience: (optional) Audience parameter for Auth0
:param scopes: list[str] oauth2 scopes
:param client_id: oauth2 client id
:param redirect_uri: oauth2 redirect uri
Expand Down Expand Up @@ -227,6 +229,7 @@ def __init__(
self._remote = endpoint_metadata
self._token_endpoint = token_endpoint
self._client_id = client_id
self._audience = audience
self._scopes = scopes or []
self._redirect_uri = redirect_uri
state = _generate_state_parameter()
Expand All @@ -246,6 +249,10 @@ def __init__(
"state": state,
}

# Conditionally add audience param if provided - value is not None
if self._audience:
self._request_auth_code_params["audience"] = self._audience

if request_auth_code_params:
# Allow adding additional parameters to the request_auth_code_params
self._request_auth_code_params.update(request_auth_code_params)
Expand Down
26 changes: 22 additions & 4 deletions flytekit/clients/auth/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,19 @@ def refresh_credentials(self):
class PKCEAuthenticator(Authenticator):
"""
This Authenticator encapsulates the entire PKCE flow and automatically opens a browser window for login
For Auth0 - you will need to manually configure your config.yaml to include a scopes list of the syntax:
admin.scopes: ["offline_access", "offline", "all", "openid"] and/or similar scopes in order to get the refresh token +
caching. Otherwise, it will just receive the access token alone. Your FlyteCTL Helm config however should only
contain ["offline", "all"] - as OIDC scopes are ungrantable in Auth0 customer APIs. They are simply requested
for in the POST request during the token caching process.
"""

def __init__(
self,
endpoint: str,
cfg_store: ClientConfigStore,
scopes: typing.Optional[typing.List[str]] = None,
header_key: typing.Optional[str] = None,
verify: typing.Optional[typing.Union[bool, str]] = None,
session: typing.Optional[requests.Session] = None,
Expand All @@ -104,6 +111,7 @@ def __init__(
super().__init__(endpoint, header_key, KeyringStore.retrieve(endpoint), verify=verify)
self._cfg_store = cfg_store
self._auth_client = None
self._scopes = scopes
self._session = session or requests.Session()

def _initialize_auth_client(self):
Expand All @@ -120,7 +128,11 @@ def _initialize_auth_client(self):
endpoint=self._endpoint,
redirect_uri=cfg.redirect_uri,
client_id=cfg.client_id,
scopes=cfg.scopes,
# Audience only needed for Auth0 - Taken from client config
audience=cfg.audience,
scopes=self._scopes or cfg.scopes,
# self._scopes refers to flytekit.configuration.PlatformConfig (config.yaml)
# cfg.scopes refers to PublicClientConfig scopes (can be defined in Helm deployments)
auth_endpoint=cfg.authorization_endpoint,
token_endpoint=cfg.token_endpoint,
verify=self._verify,
Expand Down Expand Up @@ -254,15 +266,19 @@ def __init__(
cfg_store: ClientConfigStore,
header_key: typing.Optional[str] = None,
audience: typing.Optional[str] = None,
scopes: typing.Optional[typing.List[str]] = None,
http_proxy_url: typing.Optional[str] = None,
verify: typing.Optional[typing.Union[bool, str]] = None,
session: typing.Optional[requests.Session] = None,
):
self._audience = audience
cfg = cfg_store.get_client_config()
self._audience = audience or cfg.audience
self._client_id = cfg.client_id
self._device_auth_endpoint = cfg.device_authorization_endpoint
self._scope = cfg.scopes
# Input param: scopes refers to flytekit.configuration.PlatformConfig (config.yaml)
# cfg.scopes refers to PublicClientConfig scopes (can be defined in Helm deployments)
# Use "scope" from object instantiation if value is not None - otherwise, default to cfg.scopes
self._scopes = scopes or cfg.scopes
self._token_endpoint = cfg.token_endpoint
if self._device_auth_endpoint is None:
raise AuthenticationError(
Expand All @@ -282,7 +298,7 @@ def refresh_credentials(self):
self._device_auth_endpoint,
self._client_id,
self._audience,
self._scope,
self._scopes,
self._http_proxy_url,
self._verify,
self._session,
Expand All @@ -296,6 +312,8 @@ def refresh_credentials(self):
resp,
self._token_endpoint,
client_id=self._client_id,
audience=self._audience,
scopes=self._scopes,
http_proxy_url=self._http_proxy_url,
verify=self._verify,
)
Expand Down
8 changes: 6 additions & 2 deletions flytekit/clients/auth/token_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def get_token(
if device_code:
body["device_code"] = device_code
if scopes is not None:
body["scope"] = ",".join(scopes)
body["scope"] = " ".join(s.strip("' ") for s in scopes).strip("[]'")
if audience:
body["audience"] = audience

Expand Down Expand Up @@ -135,7 +135,7 @@ def get_device_code(
Retrieves the device Authentication code that can be done to authenticate the request using a browser on a
separate device
"""
_scope = " ".join(scope) if scope is not None else ""
_scope = " ".join(s.strip("' ") for s in scope).strip("[]'") if scope is not None else ""
payload = {"client_id": client_id, "scope": _scope, "audience": audience}
proxies = {"https": http_proxy_url, "http": http_proxy_url} if http_proxy_url else None
if not session:
Expand All @@ -150,6 +150,8 @@ def poll_token_endpoint(
resp: DeviceCodeResponse,
token_endpoint: str,
client_id: str,
audience: typing.Optional[str] = None,
scopes: typing.Optional[str] = None,
http_proxy_url: typing.Optional[str] = None,
verify: typing.Optional[typing.Union[bool, str]] = None,
) -> typing.Tuple[str, int]:
Expand All @@ -162,6 +164,8 @@ def poll_token_endpoint(
token_endpoint,
grant_type=GrantType.DEVICE_CODE,
client_id=client_id,
audience=audience,
scopes=scopes,
device_code=resp.device_code,
http_proxy_url=http_proxy_url,
verify=verify,
Expand Down
3 changes: 2 additions & 1 deletion flytekit/clients/auth_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def get_authenticator(cfg: PlatformConfig, cfg_store: ClientConfigStore) -> Auth
session = get_session(cfg)

if cfg_auth == AuthType.STANDARD or cfg_auth == AuthType.PKCE:
return PKCEAuthenticator(cfg.endpoint, cfg_store, verify=verify, session=session)
return PKCEAuthenticator(cfg.endpoint, cfg_store, scopes=cfg.scopes, verify=verify, session=session)
elif cfg_auth == AuthType.BASIC or cfg_auth == AuthType.CLIENT_CREDENTIALS or cfg_auth == AuthType.CLIENTSECRET:
return ClientCredentialsAuthenticator(
endpoint=cfg.endpoint,
Expand All @@ -97,6 +97,7 @@ def get_authenticator(cfg: PlatformConfig, cfg_store: ClientConfigStore) -> Auth
endpoint=cfg.endpoint,
cfg_store=cfg_store,
audience=cfg.audience,
scopes=cfg.scopes,
http_proxy_url=cfg.http_proxy_url,
verify=verify,
session=session,
Expand Down
4 changes: 4 additions & 0 deletions flytekit/configuration/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,16 @@ class Credentials(object):
"""

SCOPES = ConfigEntry(LegacyConfigEntry(SECTION, "scopes", list), YamlConfigEntry("admin.scopes", list))
"""
This setting can be used to manually pass in scopes into authenticator flows - eg.) for Auth0 compatibility
"""

AUTH_MODE = ConfigEntry(LegacyConfigEntry(SECTION, "auth_mode"), YamlConfigEntry("admin.authType"))
"""
The auth mode defines the behavior used to request and refresh credentials. The currently supported modes include:
- 'standard' or 'Pkce': This uses the pkce-enhanced authorization code flow by opening a browser window to initiate
credentials access.
- "DeviceFlow": This uses the Device Authorization Flow
- 'basic', 'client_credentials' or 'clientSecret': This uses symmetric key auth in which the end user enters a
client id and a client secret and public key encryption is used to facilitate authentication.
- None: No auth will be attempted.
Expand Down

0 comments on commit 126bbfa

Please sign in to comment.