Skip to content

Commit 2314679

Browse files
authored
Merge pull request #46 from auth0-lab/feature/auth0-ai-python-sdk-support-for-fcat
Feature/auth0 ai python sdk support for fcat
2 parents 1ed8f36 + c12598d commit 2314679

3 files changed

Lines changed: 73 additions & 26 deletions

File tree

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,51 @@
11
import copy
22
from abc import ABC
3-
from auth0_ai.authorizers.federated_connection_authorizer import FederatedConnectionAuthorizerBase, FederatedConnectionAuthorizerParams
3+
from auth0_ai.authorizers.federated_connection_authorizer import FederatedConnectionAuthorizerBase, \
4+
FederatedConnectionAuthorizerParams
45
from auth0_ai.authorizers.types import Auth0ClientParams
56
from auth0_ai.interrupts.federated_connection_interrupt import FederatedConnectionInterrupt
67
from auth0_ai_langchain.utils.interrupt import to_graph_interrupt
78
from auth0_ai_langchain.utils.tool_wrapper import tool_wrapper
89
from langchain_core.tools import BaseTool
910
from langchain_core.runnables import ensure_config
1011

12+
1113
async def default_get_refresh_token(*_, **__) -> str | None:
1214
return ensure_config().get("configurable", {}).get("_credentials", {}).get("refresh_token")
1315

16+
async def default_get_subject_access_token(*_, **__) -> str | None:
17+
"""
18+
Returns the Auth0 *user* access token (from the LC graph config) to be used
19+
as the subject token in Token Vault exchange.
20+
"""
21+
return ensure_config().get("configurable", {}).get("_credentials", {}).get("access_token")
22+
23+
1424
class FederatedConnectionAuthorizer(FederatedConnectionAuthorizerBase, ABC):
1525
def __init__(
16-
self,
26+
self,
1727
params: FederatedConnectionAuthorizerParams,
1828
auth0: Auth0ClientParams = None,
1929
):
20-
if params.refresh_token.value is None:
30+
missing_refresh = params.refresh_token.value is None
31+
missing_subject_at = getattr(params, "subject_access_token",
32+
None) is None or params.subject_access_token.value is None
33+
34+
if missing_refresh and missing_subject_at:
2135
params = copy.copy(params)
22-
params.refresh_token.value = default_get_refresh_token
36+
params.subject_access_token.value = default_get_subject_access_token
37+
elif not missing_refresh and callable(default_get_refresh_token):
38+
if params.refresh_token.value is None:
39+
params = copy.copy(params)
40+
params.refresh_token.value = default_get_refresh_token
2341

2442
super().__init__(params, auth0)
25-
43+
2644
def _handle_authorization_interrupts(self, err: FederatedConnectionInterrupt) -> None:
2745
raise to_graph_interrupt(err)
28-
46+
2947
def authorizer(self):
3048
def wrap_tool(tool: BaseTool) -> BaseTool:
3149
return tool_wrapper(tool, self.protect)
32-
50+
3351
return wrap_tool
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1+
from __future__ import annotations
2+
13
from abc import ABC
2-
from auth0_ai.authorizers.federated_connection_authorizer import FederatedConnectionAuthorizerBase, FederatedConnectionAuthorizerParams
4+
5+
from auth0_ai.authorizers.federated_connection_authorizer import (
6+
FederatedConnectionAuthorizerBase,
7+
FederatedConnectionAuthorizerParams,
8+
)
39
from auth0_ai.authorizers.types import Auth0ClientParams
410
from auth0_ai_llamaindex.utils.tool_wrapper import tool_wrapper
511
from llama_index.core.tools import FunctionTool
612

13+
714
class FederatedConnectionAuthorizer(FederatedConnectionAuthorizerBase, ABC):
815
def __init__(
9-
self,
16+
self,
1017
params: FederatedConnectionAuthorizerParams,
1118
auth0: Auth0ClientParams = None,
1219
):
13-
if params.refresh_token.value is None:
14-
raise ValueError('params.refresh_token must be provided.')
15-
1620
super().__init__(params, auth0)
17-
21+
1822
def authorizer(self):
1923
def wrap_tool(tool: FunctionTool) -> FunctionTool:
2024
return tool_wrapper(tool, self.protect)
21-
25+
2226
return wrap_tool

