Skip to content

Commit 126bbfa

Browse files
PudgyPigeonMatthew Hoffman
authored andcommitted
Feat: Add Audience + Scopes Param to PKCE and DeviceCode Auth Flows (flyteorg#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 <[email protected]>
1 parent 5832277 commit 126bbfa

File tree

5 files changed

+41
-7
lines changed

5 files changed

+41
-7
lines changed

flytekit/clients/auth/auth_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def __init__(
179179
endpoint: str,
180180
auth_endpoint: str,
181181
token_endpoint: str,
182+
audience: typing.Optional[str] = None,
182183
scopes: typing.Optional[typing.List[str]] = None,
183184
client_id: typing.Optional[str] = None,
184185
redirect_uri: typing.Optional[str] = None,
@@ -196,6 +197,7 @@ def __init__(
196197
:param endpoint: str endpoint to connect to
197198
:param auth_endpoint: str endpoint where auth metadata can be found
198199
:param token_endpoint: str endpoint to retrieve token from
200+
:param audience: (optional) Audience parameter for Auth0
199201
:param scopes: list[str] oauth2 scopes
200202
:param client_id: oauth2 client id
201203
:param redirect_uri: oauth2 redirect uri
@@ -227,6 +229,7 @@ def __init__(
227229
self._remote = endpoint_metadata
228230
self._token_endpoint = token_endpoint
229231
self._client_id = client_id
232+
self._audience = audience
230233
self._scopes = scopes or []
231234
self._redirect_uri = redirect_uri
232235
state = _generate_state_parameter()
@@ -246,6 +249,10 @@ def __init__(
246249
"state": state,
247250
}
248251

252+
# Conditionally add audience param if provided - value is not None
253+
if self._audience:
254+
self._request_auth_code_params["audience"] = self._audience
255+
249256
if request_auth_code_params:
250257
# Allow adding additional parameters to the request_auth_code_params
251258
self._request_auth_code_params.update(request_auth_code_params)

flytekit/clients/auth/authenticator.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,19 @@ def refresh_credentials(self):
8888
class PKCEAuthenticator(Authenticator):
8989
"""
9090
This Authenticator encapsulates the entire PKCE flow and automatically opens a browser window for login
91+
92+
For Auth0 - you will need to manually configure your config.yaml to include a scopes list of the syntax:
93+
admin.scopes: ["offline_access", "offline", "all", "openid"] and/or similar scopes in order to get the refresh token +
94+
caching. Otherwise, it will just receive the access token alone. Your FlyteCTL Helm config however should only
95+
contain ["offline", "all"] - as OIDC scopes are ungrantable in Auth0 customer APIs. They are simply requested
96+
for in the POST request during the token caching process.
9197
"""
9298

9399
def __init__(
94100
self,
95101
endpoint: str,
96102
cfg_store: ClientConfigStore,
103+
scopes: typing.Optional[typing.List[str]] = None,
97104
header_key: typing.Optional[str] = None,
98105
verify: typing.Optional[typing.Union[bool, str]] = None,
99106
session: typing.Optional[requests.Session] = None,
@@ -104,6 +111,7 @@ def __init__(
104111
super().__init__(endpoint, header_key, KeyringStore.retrieve(endpoint), verify=verify)
105112
self._cfg_store = cfg_store
106113
self._auth_client = None
114+
self._scopes = scopes
107115
self._session = session or requests.Session()
108116

109117
def _initialize_auth_client(self):
@@ -120,7 +128,11 @@ def _initialize_auth_client(self):
120128
endpoint=self._endpoint,
121129
redirect_uri=cfg.redirect_uri,
122130
client_id=cfg.client_id,
123-
scopes=cfg.scopes,
131+
# Audience only needed for Auth0 - Taken from client config
132+
audience=cfg.audience,
133+
scopes=self._scopes or cfg.scopes,
134+
# self._scopes refers to flytekit.configuration.PlatformConfig (config.yaml)
135+
# cfg.scopes refers to PublicClientConfig scopes (can be defined in Helm deployments)
124136
auth_endpoint=cfg.authorization_endpoint,
125137
token_endpoint=cfg.token_endpoint,
126138
verify=self._verify,
@@ -254,15 +266,19 @@ def __init__(
254266
cfg_store: ClientConfigStore,
255267
header_key: typing.Optional[str] = None,
256268
audience: typing.Optional[str] = None,
269+
scopes: typing.Optional[typing.List[str]] = None,
257270
http_proxy_url: typing.Optional[str] = None,
258271
verify: typing.Optional[typing.Union[bool, str]] = None,
259272
session: typing.Optional[requests.Session] = None,
260273
):
261-
self._audience = audience
262274
cfg = cfg_store.get_client_config()
275+
self._audience = audience or cfg.audience
263276
self._client_id = cfg.client_id
264277
self._device_auth_endpoint = cfg.device_authorization_endpoint
265-
self._scope = cfg.scopes
278+
# Input param: scopes refers to flytekit.configuration.PlatformConfig (config.yaml)
279+
# cfg.scopes refers to PublicClientConfig scopes (can be defined in Helm deployments)
280+
# Use "scope" from object instantiation if value is not None - otherwise, default to cfg.scopes
281+
self._scopes = scopes or cfg.scopes
266282
self._token_endpoint = cfg.token_endpoint
267283
if self._device_auth_endpoint is None:
268284
raise AuthenticationError(
@@ -282,7 +298,7 @@ def refresh_credentials(self):
282298
self._device_auth_endpoint,
283299
self._client_id,
284300
self._audience,
285-
self._scope,
301+
self._scopes,
286302
self._http_proxy_url,
287303
self._verify,
288304
self._session,
@@ -296,6 +312,8 @@ def refresh_credentials(self):
296312
resp,
297313
self._token_endpoint,
298314
client_id=self._client_id,
315+
audience=self._audience,
316+
scopes=self._scopes,
299317
http_proxy_url=self._http_proxy_url,
300318
verify=self._verify,
301319
)

flytekit/clients/auth/token_client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def get_token(
9999
if device_code:
100100
body["device_code"] = device_code
101101
if scopes is not None:
102-
body["scope"] = ",".join(scopes)
102+
body["scope"] = " ".join(s.strip("' ") for s in scopes).strip("[]'")
103103
if audience:
104104
body["audience"] = audience
105105

@@ -135,7 +135,7 @@ def get_device_code(
135135
Retrieves the device Authentication code that can be done to authenticate the request using a browser on a
136136
separate device
137137
"""
138-
_scope = " ".join(scope) if scope is not None else ""
138+
_scope = " ".join(s.strip("' ") for s in scope).strip("[]'") if scope is not None else ""
139139
payload = {"client_id": client_id, "scope": _scope, "audience": audience}
140140
proxies = {"https": http_proxy_url, "http": http_proxy_url} if http_proxy_url else None
141141
if not session:
@@ -150,6 +150,8 @@ def poll_token_endpoint(
150150
resp: DeviceCodeResponse,
151151
token_endpoint: str,
152152
client_id: str,
153+
audience: typing.Optional[str] = None,
154+
scopes: typing.Optional[str] = None,
153155
http_proxy_url: typing.Optional[str] = None,
154156
verify: typing.Optional[typing.Union[bool, str]] = None,
155157
) -> typing.Tuple[str, int]:
@@ -162,6 +164,8 @@ def poll_token_endpoint(
162164
token_endpoint,
163165
grant_type=GrantType.DEVICE_CODE,
164166
client_id=client_id,
167+
audience=audience,
168+
scopes=scopes,
165169
device_code=resp.device_code,
166170
http_proxy_url=http_proxy_url,
167171
verify=verify,

flytekit/clients/auth_helper.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def get_authenticator(cfg: PlatformConfig, cfg_store: ClientConfigStore) -> Auth
7171
session = get_session(cfg)
7272

7373
if cfg_auth == AuthType.STANDARD or cfg_auth == AuthType.PKCE:
74-
return PKCEAuthenticator(cfg.endpoint, cfg_store, verify=verify, session=session)
74+
return PKCEAuthenticator(cfg.endpoint, cfg_store, scopes=cfg.scopes, verify=verify, session=session)
7575
elif cfg_auth == AuthType.BASIC or cfg_auth == AuthType.CLIENT_CREDENTIALS or cfg_auth == AuthType.CLIENTSECRET:
7676
return ClientCredentialsAuthenticator(
7777
endpoint=cfg.endpoint,
@@ -97,6 +97,7 @@ def get_authenticator(cfg: PlatformConfig, cfg_store: ClientConfigStore) -> Auth
9797
endpoint=cfg.endpoint,
9898
cfg_store=cfg_store,
9999
audience=cfg.audience,
100+
scopes=cfg.scopes,
100101
http_proxy_url=cfg.http_proxy_url,
101102
verify=verify,
102103
session=session,

flytekit/configuration/internal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,16 @@ class Credentials(object):
110110
"""
111111

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

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

0 commit comments

Comments
 (0)