Enterprise Connect lets your application sign users in through their company's identity provider while your app owns the session. Auth0 acts as a pure SSO relay: it authenticates the user and returns verified claims, but issues no refresh token and holds no session on your behalf. This guide covers the enterprise_connect mode on ServerClient, email-domain discovery, the callback contract, and federated logout.
Important
Enterprise Connect is an Early Access feature. Tenant setup (entitlements, connection type, and the exact claims a token carries) depends on your Auth0 configuration and may change. Treat the tenant-side steps below as a starting point and confirm them against your tenant. The SDK behavior described here is stable.
Important
These flows are for confidential server-side applications. The verified claims and access token are handed to your server, which creates and owns the user session. The browser should only ever receive your application's own session cookie or opaque session reference, never an Auth0 token.
- How the flow works
- Prerequisites
- 1. Configure the client
- 2. Discover and start the login
- 3. Complete the callback
- 4. Protect your routes
- 5. Organizations and multi-tenant apps
- 6. Logout
- Available and blocked methods
- Error Handling
- The user enters their email. Your app calls
start_enterprise_login(), which runs WebFinger discovery on the email domain. - If the domain is managed by Auth0 for enterprise SSO, the SDK returns an authorization URL with the email as
login_hintso Auth0 can resolve the connection and organization. If it is not managed, the method returnsNoneand your app falls back to its own login. - The user authenticates at their identity provider and is redirected back to your callback.
- Your app calls
complete_interactive_login(). The SDK exchanges the code, verifies the ID token's signature and issuer, and returns the claims from it. It persists nothing and issues no refresh token. - Your app creates its own first-party session from the returned claims.
The contract is inverted from a normal login: the SDK does not store a session, so the session-reading methods (get_session, get_access_token) are not available in this mode.
Enterprise Connect requires a Regular Web Application with a client secret. The tenant and connection must be provisioned for Enterprise Connect (Early Access), and WebFinger discovery must be enabled on the tenant. Work with your Auth0 contact to confirm entitlements for your tenant.
Do not request offline_access and do not set a static organization on the client. Enterprise Connect issues no refresh token, and the organization is resolved from the login email at Auth0. The SDK warns at construction if either is set.
Opt in with enterprise_connect=True. Supply a transaction_store (used to protect the callback with state and PKCE). A state_store is not needed because the SDK persists no session.
from auth0_server_python.auth_server.server_client import ServerClient
server_client = ServerClient(
domain="YOUR_AUTH0_DOMAIN",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
secret="YOUR_SECRET",
redirect_uri="https://app.example.com/auth/callback",
authorization_params={"scope": "openid profile email"},
enterprise_connect=True,
)For apps using request/response-backed stores or multiple custom domains, pass store_options={"request": request, "response": response} to each method that reads or writes transaction state.
Important
The SDK writes its transaction cookie onto the response you pass in store_options: the state and PKCE values when a login starts, and a deletion of that cookie when the callback completes. Return that same response object, with your own redirect and session cookie set on it. Do not return a fresh redirect(url) built after the SDK call. A new response object carries none of the SDK's Set-Cookie headers, so the transaction cookie never reaches the browser and the callback finds no transaction to match. The examples below create the response first, thread it through store_options, then set its Location last. The pattern is framework-neutral: redirect(...), .set_cookie(...), and .headers["Location"] map onto the equivalent calls in your web framework.
Your app must serve a login page that collects the user's work email. Pass it to start_enterprise_login(), which runs WebFinger discovery and returns the authorization URL when the domain is managed, or None when it is not.
from auth0_server_python.auth_types import StartEnterpriseLoginOptions
from auth0_server_python.error import MissingRequiredArgumentError, InvalidArgumentError
response = redirect("/") # placeholder target, overwritten once discovery returns
try:
auth_url = await server_client.start_enterprise_login(
StartEnterpriseLoginOptions(
email=user_email,
app_state={"return_to": "/dashboard"},
),
store_options={"request": request, "response": response},
)
except (MissingRequiredArgumentError, InvalidArgumentError):
auth_url = None
response.headers["Location"] = auth_url or "/login/password"
return responseImportant
Discovery is a routing hint, not an authorization decision. It fails closed to "not managed" on any error, so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in - the callback must still complete.
If you only need the discovery signal (for example, to decide which login button to show), call the standalone helper. It takes no client instance and is stateless.
from auth0_server_python.auth_server import is_federated_domain
managed = await is_federated_domain("YOUR_AUTH0_DOMAIN", "acme.example")When Auth0 redirects back, call complete_interactive_login(). In Enterprise Connect mode it returns the verified claims and an access token instead of a session record.
response = redirect("/") # placeholder target, overwritten once app_state is known
result = await server_client.complete_interactive_login(
str(request.url),
store_options={"request": request, "response": response},
)
user = result["user"]
response.set_cookie(
"app_session",
sign_session({"sub": user.get("sub"), "email": user.get("email"), "org_id": user.get("org_id") or ""}),
httponly=True,
secure=True,
samesite="lax",
)
app_state = result.get("app_state") or {}
response.headers["Location"] = app_state.get("return_to", "/")
return responseThe returned dict contains:
user- the verified claims from the ID token, as a dicttoken_set-audience,access_token,scope, andexpires_atid_token- the raw ID token, for your own usedomain- the Auth0 domain the login came fromapp_state- present only when you passedapp_stateatstart_enterprise_login()
result and user are both plain dicts. Read claims with user.get("sub") so a claim the connection did not return yields None instead of raising KeyError. sub is always present, the optional claims (email, org_id, name) may not be.
The SDK verifies the ID token's signature and issuer, and derives the returned claims from it, before returning. It does not write a session store record and retains no refresh token.
Warning
The access token expires and cannot be renewed. Enterprise Connect issues no refresh token, so when it expires the user must re-authenticate. token_set["expires_at"] is a Unix timestamp in seconds. Compare it with time.time() before using the access token. For API authorization, issue your own tokens from the callback claims rather than relying on the Auth0 access token long-term.
The session cookie is your trust boundary now, not Auth0's. A signed-in user controls their own cookies, so an unsigned session lets them rewrite sub, email, or org_id to impersonate another user or forge membership in an organization they were denied. Sign the cookie with a server-side secret and verify the signature in constant time before trusting any claim in it.
import base64
import hashlib
import hmac
import json
from typing import Optional
_SESSION_SECRET = b"YOUR_SESSION_SECRET"
def sign_session(claims: dict) -> str:
body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
sig = hmac.new(_SESSION_SECRET, body.encode(), hashlib.sha256).hexdigest()
return f"{body}.{sig}"
def read_session(cookie: Optional[str]) -> Optional[dict]:
if not cookie or "." not in cookie:
return None
body, _, sig = cookie.partition(".")
expected = hmac.new(_SESSION_SECRET, body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return None
padded = body + "=" * (-len(body) % 4)
try:
return json.loads(base64.urlsafe_b64decode(padded).decode())
except ValueError:
return NoneImportant
Hold _SESSION_SECRET server-side only (Tier 0) and never ship it to the browser. Use a dedicated secret rather than reusing one for another purpose. The hmac.compare_digest check is what makes a forged cookie fail closed - a plain == comparison leaks the signature through timing.
Your app owns the session - the SDK holds nothing. Check your session store at the start of any route that requires authentication and redirect to your login page when the session is absent.
session = read_session(request.cookies.get("app_session"))
if not session:
return redirect("/login")
your_template.render(user=session)Important
Do not store an Auth0 access or ID token in your session. Store only the claims you need (for example sub, email, org_id). The browser must never see an Auth0 token.
Auth0 stamps the resolved organization into the token as org_id, available on result["user"]. The SDK surfaces it but does not enforce it.
Clear your own application session first, then send the user to the Auth0 logout URL. For Enterprise Connect, always request federated logout with federated=True.
from auth0_server_python.auth_types import LogoutOptions
response = redirect("/") # placeholder target, overwritten once logout returns
clear_app_session(response) # delete your session cookie on the response you return
logout_url = await server_client.logout(
LogoutOptions(return_to="https://app.example.com/login", federated=True),
store_options={"request": request, "response": response},
)
response.headers["Location"] = logout_url
return responseEnterprise Connect signs the user in through their enterprise identity provider and keeps no refresh token, so the identity provider session is the durable credential, not anything your app or Auth0 holds. A non-federated logout clears your app session and the Auth0 session but leaves that identity provider session alive, so the next start_enterprise_login() signs the user straight back in without a prompt. On a shared device that is not a real logout, which is why Enterprise Connect logout must be federated.
Note
Federated logout also ends the user's session at other applications that share the same enterprise SSO, not just yours. That is the intended reach of an enterprise sign-out. It depends on the identity provider supporting federated logout, so confirm that on the connection.
Note
return_to must be an absolute URL on your tenant's Allowed Logout URLs list. Auth0 rejects a URL that is not allow-listed.
These members work in Enterprise Connect mode:
| Member | Notes |
|---|---|
start_enterprise_login() |
EC login entry point |
start_interactive_login() |
Writes the transaction store only |
complete_interactive_login() |
Returns verified claims without persisting a session |
logout() |
Returns the Auth0 logout URL. No session or transaction state to clear in this mode |
custom_token_exchange() |
Works once, while the callback access token is valid. No refresh after it expires |
handle_backchannel_logout() |
No-op. The SDK holds no session to revoke |
Everything else raises EnterpriseConnectError with code enterprise_connect_not_supported. The message names the member that was called. For a session or access token, read them from complete_interactive_login() instead.
Own the session and any token refresh in your app.
from auth0_server_python.error import ApiError, EnterpriseConnectError
try:
result = await server_client.complete_interactive_login(
str(request.url),
store_options={"request": request, "response": response},
)
except ApiError as e:
return {"error": e.code}
try:
await server_client.get_access_token()
except EnterpriseConnectError as e:
return {"error": e.code}Errors you may see:
EnterpriseConnectError- a session or refresh-dependent member is not available in this mode. Itscodeis alwaysenterprise_connect_not_supported. The message names the member that was calledApiError- the token exchange failed, or the login returned no verifiable claims (invalid_response)