packages/auth0-ai/auth0_ai/authorizers/federated_connection_authorizer.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def __init__(
6565
Callable[ToolInput, Union[str | None, Awaitable[str | None]]],
6666
str | None,
6767
]] = None,
68+
subject_access_token: Optional[Union[
69+
AuthorizerToolParameter[ToolInput, str | None],
70+
Callable[ToolInput, Union[str | None, Awaitable[str | None]]],
71+
str | None,
72+
]] = None,
6873
access_token: Optional[Union[
6974
AuthorizerToolParameter[ToolInput, TokenResponse | None],
7075
Callable[ToolInput, Union[TokenResponse | None, Awaitable[TokenResponse | None]]],
@@ -79,10 +84,14 @@ def __init__(
7984
Args:
8085
scopes: The scopes required in the access token of the federated connection provider.
8186
connection: The connection name of the federated connection provider.
82-
refresh_token: Optional. The Auth0 refresh token to exchange for an federated connection access token. Can be:
87+
refresh_token: Optional. The Auth0 refresh token to exchange for a federated connection access token. Can be:
8388
- A string or None
8489
- A callable that receives the tool input and returns the user refresh token (sync or async)
85-
access_token: Optional. The federated connection access token if available in the tool context. Can be:
90+
subject_access_token: Optional. The Auth0 *access token* (for the logged-in user) to exchange
91+
for a federated connection access token via Token Vault. Can be:
92+
- A string or None
93+
- A callable that receives the tool input and returns the user access token (sync or async)
94+
access_token: Optional. The *federated connection* access token if available in the tool context. Can be:
8695
- A `TokenResponse`
8796
- A callable that receives the tool input and returns a `TokenResponse` (sync or async)
8897
store: Optional. An store used to temporarly store the authorization response data while the user is completing the authorization in another device (default: InMemoryStore).
@@ -101,6 +110,7 @@ def wrap(val, result_type):
101110
self.scopes = scopes
102111
self.connection = connection
103112
self.refresh_token = wrap(refresh_token, str | None)
113+
self.subject_access_token = wrap(subject_access_token, str | None)
104114
self.access_token = wrap(access_token, TokenResponse | None)
105115
self.store = store
106116
self.credentials_context = credentials_context
@@ -137,20 +147,23 @@ def __init__(
137147
"get_ttl": lambda credential: credential["expires_in"] * 1000 if "expires_in" in credential else None
138148
})
139149

140-
# Ensure either refreshToken or accessToken is provided
141-
if params.refresh_token.value is None and params.access_token.value is None:
142-
raise ValueError("Either refresh_token or access_token must be provided to initialize the Authorizer.")
143-
144-
if params.refresh_token.value is not None and params.access_token.value is not None:
145-
raise ValueError("Only one of refresh_token or access_token can be provided to initialize the Authorizer.")
150+
provided = sum([
151+
1 if params.refresh_token.value is not None else 0,
152+
1 if params.subject_access_token.value is not None else 0,
153+
1 if params.access_token.value is not None else 0,
154+
])
155+
if provided != 1:
156+
raise ValueError(
157+
"Exactly one of refresh_token, subject_access_token, or access_token must be provided to initialize the Authorizer."
158+
)
146159

147160
def _handle_authorization_interrupts(self, err: Auth0Interrupt) -> None:
148161
raise err
149162

150163
def _get_instance_id(self) -> str:
151164
props = {
152165
"auth0": omit(self.auth0, ["client_secret", "client_assertion_signing_key"]),
153-
"params": omit(self.params, ["store", "refresh_token", "access_token"])
166+
"params": omit(self.params, ["store", "refresh_token", "subject_access_token", "access_token"])
154167
}
155168
sh = json.dumps(props, sort_keys=True, separators=(",", ":"))
156169
return hashlib.md5(sh.encode("utf-8")).hexdigest()
@@ -184,13 +197,22 @@ async def get_access_token_impl(self, *args: ToolInput.args, **kwargs: ToolInput
184197
store = _get_local_storage()
185198

186199
connection = store["connection"]
187-
subject_token = await self.get_refresh_token(*args, **kwargs)
200+
subject_token_type: str | None = None
201+
subject_token: str | None = None
202+
203+
if self.params.refresh_token.value is not None:
204+
subject_token = await self.get_refresh_token(*args, **kwargs)
205+
subject_token_type = "urn:ietf:params:oauth:token-type:refresh_token"
206+
else:
207+
subject_token = await self.get_subject_access_token(*args, **kwargs)
208+
subject_token_type = "urn:ietf:params:oauth:token-type:access_token"
209+
188210
if not subject_token:
189211
return None
190212

191213
try:
192214
response = self.get_token.access_token_for_connection(
193-
subject_token_type="urn:ietf:params:oauth:token-type:refresh_token",
215+
subject_token_type=subject_token_type,
194216
subject_token=subject_token,
195217
requested_token_type="http://auth0.com/oauth/token-type/federated-connection-access-token",
196218
connection=connection,
@@ -208,7 +230,7 @@ async def get_access_token_impl(self, *args: ToolInput.args, **kwargs: ToolInput
208230
raise FederatedConnectionError(err.message) if 400 <= err.status_code <= 499 else err
209231

210232
async def get_access_token(self, *args: ToolInput.args, **kwargs: ToolInput.kwargs) -> TokenResponse | None:
211-
if callable(self.params.refresh_token.value) or asyncio.iscoroutinefunction(self.params.refresh_token.value):
233+
if self.params.refresh_token.value is not None or self.params.subject_access_token.value is not None:
212234
token_response = await self.get_access_token_impl(*args, **kwargs)
213235
else:
214236
token_response = await self.params.access_token.resolve(*args, **kwargs)
@@ -219,6 +241,9 @@ async def get_access_token(self, *args: ToolInput.args, **kwargs: ToolInput.kwar
219241
async def get_refresh_token(self, *args: ToolInput.args, **kwargs: ToolInput.kwargs):
220242
return await self.params.refresh_token.resolve(*args, **kwargs)
221243

244+
async def get_subject_access_token(self, *args: ToolInput.args, **kwargs: ToolInput.kwargs):
245+
return await self.params.subject_access_token.resolve(*args, **kwargs)
246+
222247
def protect(
223248
self,
224249
get_context: ContextGetter[ToolInput],

0 commit comments

Comments
 (0)