From 2b7603ddca1ab4b5b1fafdc58acef596ba613e05 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Thu, 6 Nov 2025 15:55:34 +0000 Subject: [PATCH 01/17] Basic support for connected accounts/mrrt --- src/auth0_fastapi/auth/auth_client.py | 33 ++++++++++++++++++++++++++- src/auth0_fastapi/config.py | 2 ++ src/auth0_fastapi/server/routes.py | 32 +++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index 33c2eea..7d88308 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -1,7 +1,7 @@ # Imported from auth0-server-python from auth0_server_python.auth_server.server_client import ServerClient -from auth0_server_python.auth_types import LogoutOptions, StartInteractiveLoginOptions +from auth0_server_python.auth_types import LogoutOptions, StartInteractiveLoginOptions, ConnectAccountOptions from fastapi import HTTPException, Request, Response, status from auth0_fastapi.config import Auth0Config @@ -45,6 +45,7 @@ def __init__( transaction_store=transaction_store, state_store=state_store, pushed_authorization_requests=config.pushed_authorization_requests, + use_mrrt=config.use_mrrt, authorization_params={ "audience": config.audience, "redirect_uri": redirect_uri, @@ -82,6 +83,36 @@ async def complete_login( """ return await self.client.complete_interactive_login(callback_url, store_options=store_options) + async def start_connect_account( + self, + connection: str, + authorization_params: dict = None, + store_options: dict = None, + ) -> str: + """ + Initiates the connected account process. + Optionally, an app_state dictionary can be passed to persist additional state. + Returns the authorization URL to redirect the user. + """ + options = ConnectAccountOptions( + connection=connection, + authorization_params= authorization_params + ) + return await self.client.start_connect_account(options=options, store_options=store_options) + + async def complete_connect_account( + self, + connect_code: str, + state: str, + store_options: dict = None, + ) -> str: + """ + Initiates the interactive login process. + Optionally, an app_state dictionary can be passed to persist additional state. + Returns the authorization URL to redirect the user. + """ + return await self.client.complete_connect_account(connect_code=connect_code, state=state, store_options=store_options) + async def logout( self, return_to: str = None, diff --git a/src/auth0_fastapi/config.py b/src/auth0_fastapi/config.py index 9678b00..c227db5 100644 --- a/src/auth0_fastapi/config.py +++ b/src/auth0_fastapi/config.py @@ -15,9 +15,11 @@ class Auth0Config(BaseModel): audience: Optional[str] = Field(None, description="Target audience for tokens (if applicable)") authorization_params: Optional[dict[str, Any]] = Field(None, description="Additional parameters to include in the authorization request") pushed_authorization_requests: bool = Field(False, description="Whether to use pushed authorization requests") + use_mrrt: bool = Field(False, description="Whether to use Multi-Resource Refresh Tokens (MRRT)") # Route-mounting flags with desired defaults mount_routes: bool = Field(True, description="Controls /auth/* routes: login, logout, callback, backchannel-logout") mount_connect_routes: bool = Field(False, description="Controls /auth/connect routes (account-linking)") + mount_connected_account_routes: bool = Field(False, description="Controls /auth/connect-account routes (for connected accounts)") #Cookie Settings cookie_name: str = Field("_a0_session", description="Name of the cookie storing session data") session_expiration: int = Field(259200, description="Session expiration time in seconds (default: 3 days)") diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 4d01d2d..6c87f4f 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -58,10 +58,20 @@ async def callback( ): """ Endpoint to handle the callback after Auth0 authentication. - Processes the callback URL and completes the login flow. + Processes the callback URL and completes the login or connected account flow. Redirects the user to a post-login URL based on appState or a default. """ + connect_code = request.query_params.get("connect_code") + if connect_code and config.mount_connected_account_routes: + state = request.query_params.get("state") + return await auth_client.complete_connect_account( + connect_code=connect_code, + state=state, + store_options={"request": request, "response": response}, + ) + full_callback_url = str(request.url) + try: session_data = await auth_client.complete_login( full_callback_url, @@ -123,7 +133,27 @@ async def backchannel_logout( raise HTTPException(status_code=400, detail=str(e)) return Response(status_code=204) + if config.mount_connected_account_routes: + @router.get("/auth/connect-account") + async def connect_account( + request: Request, + response: Response, + connection: str = Query(), + auth_client: AuthClient = Depends(get_auth_client), + ): + """ + Endpoint to initiate the connect account flow for linking a third-party account to the user's profile. + Redirects the user to the Auth0 connect account URL. + """ + authorization_params = {k: v for k, v in request.query_params.items() if k not in [ + "connection"]} + connect_account_url = await auth_client.start_connect_account( + connection=connection, + authorization_params=authorization_params, + store_options={"request": request, "response": response}, + ) + return RedirectResponse(url=connect_account_url, headers=response.headers) if config.mount_connect_routes: From f77aa7e64938a9e6eed0c0a55c731f96f96ab197 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Thu, 6 Nov 2025 16:09:44 +0000 Subject: [PATCH 02/17] Fix linting issues --- src/auth0_fastapi/auth/auth_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index 7d88308..d4d4824 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -1,7 +1,7 @@ # Imported from auth0-server-python from auth0_server_python.auth_server.server_client import ServerClient -from auth0_server_python.auth_types import LogoutOptions, StartInteractiveLoginOptions, ConnectAccountOptions +from auth0_server_python.auth_types import ConnectAccountOptions, LogoutOptions, StartInteractiveLoginOptions from fastapi import HTTPException, Request, Response, status from auth0_fastapi.config import Auth0Config @@ -111,7 +111,8 @@ async def complete_connect_account( Optionally, an app_state dictionary can be passed to persist additional state. Returns the authorization URL to redirect the user. """ - return await self.client.complete_connect_account(connect_code=connect_code, state=state, store_options=store_options) + return await self.client.complete_connect_account( + connect_code=connect_code, state=state, store_options=store_options) async def logout( self, From e4e8d7425c01f2f82d229da12e4a4b9860b3186e Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Thu, 6 Nov 2025 21:14:25 +0000 Subject: [PATCH 03/17] Pass returnUrl in app_state --- src/auth0_fastapi/auth/auth_client.py | 8 +++---- src/auth0_fastapi/server/routes.py | 32 ++++++++++++++------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index d4d4824..f2e0220 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -86,6 +86,7 @@ async def complete_login( async def start_connect_account( self, connection: str, + app_state: dict = None, authorization_params: dict = None, store_options: dict = None, ) -> str: @@ -96,14 +97,14 @@ async def start_connect_account( """ options = ConnectAccountOptions( connection=connection, + app_state=app_state, authorization_params= authorization_params ) return await self.client.start_connect_account(options=options, store_options=store_options) async def complete_connect_account( self, - connect_code: str, - state: str, + url: str, store_options: dict = None, ) -> str: """ @@ -111,8 +112,7 @@ async def complete_connect_account( Optionally, an app_state dictionary can be passed to persist additional state. Returns the authorization URL to redirect the user. """ - return await self.client.complete_connect_account( - connect_code=connect_code, state=state, store_options=store_options) + return await self.client.complete_connect_account(url, store_options=store_options) async def logout( self, diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 6c87f4f..6fa55a2 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -61,27 +61,26 @@ async def callback( Processes the callback URL and completes the login or connected account flow. Redirects the user to a post-login URL based on appState or a default. """ - connect_code = request.query_params.get("connect_code") - if connect_code and config.mount_connected_account_routes: - state = request.query_params.get("state") - return await auth_client.complete_connect_account( - connect_code=connect_code, - state=state, - store_options={"request": request, "response": response}, - ) - full_callback_url = str(request.url) try: - session_data = await auth_client.complete_login( - full_callback_url, - store_options={"request": request, "response": response}, - ) + if "connect_code" in request.query_params.keys() and config.mount_connected_account_routes: + connect_complete_response = await auth_client.complete_connect_account( + full_callback_url, store_options={"request": request, "response": response}) + + app_state = connect_complete_response.app_state or {} + else: + session_data = await auth_client.complete_login( + full_callback_url,store_options={"request": request, "response": response}) + + # Extract the returnTo URL from the appState if available. + app_state = session_data.get("app_state", {}) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) + # Extract the returnTo URL from the appState if available. - return_to = session_data.get("app_state", {}).get("returnTo") + return_to = app_state.get("returnTo") # Assuming config is stored on app.state default_redirect = auth_client.config.app_base_url @@ -146,9 +145,12 @@ async def connect_account( Redirects the user to the Auth0 connect account URL. """ authorization_params = {k: v for k, v in request.query_params.items() if k not in [ - "connection"]} + "connection", "returnTo"]} + + return_to = request.query_params.get("returnTo") connect_account_url = await auth_client.start_connect_account( connection=connection, + app_state={"returnTo": return_to} if return_to else None, authorization_params=authorization_params, store_options={"request": request, "response": response}, ) From e24ee1742e5278573802d13e794708fea9139d88 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Thu, 6 Nov 2025 21:15:06 +0000 Subject: [PATCH 04/17] Fix lint issues --- src/auth0_fastapi/server/routes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 6fa55a2..2cd7632 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -67,12 +67,12 @@ async def callback( if "connect_code" in request.query_params.keys() and config.mount_connected_account_routes: connect_complete_response = await auth_client.complete_connect_account( full_callback_url, store_options={"request": request, "response": response}) - + app_state = connect_complete_response.app_state or {} else: session_data = await auth_client.complete_login( full_callback_url,store_options={"request": request, "response": response}) - + # Extract the returnTo URL from the appState if available. app_state = session_data.get("app_state", {}) except Exception as e: @@ -146,7 +146,7 @@ async def connect_account( """ authorization_params = {k: v for k, v in request.query_params.items() if k not in [ "connection", "returnTo"]} - + return_to = request.query_params.get("returnTo") connect_account_url = await auth_client.start_connect_account( connection=connection, From aaddd3124c8ba4f2d35250d3632fa00b45f3b52c Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Fri, 7 Nov 2025 10:36:29 +0000 Subject: [PATCH 05/17] Add some authclient tests --- src/auth0_fastapi/server/routes.py | 4 +- src/auth0_fastapi/test/test_auth_client.py | 46 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 2cd7632..144006f 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -144,8 +144,8 @@ async def connect_account( Endpoint to initiate the connect account flow for linking a third-party account to the user's profile. Redirects the user to the Auth0 connect account URL. """ - authorization_params = {k: v for k, v in request.query_params.items() if k not in [ - "connection", "returnTo"]} + authorization_params = { + k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo"]} return_to = request.query_params.get("returnTo") connect_account_url = await auth_client.start_connect_account( diff --git a/src/auth0_fastapi/test/test_auth_client.py b/src/auth0_fastapi/test/test_auth_client.py index 4cb3289..ba1f5f9 100644 --- a/src/auth0_fastapi/test/test_auth_client.py +++ b/src/auth0_fastapi/test/test_auth_client.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from auth0_server_python.auth_types import CompleteConnectAccountResponse, ConnectAccountOptions from fastapi import HTTPException, Request, Response from auth0_fastapi.auth.auth_client import AuthClient @@ -392,3 +393,48 @@ async def test_store_options_validation(self, auth_client): await auth_client.start_login(store_options=valid_options) mock_start.assert_called() + + +class TestConnectedAccountFlow: + """Test connected account functionality.""" + + @pytest.mark.asyncio + async def test_start_connect_account(self, auth_client): + """Test initiating user account linking.""" + mock_connect_url = "https://test.auth0.com/connected-accounts/connect?ticket" + + with patch.object(auth_client.client, 'start_connect_account', new_callable=AsyncMock) as mock_start_connect: + mock_start_connect.return_value = mock_connect_url + + result = await auth_client.start_connect_account( + connection="google-oauth2", + app_state={"returnTo": "/profile"}, + authorization_params={"prompt": "consent"}, + ) + + assert result == mock_connect_url + mock_start_connect.assert_called_once_with( + options=ConnectAccountOptions( + connection="google-oauth2", + app_state={"returnTo": "/profile"}, + authorization_params={"prompt": "consent"}, + ), store_options=None) + + @pytest.mark.asyncio + async def test_complete_connect_account(self, auth_client): + """Test initiating user account linking.""" + mock_callback_url = "https://test.auth0.com/connected-accounts/connect?ticket" + mock_result = CompleteConnectAccountResponse( + id="id_12345", + connection="google-oauth2", + access_type="offline", + scopes=["read:foo"], + created_at="1970-01-01T00:00:00Z" + ) + with patch.object(auth_client.client, 'complete_connect_account', new_callable=AsyncMock) as mock_complete: + mock_complete.return_value = mock_result + + result = await auth_client.complete_connect_account(mock_callback_url) + + assert result == mock_result + mock_complete.assert_called_once_with(mock_callback_url, store_options=None) From 17c21bec677b5bf8964d6605d2ebcaaff1885a40 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Fri, 7 Nov 2025 15:47:18 +0000 Subject: [PATCH 06/17] Code review fixes --- src/auth0_fastapi/auth/auth_client.py | 18 +++++++++++------- src/auth0_fastapi/server/routes.py | 7 ++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index f2e0220..44bffff 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -1,7 +1,12 @@ # Imported from auth0-server-python from auth0_server_python.auth_server.server_client import ServerClient -from auth0_server_python.auth_types import ConnectAccountOptions, LogoutOptions, StartInteractiveLoginOptions +from auth0_server_python.auth_types import ( + CompleteConnectAccountResponse, + ConnectAccountOptions, + LogoutOptions, + StartInteractiveLoginOptions, +) from fastapi import HTTPException, Request, Response, status from auth0_fastapi.config import Auth0Config @@ -93,12 +98,12 @@ async def start_connect_account( """ Initiates the connected account process. Optionally, an app_state dictionary can be passed to persist additional state. - Returns the authorization URL to redirect the user. + Returns the connect URL to redirect the user. """ options = ConnectAccountOptions( connection=connection, app_state=app_state, - authorization_params= authorization_params + authorization_params=authorization_params ) return await self.client.start_connect_account(options=options, store_options=store_options) @@ -106,11 +111,10 @@ async def complete_connect_account( self, url: str, store_options: dict = None, - ) -> str: + ) -> CompleteConnectAccountResponse: """ - Initiates the interactive login process. - Optionally, an app_state dictionary can be passed to persist additional state. - Returns the authorization URL to redirect the user. + Completes the connect account process using the callback URL. + Returns the completed connect account response. """ return await self.client.complete_connect_account(url, store_options=store_options) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 144006f..5bfe6e9 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -64,14 +64,14 @@ async def callback( full_callback_url = str(request.url) try: - if "connect_code" in request.query_params.keys() and config.mount_connected_account_routes: + if "connect_code" in request.query_params and config.mount_connected_account_routes: connect_complete_response = await auth_client.complete_connect_account( full_callback_url, store_options={"request": request, "response": response}) app_state = connect_complete_response.app_state or {} else: session_data = await auth_client.complete_login( - full_callback_url,store_options={"request": request, "response": response}) + full_callback_url, store_options={"request": request, "response": response}) # Extract the returnTo URL from the appState if available. app_state = session_data.get("app_state", {}) @@ -85,7 +85,8 @@ async def callback( # Assuming config is stored on app.state default_redirect = auth_client.config.app_base_url - return RedirectResponse(url=return_to or default_redirect, headers=response.headers) + safe_redirect = to_safe_redirect(return_to or default_redirect, auth_client.config.app_base_url) + return RedirectResponse(url=safe_redirect, headers=response.headers) @router.get("/auth/logout") async def logout( From ac1ed9c7826b1b4664b7ea97d6f336032a4a561d Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Mon, 10 Nov 2025 18:33:08 +0000 Subject: [PATCH 07/17] Remove use_mrrt config value --- src/auth0_fastapi/auth/auth_client.py | 1 - src/auth0_fastapi/config.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index 44bffff..6345b79 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -50,7 +50,6 @@ def __init__( transaction_store=transaction_store, state_store=state_store, pushed_authorization_requests=config.pushed_authorization_requests, - use_mrrt=config.use_mrrt, authorization_params={ "audience": config.audience, "redirect_uri": redirect_uri, diff --git a/src/auth0_fastapi/config.py b/src/auth0_fastapi/config.py index c227db5..f68eb37 100644 --- a/src/auth0_fastapi/config.py +++ b/src/auth0_fastapi/config.py @@ -15,7 +15,6 @@ class Auth0Config(BaseModel): audience: Optional[str] = Field(None, description="Target audience for tokens (if applicable)") authorization_params: Optional[dict[str, Any]] = Field(None, description="Additional parameters to include in the authorization request") pushed_authorization_requests: bool = Field(False, description="Whether to use pushed authorization requests") - use_mrrt: bool = Field(False, description="Whether to use Multi-Resource Refresh Tokens (MRRT)") # Route-mounting flags with desired defaults mount_routes: bool = Field(True, description="Controls /auth/* routes: login, logout, callback, backchannel-logout") mount_connect_routes: bool = Field(False, description="Controls /auth/connect routes (account-linking)") From 49069a0bd080f6a5aa043e4e8585bd56548e54cc Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Mon, 10 Nov 2025 18:55:57 +0000 Subject: [PATCH 08/17] Mount connected account start flow on the auth/connect route and make mutually exclusive with legacy connect behaviour --- src/auth0_fastapi/errors/__init__.py | 10 ++++++++++ src/auth0_fastapi/server/routes.py | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/auth0_fastapi/errors/__init__.py b/src/auth0_fastapi/errors/__init__.py index 9924a74..ddb6355 100644 --- a/src/auth0_fastapi/errors/__init__.py +++ b/src/auth0_fastapi/errors/__init__.py @@ -12,6 +12,16 @@ from fastapi.responses import JSONResponse +class ConfigurationError(Auth0Error): + """ + Error raised when an invalid configuration is used. + """ + code = "configuration_error" + + def __init__(self, message=None): + super().__init__(message or "An invalid configuration was provided.") + self.name = "ConfigurationError" + def auth0_exception_handler(request: Request, exc: Auth0Error): """ Exception handler for Auth0 SDK errors. diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 5bfe6e9..4a3bd90 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -6,6 +6,7 @@ from ..auth.auth_client import AuthClient from ..config import Auth0Config from ..util import create_route_url, to_safe_redirect +from ..errors import ConfigurationError router = APIRouter() @@ -26,6 +27,13 @@ def register_auth_routes(router: APIRouter, config: Auth0Config): """ Conditionally register auth routes based on config.mount_routes and config.mount_connect_routes. """ + if config.mount_connect_routes and config.mount_connected_account_routes: + # Connect routes uses the legacy account linking flow for token vault + # Connects Accounts is the preferred mechanism + # Both mount the `/auth/connect` route to initiate the flow + raise ConfigurationError( + "'mount_connect_routes' and 'mount_connected_account_routes' cannot be used together.") + if config.mount_routes: @router.get("/auth/login") async def login( @@ -134,7 +142,7 @@ async def backchannel_logout( return Response(status_code=204) if config.mount_connected_account_routes: - @router.get("/auth/connect-account") + @router.get("/auth/connect") async def connect_account( request: Request, response: Response, From 1046eb65b7c4110935bfe19262ae93dd066fb5aa Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Wed, 12 Nov 2025 14:43:33 +0000 Subject: [PATCH 09/17] Name scopes param consistantly --- src/auth0_fastapi/auth/auth_client.py | 2 ++ src/auth0_fastapi/server/routes.py | 12 +++++++----- src/auth0_fastapi/test/test_auth_client.py | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index 6345b79..1e5bbb8 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -90,6 +90,7 @@ async def complete_login( async def start_connect_account( self, connection: str, + scopes: list[str] | None = None, app_state: dict = None, authorization_params: dict = None, store_options: dict = None, @@ -101,6 +102,7 @@ async def start_connect_account( """ options = ConnectAccountOptions( connection=connection, + scopes=scopes, app_state=app_state, authorization_params=authorization_params ) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 4a3bd90..5a147e5 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from fastapi.responses import RedirectResponse @@ -93,7 +93,7 @@ async def callback( # Assuming config is stored on app.state default_redirect = auth_client.config.app_base_url - safe_redirect = to_safe_redirect(return_to or default_redirect, auth_client.config.app_base_url) + safe_redirect = to_safe_redirect(return_to, default_redirect) if return_to else str(default_redirect) return RedirectResponse(url=safe_redirect, headers=response.headers) @router.get("/auth/logout") @@ -147,18 +147,20 @@ async def connect_account( request: Request, response: Response, connection: str = Query(), + scopes: Annotated[list[str] | None, Query()] = None, + return_to: str = Query(default=None), auth_client: AuthClient = Depends(get_auth_client), ): """ Endpoint to initiate the connect account flow for linking a third-party account to the user's profile. Redirects the user to the Auth0 connect account URL. """ - authorization_params = { - k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo"]} + authorization_params = { + k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo", "scope"]} - return_to = request.query_params.get("returnTo") connect_account_url = await auth_client.start_connect_account( connection=connection, + scopes=scopes, app_state={"returnTo": return_to} if return_to else None, authorization_params=authorization_params, store_options={"request": request, "response": response}, diff --git a/src/auth0_fastapi/test/test_auth_client.py b/src/auth0_fastapi/test/test_auth_client.py index ba1f5f9..e7b4d5b 100644 --- a/src/auth0_fastapi/test/test_auth_client.py +++ b/src/auth0_fastapi/test/test_auth_client.py @@ -408,6 +408,7 @@ async def test_start_connect_account(self, auth_client): result = await auth_client.start_connect_account( connection="google-oauth2", + scopes=["openid", "profile", "email"], app_state={"returnTo": "/profile"}, authorization_params={"prompt": "consent"}, ) @@ -417,6 +418,7 @@ async def test_start_connect_account(self, auth_client): options=ConnectAccountOptions( connection="google-oauth2", app_state={"returnTo": "/profile"}, + scopes=["openid", "profile", "email"], authorization_params={"prompt": "consent"}, ), store_options=None) From e6ed8bb7ff246844335bcb94c7cda5f3d42ed7f1 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Thu, 13 Nov 2025 18:29:57 +0000 Subject: [PATCH 10/17] Correctly filter out scopes param rather than scope param --- src/auth0_fastapi/server/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 5a147e5..7a8dc0e 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -156,7 +156,7 @@ async def connect_account( Redirects the user to the Auth0 connect account URL. """ authorization_params = { - k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo", "scope"]} + k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo", "scopes"]} connect_account_url = await auth_client.start_connect_account( connection=connection, From 45f0f4effa22c536f5f073805b74ff08436f7380 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Fri, 14 Nov 2025 14:14:34 +0000 Subject: [PATCH 11/17] Alias the returnTo parameter --- src/auth0_fastapi/server/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 7a8dc0e..cc48eba 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -148,7 +148,7 @@ async def connect_account( response: Response, connection: str = Query(), scopes: Annotated[list[str] | None, Query()] = None, - return_to: str = Query(default=None), + return_to: str = Query(default=None, alias="returnTo"), auth_client: AuthClient = Depends(get_auth_client), ): """ From 20ae342c7bf75d9d782aab32000bb5be059f3205 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:11:03 +0000 Subject: [PATCH 12/17] Update auth0-server-python package --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index b2a6ccb..f1d3b84 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -35,14 +35,14 @@ trio = ["trio (>=0.31.0)"] [[package]] name = "auth0-server-python" -version = "1.0.0b5" +version = "1.0.0b6" description = "Auth0 server-side Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "auth0_server_python-1.0.0b5-py3-none-any.whl", hash = "sha256:23d8049870781b760b632dab522e55436e51e5817dc1c857265dd1574a0328a8"}, - {file = "auth0_server_python-1.0.0b5.tar.gz", hash = "sha256:1c20a30c9627f28209727ffc0e4013562c75b3455ae7db2400b0fc66a657cc8e"}, + {file = "auth0_server_python-1.0.0b6-py3-none-any.whl", hash = "sha256:2e59838919550d4ad643bc2332be1965ade61a8e0b95273ccaf2fc269cbd1d7f"}, + {file = "auth0_server_python-1.0.0b6.tar.gz", hash = "sha256:028dae820ecec556c91bf9015eb7ab7ce7712ee2e954560fda783a9675a3016f"}, ] [package.dependencies] @@ -973,4 +973,4 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3) [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "2e8ecdc2e20aa176a24302c51adf9e3f105af7886e076952d9e9c96326882fc9" +content-hash = "b4d8fbf1f699042e986c014a3b64078897cfb5238977bfccb85a4debe1056e8e" diff --git a/pyproject.toml b/pyproject.toml index 904a557..b0fc4d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ packages = [ [tool.poetry.dependencies] python = ">=3.9" -auth0-server-python = ">=1.0.0b5" +auth0-server-python = ">=1.0.0b6" fastapi = ">=0.115.11,<0.117.0" pydantic = "^2.12.3" From 4df4c27f2118f0cbb6ab6f47cf4dfb3590aa9a83 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:11:07 +0000 Subject: [PATCH 13/17] Update README.md --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 29be6db..372a7b9 100644 --- a/README.md +++ b/README.md @@ -189,14 +189,20 @@ config = Auth0Config( ) ``` -Additionally, by setting `mount_connect_routes` to `True` (it's `False` by default) the SDK also can also mount 4 routes useful for account-linking: +Additionally, by setting `mount_connected_account_routes` to `True` (it's `False` by default) the SDK also can also mount routes useful for using Token Vault with Connected Accounts: + +1. `/auth/connect`: the route that the user will be redirected to to initiate account linking +2. `/auth/callback`: will also handle the callback behaviour from the Connected Accounts flow + +Alternatively, by setting `mount_connect_routes` to `True` (it's `False` by default) the SDK also can also mount 4 routes useful for account-linking: 1. `/auth/connect`: the route that the user will be redirected to to initiate account linking 2. `/auth/connect/callback`: the callback route for account linking that must be added to your Auth0 application's Allowed Callback URLs 3. `/auth/unconnect`: the route that the user will be redirected to to initiate account linking 4. `/auth/unconnect/callback`: the callback route for account linking that must be added to your Auth0 application's Allowed Callback URLs - + These two behaviours cannot be used simultaneously. This form of account-linking is now considered legacy, use of Connected Accounts is preferred. + #### Protecting Routes In order to protect a FastAPI route, you can use the SDK's `get_session()` method and pass it through `Depends`: From a2bc7d8c957a6b891d2c6ba6eece83d8578158e7 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:11:18 +0000 Subject: [PATCH 14/17] Fix lint issues --- src/auth0_fastapi/server/routes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index cc48eba..9af9ace 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -1,12 +1,12 @@ -from typing import Optional, Annotated +from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from fastapi.responses import RedirectResponse from ..auth.auth_client import AuthClient from ..config import Auth0Config -from ..util import create_route_url, to_safe_redirect from ..errors import ConfigurationError +from ..util import create_route_url, to_safe_redirect router = APIRouter() @@ -33,7 +33,7 @@ def register_auth_routes(router: APIRouter, config: Auth0Config): # Both mount the `/auth/connect` route to initiate the flow raise ConfigurationError( "'mount_connect_routes' and 'mount_connected_account_routes' cannot be used together.") - + if config.mount_routes: @router.get("/auth/login") async def login( @@ -155,7 +155,7 @@ async def connect_account( Endpoint to initiate the connect account flow for linking a third-party account to the user's profile. Redirects the user to the Auth0 connect account URL. """ - authorization_params = { + authorization_params = { k: v for k, v in request.query_params.items() if k not in ["connection", "returnTo", "scopes"]} connect_account_url = await auth_client.start_connect_account( From 517f183302fb4fecafa609c3750a178742762004 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:43:09 +0000 Subject: [PATCH 15/17] Fix type issue on scopes array --- src/auth0_fastapi/auth/auth_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index 1e5bbb8..b9a8926 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -1,5 +1,6 @@ # Imported from auth0-server-python +from typing import Optional from auth0_server_python.auth_server.server_client import ServerClient from auth0_server_python.auth_types import ( CompleteConnectAccountResponse, @@ -90,7 +91,7 @@ async def complete_login( async def start_connect_account( self, connection: str, - scopes: list[str] | None = None, + scopes: Optional[list[str]] = None, app_state: dict = None, authorization_params: dict = None, store_options: dict = None, From 38b498140cc3b78311ea5f79619db2f33e32a2e8 Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:47:00 +0000 Subject: [PATCH 16/17] Fix liniting --- src/auth0_fastapi/auth/auth_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auth0_fastapi/auth/auth_client.py b/src/auth0_fastapi/auth/auth_client.py index b9a8926..be42a8b 100644 --- a/src/auth0_fastapi/auth/auth_client.py +++ b/src/auth0_fastapi/auth/auth_client.py @@ -1,6 +1,7 @@ # Imported from auth0-server-python from typing import Optional + from auth0_server_python.auth_server.server_client import ServerClient from auth0_server_python.auth_types import ( CompleteConnectAccountResponse, From 95972e8bccd617a0d33d69fd821d0b7c907a4e2d Mon Sep 17 00:00:00 2001 From: Sam Muncke Date: Tue, 18 Nov 2025 14:49:28 +0000 Subject: [PATCH 17/17] Fix type issues --- src/auth0_fastapi/server/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth0_fastapi/server/routes.py b/src/auth0_fastapi/server/routes.py index 9af9ace..3167ee4 100644 --- a/src/auth0_fastapi/server/routes.py +++ b/src/auth0_fastapi/server/routes.py @@ -147,7 +147,7 @@ async def connect_account( request: Request, response: Response, connection: str = Query(), - scopes: Annotated[list[str] | None, Query()] = None, + scopes: Annotated[Optional[list[str]], Query()] = None, return_to: str = Query(default=None, alias="returnTo"), auth_client: AuthClient = Depends(get_auth_client), ):