-
Notifications
You must be signed in to change notification settings - Fork 0
feature:FGI-1575 Add ability to manage a users connected accounts #60
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
base: main
Are you sure you want to change the base?
Changes from all commits
a2a7d5c
ea85e80
100ce13
a8db5d5
c764468
4f2e805
9717a26
2957000
8fb55b1
6f46801
e672073
4f77836
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,79 @@ You can now call the API with your access token and the API can use [Access Toke | |
| ```python | ||
| access_token_for_google = await server_client.get_access_token_for_connection( | ||
| { "connection": "google-oauth2" }, | ||
| store_options=store_options | ||
| store_options={"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
| ## Managing Connected Accounts | ||
|
|
||
| `ServerClient` exposes three methods for managing a user's connected accounts | ||
|
|
||
| ### List Available Connections | ||
|
|
||
| This method provides a list of connections that have been enabled for use with Connected Accounts for Token Vault that the user may use to connect accounts. | ||
|
|
||
| This method requires the My Account `read:me:connected_accounts` scope to be enabled for your application and configured for MRRT. | ||
|
|
||
| This method supports paging via optional the use of `take` parameter. Without this parameters, a default page size of 10 is used. Subsequent pages can be retrieved by also passing the `from_param` parameter with the token returned in the `next` property of the response | ||
|
|
||
| ```python | ||
| available_connections = await client.list_connected_account_connections( | ||
| take= 5, # optional | ||
| from_param= "NEXT_VALUE_FROM_PREVIOUS_RESPONSE", # optional | ||
| store_options= {"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
| ### List Connected Accounts | ||
|
|
||
| This method provides a list of accounts that you have already connected. | ||
|
|
||
| This method requires the My Account `read:me:connected_accounts` scope to be enabled for your application and configured for MRRT. | ||
|
|
||
| An optional `connection` parameter can be used to filter the connected accounts for a specific connection, otherwise all connected accounts will be returns | ||
|
|
||
| This method supports paging via optional the use of `take` parameter. Without this parameters, a default page size of 10 is used. Subsequent pages can be retrieved by also passing the `from_param` parameter with the token returned in the `next` property of the response | ||
|
|
||
| ```python | ||
| connected_accounts = await client.list_connected_accounts( | ||
| connection= "google-oauth2", # optional | ||
| take= 5, # optional | ||
| from_param= "NEXT_VALUE_FROM_PREVIOUS_RESPONSE", # optional | ||
| store_options= {"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
| ### Delete Connected Account | ||
|
|
||
| This method removes a connected account for the user. | ||
|
|
||
| This method requires the My Account `delete:me:connected_accounts` scope to be enabled for your application and configured for MRRT. | ||
|
|
||
| This method takes a `connected_account_id` parameter which can be obtained from `list_connected_accounts`. | ||
|
|
||
| ```python | ||
| connected_accounts = await client.delete_connected_account( | ||
| connected_account_id= "CONNECTED_ACCOUNT_ID", | ||
| store_options= {"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add an Error handling Example Section here as I can see the whole document only talks about the happy path. |
||
| ## A note about scopes | ||
|
|
||
| If multiple pieces of Connected Account functionality are intended to be used, it is recommended that you set the default `scope` for the My Account audience when creating you `ServerClient`. This will avoid multiple token requests as without it a new token will be requested for each scope used. This can be done by configuring the `scope` dictionary in the `authorization_params` when configuring the SDK. Each value in the dictionary corresponds to an `audience` and sets the `default` requested scopes for that audience. | ||
|
|
||
| ```python | ||
| server_client = ServerClient( | ||
| domain="YOUR_AUTH0_DOMAIN", | ||
| client_id="YOUR_CLIENT_ID", | ||
| client_secret="YOUR_CLIENT_SECRET", | ||
| secret="YOUR_SECRET", | ||
| authorization_params={ | ||
| "scope" { | ||
| "https://YOUR_AUTH0_DOMAIN/me/": "create:me:connected_accounts read:me:connected_accounts delete:me:connected_accounts", # scopes required for the My Account audience | ||
| # default scopes for custom API audiences can also be defined | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| ) | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,20 @@ | ||
|
|
||
| from typing import Optional | ||
|
|
||
| import httpx | ||
| from auth0_server_python.auth_schemes.bearer_auth import BearerAuth | ||
| from auth0_server_python.auth_types import ( | ||
| CompleteConnectAccountRequest, | ||
| CompleteConnectAccountResponse, | ||
| ConnectAccountRequest, | ||
| ConnectAccountResponse, | ||
| ListConnectedAccountConnectionsResponse, | ||
| ListConnectedAccountsResponse, | ||
| ) | ||
| from auth0_server_python.error import ( | ||
| ApiError, | ||
| InvalidArgumentError, | ||
| MissingRequiredArgumentError, | ||
| MyAccountApiError, | ||
| ) | ||
|
|
||
|
|
@@ -92,3 +98,143 @@ async def complete_connect_account( | |
| f"Connected Accounts complete request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
|
|
||
| async def list_connected_accounts( | ||
| self, | ||
| access_token: str, | ||
| connection: Optional[str] = None, | ||
| from_param: Optional[str] = None, | ||
| take: Optional[int] = None | ||
| ) -> ListConnectedAccountsResponse: | ||
| if access_token is None: | ||
| raise MissingRequiredArgumentError("access_token") | ||
|
|
||
| if take is not None and (not isinstance(take, int) or take < 1): | ||
| raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.") | ||
|
|
||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| params = {} | ||
| if connection: | ||
| params["connection"] = connection | ||
| if from_param: | ||
| params["from"] = from_param | ||
| if take: | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| params["take"] = take | ||
|
|
||
| response = await client.get( | ||
| url=f"{self.audience}v1/connected-accounts/accounts", | ||
| params=params, | ||
| auth=BearerAuth(access_token) | ||
| ) | ||
|
|
||
| if response.status_code != 200: | ||
| error_data = response.json() | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| raise MyAccountApiError( | ||
| title=error_data.get("title", None), | ||
| type=error_data.get("type", None), | ||
| detail=error_data.get("detail", None), | ||
| status=error_data.get("status", None), | ||
| validation_errors=error_data.get("validation_errors", None) | ||
| ) | ||
|
|
||
| data = response.json() | ||
|
|
||
| return ListConnectedAccountsResponse.model_validate(data) | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, MyAccountApiError): | ||
| raise | ||
| raise ApiError( | ||
| "connect_account_error", | ||
| f"Connected Accounts list request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
|
|
||
|
|
||
| async def delete_connected_account( | ||
| self, | ||
| access_token: str, | ||
| connected_account_id: str | ||
| ) -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't see any docstrings for any of the functions in these files. Is there a specific reason not adding them? |
||
|
|
||
| if access_token is None: | ||
| raise MissingRequiredArgumentError("access_token") | ||
|
|
||
| if connected_account_id is None: | ||
| raise MissingRequiredArgumentError("connected_account_id") | ||
|
|
||
| try: | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| async with httpx.AsyncClient() as client: | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| response = await client.delete( | ||
| url=f"{self.audience}v1/connected-accounts/accounts/{connected_account_id}", | ||
| auth=BearerAuth(access_token) | ||
| ) | ||
|
|
||
| if response.status_code != 204: | ||
| error_data = response.json() | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| raise MyAccountApiError( | ||
| title=error_data.get("title", None), | ||
| type=error_data.get("type", None), | ||
| detail=error_data.get("detail", None), | ||
| status=error_data.get("status", None), | ||
| validation_errors=error_data.get("validation_errors", None) | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, MyAccountApiError): | ||
| raise | ||
| raise ApiError( | ||
| "connect_account_error", | ||
| f"Connected Accounts delete request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
|
|
||
| async def list_connected_account_connections( | ||
| self, | ||
| access_token: str, | ||
| from_param: Optional[str] = None, | ||
| take: Optional[int] = None | ||
| ) -> ListConnectedAccountConnectionsResponse: | ||
| if access_token is None: | ||
| raise MissingRequiredArgumentError("access_token") | ||
|
|
||
| if take is not None and (not isinstance(take, int) or take < 1): | ||
| raise InvalidArgumentError("take", "The 'take' parameter must be a positive integer.") | ||
|
|
||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
| params = {} | ||
| if from_param: | ||
| params["from"] = from_param | ||
| if take: | ||
| params["take"] = take | ||
|
|
||
| response = await client.get( | ||
| url=f"{self.audience}v1/connected-accounts/connections", | ||
| params=params, | ||
| auth=BearerAuth(access_token) | ||
| ) | ||
|
|
||
| if response.status_code != 200: | ||
| error_data = response.json() | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| raise MyAccountApiError( | ||
| title=error_data.get("title", None), | ||
| type=error_data.get("type", None), | ||
| detail=error_data.get("detail", None), | ||
| status=error_data.get("status", None), | ||
| validation_errors=error_data.get("validation_errors", None) | ||
| ) | ||
|
|
||
| data = response.json() | ||
|
|
||
| return ListConnectedAccountConnectionsResponse.model_validate(data) | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, MyAccountApiError): | ||
| raise | ||
| raise ApiError( | ||
| "connect_account_error", | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| f"Connected Accounts list connections request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -252,3 +252,24 @@ class CompleteConnectAccountResponse(BaseModel): | |
| created_at: str | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would request you to restructure the current class structure related to |
||
| expires_at: Optional[str] = None | ||
| app_state: Optional[Any] = None | ||
|
|
||
| class ConnectedAccount(BaseModel): | ||
| id: str | ||
| connection: str | ||
| access_type: str | ||
| scopes: list[str] | ||
| created_at: str | ||
| expires_at: Optional[str] = None | ||
|
|
||
| class ListConnectedAccountsResponse(BaseModel): | ||
| accounts: list[ConnectedAccount] | ||
| next: Optional[str] = None | ||
|
|
||
| class ConnectedAccountConnection(BaseModel): | ||
| name: str | ||
| strategy: str | ||
| scopes: Optional[list[str]] = None | ||
|
|
||
| class ListConnectedAccountConnectionsResponse(BaseModel): | ||
| connections: list[ConnectedAccountConnection] | ||
| next: Optional[str] = None | ||
Uh oh!
There was an error while loading. Please reload this page.