-
Notifications
You must be signed in to change notification settings - Fork 0
[DO NOT MERGE]feat: FGI-1573 add connected account support #57
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
Open
sam-muncke
wants to merge
35
commits into
main
Choose a base branch
from
FGI-1573_connected-account-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
e6dd7cb
Basic connected account connect/complete flow
sam-muncke a27ca1c
Add MRRT behaviour
sam-muncke c1d0167
Move BearerAuth to own file
sam-muncke c62cdc3
Handle redirect_uri properly
sam-muncke e7c65d1
Add some doc comments
sam-muncke f1a52b6
Add tests around MyAccountClient
sam-muncke 8808ed9
Make use of mrrt configurable
sam-muncke 3e27dc5
Fix for code scanning alert no. 3: Unused import
sam-muncke 161e6a7
Merge branch 'FGI-1573_connected-account-support' of github.com:auth0…
sam-muncke c836d2e
Fix linting issues
sam-muncke b8fcc89
Test to ensure mrrt is used for connected accounts
sam-muncke c8cf1cf
add example docs
sam-muncke d6e210d
Allow passing of app state
sam-muncke 08fbc31
Fix comment
sam-muncke 2b0439d
Cleanup transaction data at end of the flow
sam-muncke c09d803
Fix case where MRRT is disabled but we may have multiple token sets w…
sam-muncke 3fecdbb
Fix docs issues from code review
sam-muncke bbbc824
Merge branch 'FGI-1573_connected-account-support' of github.com:auth0…
sam-muncke 4ea8e5f
Code review fixes
sam-muncke 2d070d9
Code review fixes
sam-muncke e91eab1
Update examples/ConnectedAccounts.md
sam-muncke f658b8b
Update examples/ConnectedAccounts.md
sam-muncke 2e27549
Update examples/ConnectedAccounts.md
sam-muncke 70529b9
Code review fixes
sam-muncke 7111e65
Clean up transaction data regardless of success/failure
sam-muncke 61e5d42
Populate/pull audience to store in token set from transaction state o…
sam-muncke 5a6e387
Dont merge the default auth params with the ones provided for connect…
sam-muncke 19977f8
Parsed returned url safely
sam-muncke b5154aa
Remove use_mrrt flag and have mrrt used by default
sam-muncke 2a863dd
Add support for scope parameter on start_connect_account
sam-muncke 4aa2651
Fix docs issues
sam-muncke c7a869e
Rename my_account_client audience_identifier to audience
sam-muncke d295e93
Revert MRRT related changes
sam-muncke 39e0df7
Fix docs and scope/scopes when passing to connected accounts
sam-muncke 48f5b88
Fix test
sam-muncke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| # Connect Accounts for using Token Vault | ||
|
|
||
| The Connect Accounts feature uses the Auth0 My Account API to allow users to link multiple third party accounts to a single Auth0 user profile. In order to use this feature, [My Account API](https://auth0.com/docs/manage-users/my-account-api) must be activated on your Auth0 tenant. | ||
|
|
||
| >[!NOTE] | ||
| >DPoP sender token constraining is not yet supported in this SDK. My Account API can be configured to support it (default behaviour) but must not be configured to require it. | ||
|
|
||
|
|
||
| When using Connected Accounts, Auth0 acquires tokens from upstream Identity Providers (like Google) and stores them in a secure [Token Vault](https://auth0.com/docs/secure/tokens/token-vault). These tokens can then be used to access third-party APIs (like Google Calendar) on behalf of the user. | ||
|
|
||
| The tokens in the Token Vault are then accessible to [Resource Servers](https://auth0.com/docs/get-started/apis) (APIs) configured in Auth0. The application can then issue requests to the API, which can retrieve the tokens from the Token Vault and use them to access the third-party APIs. | ||
|
|
||
| This is particularly useful for applications that require access to different resources on behalf of a user, like AI Agents. | ||
|
|
||
| ## Configure the SDK | ||
|
|
||
| The SDK must be configured with an audience (an API Identifier) - this will be the resource server that uses the tokens from the Token Vault. | ||
|
|
||
| The Auth0 client Application must be configured to use refresh tokens and [MRRT (Multiple Resource Refresh Tokens)](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token) since we will use the refresh token grant to get Access Tokens for the My Account API in addition to the API we are calling. | ||
|
|
||
| ```python | ||
| server_client = ServerClient( | ||
| domain="YOUR_AUTH0_DOMAIN", | ||
| client_id="YOUR_CLIENT_ID", | ||
| client_secret="YOUR_CLIENT_SECRET", | ||
| secret="YOUR_SECRET", | ||
| authorization_params={ | ||
| "redirect_uri":"YOUR_CALLBACK_URL", | ||
| "audience": "YOUR_API_IDENTIFIER" | ||
| } | ||
| ) | ||
| ``` | ||
|
|
||
| ## Login to the application | ||
|
|
||
| Use the login methods to authenticate to the application and get a refresh and access token for the API. | ||
|
|
||
| ```python | ||
| # Login specifying any scopes for the Auth0 API | ||
|
|
||
| authorization_url = await server_client.start_interactive_login( | ||
| { | ||
| "authorization_params": { | ||
| # must include offline_access to obtain a refresh token | ||
| "scope": "openid profile email offline_access" | ||
| } | ||
| }, | ||
| store_options={"request": request, "response": response} | ||
| ) | ||
|
|
||
| # redirect user | ||
|
|
||
| # handle redirect | ||
| result = await server_client.complete_interactive_login( | ||
| callback_url, | ||
| store_options={"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
| ## Connect to a third-party account | ||
|
|
||
| Start the flow using the `start_connect_account` method to redirect the user to the third-party Identity Provider to connect their account. | ||
|
|
||
| The `authorization_params` is used to pass additional parameters required by the third-party IdP | ||
| The `app_state` parameter allows you to pass custom state (for example, a return URL) that is later available when the connect process completes. | ||
|
|
||
| ```python | ||
|
|
||
| connect_url = await self.client.start_connect_account( | ||
| ConnectAccountOptions( | ||
| connection="CONNECTION", # e.g. google-oauth2 | ||
| redirect_uri="YOUR_CALLBACK_URL" | ||
| app_state= { | ||
| "returnUrl":"SOME_URL" | ||
| } | ||
| scopes= [ | ||
| # scopes to passed to the third-party IdP | ||
| "openid", | ||
| "email", | ||
| "profile" | ||
| "offline_access" | ||
| ] | ||
| authorization_params= { | ||
| # additional auth parameters to be sent to the third-party IdP e.g. | ||
| "login_hint": "user123", | ||
| "resource": "some_resource" | ||
| } | ||
| ), | ||
| store_options={"request": request, "response": response} | ||
| ) | ||
| ``` | ||
|
|
||
| Using the url returned, redirect the user to the third-party Identity Provider to complete any required authorization. Once authorized, the user will be redirected back to the provided `redirect_uri` with a `connect_code` and `state` parameter. | ||
|
|
||
| ## Complete the account connection | ||
|
|
||
| Call the `complete_connect_account` method using the full callback url returned from the third-party IdP to complete the connected account flow. This method extracts the connect_code from the URL, completes the connection, and returns the response data (including any `app_state` you passed originally). | ||
|
|
||
| ```python | ||
| complete_response = await self.client.complete_connect_account( | ||
| url= callback_url, | ||
| store_options=store_options | ||
| ) | ||
| ``` | ||
|
|
||
| >[!NOTE] | ||
| >The `callback_url` must include the necessary parameters (`state` and `connect_code`) that Auth0 sends upon successful authentication. | ||
|
|
||
| You can now call the API with your access token and the API can use [Access Token Exchange with Token Vault](https://auth0.com/docs/secure/tokens/token-vault/access-token-exchange-with-token-vault) to get tokens from the Token Vault to access third-party APIs on behalf of the user. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .bearer_auth import BearerAuth | ||
|
|
||
| __all__ = ["BearerAuth"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import httpx | ||
|
|
||
|
|
||
| class BearerAuth(httpx.Auth): | ||
| def __init__(self, token: str): | ||
| self.token = token | ||
|
|
||
| def auth_flow(self, request): | ||
| request.headers['Authorization'] = f"Bearer {self.token}" | ||
| yield request |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from .my_account_client import MyAccountClient | ||
| from .server_client import ServerClient | ||
|
|
||
| __all__ = ["ServerClient"] | ||
| __all__ = ["ServerClient", "MyAccountClient"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
|
|
||
| import httpx | ||
| from auth0_server_python.auth_schemes.bearer_auth import BearerAuth | ||
| from auth0_server_python.auth_types import ( | ||
| CompleteConnectAccountRequest, | ||
| CompleteConnectAccountResponse, | ||
| ConnectAccountRequest, | ||
| ConnectAccountResponse, | ||
| ) | ||
| from auth0_server_python.error import ( | ||
| ApiError, | ||
| MyAccountApiError, | ||
| ) | ||
|
|
||
|
|
||
| class MyAccountClient: | ||
| def __init__(self, domain: str): | ||
| self._domain = domain | ||
|
|
||
| @property | ||
| def audience(self): | ||
| return f"https://{self._domain}/me/" | ||
|
|
||
| async def connect_account( | ||
| self, | ||
| access_token: str, | ||
| request: ConnectAccountRequest | ||
| ) -> ConnectAccountResponse: | ||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
| response = await client.post( | ||
| url=f"{self.audience}v1/connected-accounts/connect", | ||
| json=request.model_dump(exclude_none=True), | ||
| auth=BearerAuth(access_token) | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| if response.status_code != 201: | ||
| error_data = response.json() | ||
| 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() | ||
sam-muncke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return ConnectAccountResponse.model_validate(data) | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, MyAccountApiError): | ||
| raise | ||
| raise ApiError( | ||
| "connect_account_error", | ||
| f"Connected Accounts connect request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
|
|
||
| async def complete_connect_account( | ||
| self, | ||
| access_token: str, | ||
| request: CompleteConnectAccountRequest | ||
| ) -> CompleteConnectAccountResponse: | ||
| try: | ||
| async with httpx.AsyncClient() as client: | ||
| response = await client.post( | ||
| url=f"{self.audience}v1/connected-accounts/complete", | ||
| json=request.model_dump(exclude_none=True), | ||
| auth=BearerAuth(access_token) | ||
| ) | ||
|
|
||
| if response.status_code != 201: | ||
| error_data = response.json() | ||
| 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 CompleteConnectAccountResponse.model_validate(data) | ||
|
|
||
| except Exception as e: | ||
| if isinstance(e, MyAccountApiError): | ||
| raise | ||
| raise ApiError( | ||
| "connect_account_error", | ||
| f"Connected Accounts complete request failed: {str(e) or 'Unknown error'}", | ||
| e | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.