|
| 1 | +# Organizations |
| 2 | + |
| 3 | +[Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers building SaaS and B2B applications. This guide covers the two main deployment patterns and the invitation flow. |
| 4 | + |
| 5 | +- [1. Dedicated-org instance](#1-dedicated-org-instance) |
| 6 | +- [2. Multi-org — per-login override](#2-multi-org--per-login-override) |
| 7 | +- [3. Log in using an organization name](#3-log-in-using-an-organization-name) |
| 8 | +- [4. Accept user invitations](#4-accept-user-invitations) |
| 9 | +- [5. Handling organization errors](#5-handling-organization-errors) |
| 10 | +- [6. Reading organization data from the session](#6-reading-organization-data-from-the-session) |
| 11 | + |
| 12 | +## 1. Dedicated-org instance |
| 13 | + |
| 14 | +When a single instance of your application serves one organization, set `organization` at client initialization. Every login from that instance will include the `organization` parameter in the `/authorize` request and validate the `org_id` claim in the returned token automatically. |
| 15 | + |
| 16 | +```python |
| 17 | +from auth0_server_python.auth_server.server_client import ServerClient |
| 18 | + |
| 19 | +auth0 = ServerClient( |
| 20 | + domain="YOUR_AUTH0_DOMAIN", |
| 21 | + client_id="YOUR_CLIENT_ID", |
| 22 | + client_secret="YOUR_CLIENT_SECRET", |
| 23 | + secret="YOUR_SECRET", |
| 24 | + organization="org_abc123", |
| 25 | + authorization_params={ |
| 26 | + "redirect_uri": "http://localhost:3000/auth/callback", |
| 27 | + } |
| 28 | +) |
| 29 | +``` |
| 30 | + |
| 31 | +```python |
| 32 | +from fastapi import FastAPI, Request, Response |
| 33 | +from starlette.responses import RedirectResponse |
| 34 | + |
| 35 | +app = FastAPI() |
| 36 | + |
| 37 | +@app.get("/auth/login") |
| 38 | +async def login(request: Request, response: Response): |
| 39 | + authorization_url = await auth0.start_interactive_login( |
| 40 | + store_options={"request": request, "response": response} |
| 41 | + ) |
| 42 | + return RedirectResponse(url=authorization_url) |
| 43 | + |
| 44 | +@app.get("/auth/callback") |
| 45 | +async def callback(request: Request, response: Response): |
| 46 | + result = await auth0.complete_interactive_login( |
| 47 | + str(request.url), |
| 48 | + store_options={"request": request, "response": response} |
| 49 | + ) |
| 50 | + return RedirectResponse(url="/dashboard") |
| 51 | +``` |
| 52 | + |
| 53 | +> [!NOTE] |
| 54 | +> You do not need to pass `organization` to `complete_interactive_login`. The SDK stores it in the encrypted transaction at login time and reads it back at callback — the validation is automatic. |
| 55 | +
|
| 56 | +## 2. Multi-org — per-login override |
| 57 | + |
| 58 | +When one application instance serves multiple organizations (for example, a B2B SaaS where different users belong to different orgs), pass `organization` at login time using `StartInteractiveLoginOptions`. This overrides any client-level default for that specific login. |
| 59 | + |
| 60 | +```python |
| 61 | +from auth0_server_python.auth_types import StartInteractiveLoginOptions |
| 62 | + |
| 63 | +@app.get("/auth/login") |
| 64 | +async def login(request: Request, response: Response, org_id: str): |
| 65 | + authorization_url = await auth0.start_interactive_login( |
| 66 | + StartInteractiveLoginOptions(organization=org_id), |
| 67 | + store_options={"request": request, "response": response} |
| 68 | + ) |
| 69 | + return RedirectResponse(url=authorization_url) |
| 70 | +``` |
| 71 | + |
| 72 | +> [!IMPORTANT] |
| 73 | +> Validate that `org_id` comes from a trusted source (your own data, a verified session, or a registered tenant list) — never pass it unvalidated from a query parameter directly from an untrusted user. |
| 74 | +
|
| 75 | +## 3. Log in using an organization name |
| 76 | + |
| 77 | +`organization` accepts either an org ID (starts with `org_`) or an org name (any other value). The SDK uses the prefix to determine which token claim to validate at callback: |
| 78 | + |
| 79 | +- **`org_` prefix** → validates `org_id` claim (exact, case-sensitive match) |
| 80 | +- **No `org_` prefix** → validates `org_name` claim (case-insensitive match) |
| 81 | + |
| 82 | +```python |
| 83 | +# By org ID — validates the org_id claim in the token |
| 84 | +authorization_url = await auth0.start_interactive_login( |
| 85 | + StartInteractiveLoginOptions(organization="org_abc123") |
| 86 | +) |
| 87 | + |
| 88 | +# By org name — validates the org_name claim in the token (case-insensitive) |
| 89 | +authorization_url = await auth0.start_interactive_login( |
| 90 | + StartInteractiveLoginOptions(organization="acme-corp") |
| 91 | +) |
| 92 | +``` |
| 93 | + |
| 94 | +> [!NOTE] |
| 95 | +> Auth0 enforces that organization names cannot start with `org_`, so the prefix dispatch is unambiguous. When using org name, the SDK applies NFC Unicode normalization before comparison to prevent false rejections from visually identical characters with different byte representations. |
| 96 | +
|
| 97 | +## 4. Accept user invitations |
| 98 | + |
| 99 | +When a user follows an invitation link, extract the `invitation` and `organization` parameters from the URL and pass them at login time. Auth0 validates the invitation ticket server-side — your application does not need to verify it. |
| 100 | + |
| 101 | +The invitation URL Auth0 generates has this shape: |
| 102 | +``` |
| 103 | +https://your-tenant.auth0.com/login?invitation={INVITATION_TOKEN}&organization={ORG_ID}&organization_name={ORG_NAME} |
| 104 | +``` |
| 105 | + |
| 106 | +```python |
| 107 | +@app.get("/auth/login") |
| 108 | +async def login(request: Request, response: Response): |
| 109 | + invitation = request.query_params.get("invitation") |
| 110 | + organization = request.query_params.get("organization") |
| 111 | + |
| 112 | + options = StartInteractiveLoginOptions(organization=organization) |
| 113 | + if invitation: |
| 114 | + options.authorization_params = {"invitation": invitation} |
| 115 | + |
| 116 | + authorization_url = await auth0.start_interactive_login( |
| 117 | + options, |
| 118 | + store_options={"request": request, "response": response} |
| 119 | + ) |
| 120 | + return RedirectResponse(url=authorization_url) |
| 121 | +``` |
| 122 | + |
| 123 | +> [!NOTE] |
| 124 | +> `organization` and `invitation` are forwarded to `/authorize`. Auth0 consumes the invitation ticket server-side — it is not stored in the encrypted transaction. If the ticket is expired or already used, `complete_interactive_login` raises `OrganizationInvitationError`. |
| 125 | +
|
| 126 | +## 5. Handling organization errors |
| 127 | + |
| 128 | +The SDK raises typed exceptions for org-specific failure modes. Catch them in your callback handler to return meaningful responses to your users. |
| 129 | + |
| 130 | +```python |
| 131 | +from auth0_server_python.error import ( |
| 132 | + OrganizationInvitationError, |
| 133 | + OrganizationAccessDeniedError, |
| 134 | + OrganizationRequiredError, |
| 135 | + OrganizationTokenValidationError, |
| 136 | +) |
| 137 | + |
| 138 | +@app.get("/auth/callback") |
| 139 | +async def callback(request: Request, response: Response): |
| 140 | + try: |
| 141 | + result = await auth0.complete_interactive_login( |
| 142 | + str(request.url), |
| 143 | + store_options={"request": request, "response": response} |
| 144 | + ) |
| 145 | + return RedirectResponse(url="/dashboard") |
| 146 | + except OrganizationAccessDeniedError: |
| 147 | + # User is not a member of the org, the connection is not enabled |
| 148 | + # for the org, or the org member quota has been exceeded. |
| 149 | + return RedirectResponse(url="/error?reason=not_org_member") |
| 150 | + except OrganizationRequiredError: |
| 151 | + # Configuration problem — invalid org format, Organizations feature |
| 152 | + # disabled, or the client is not configured for organizations. |
| 153 | + return RedirectResponse(url="/error?reason=org_config") |
| 154 | + except OrganizationInvitationError: |
| 155 | + # The invitation ticket is expired, already used, or invalid. |
| 156 | + return RedirectResponse(url="/error?reason=invitation_invalid") |
| 157 | + except OrganizationTokenValidationError: |
| 158 | + # The org_id or org_name in the returned token does not match |
| 159 | + # the organization that was requested at login. |
| 160 | + return RedirectResponse(url="/error?reason=org_mismatch") |
| 161 | +``` |
| 162 | + |
| 163 | +| Exception | When raised | |
| 164 | +|-----------|-------------| |
| 165 | +| `OrganizationAccessDeniedError` | User not a member, connection not enabled for org, member quota exceeded | |
| 166 | +| `OrganizationRequiredError` | Invalid org format, feature disabled, client not configured for orgs | |
| 167 | +| `OrganizationInvitationError` | Invitation ticket expired, already used, or invalid | |
| 168 | +| `OrganizationTokenValidationError` | `org_id` / `org_name` in the returned token does not match what was requested | |
| 169 | + |
| 170 | +## 6. Reading organization data from the session |
| 171 | + |
| 172 | +After a successful org login, `org_id` and `org_name` are available on the user object. Use `get_user()` to retrieve them on subsequent requests: |
| 173 | + |
| 174 | +```python |
| 175 | +user = await auth0.get_user(store_options={"request": request, "response": response}) |
| 176 | +if user: |
| 177 | + print(user.get("org_id")) # e.g. "org_abc123" |
| 178 | + print(user.get("org_name")) # e.g. "acme-corp" |
| 179 | +``` |
| 180 | + |
| 181 | +You can also read them immediately from the `complete_interactive_login` result: |
| 182 | + |
| 183 | +```python |
| 184 | +result = await auth0.complete_interactive_login( |
| 185 | + str(request.url), |
| 186 | + store_options={"request": request, "response": response} |
| 187 | +) |
| 188 | +user = result["state_data"].get("user", {}) |
| 189 | +print(user.get("org_id")) |
| 190 | +print(user.get("org_name")) |
| 191 | +``` |
| 192 | + |
| 193 | +> [!NOTE] |
| 194 | +> `org_name` is mutable — Auth0 allows renaming an organization after creation. Use `org_id` as the stable identifier for any persistent storage (e.g., mapping users to tenants in your database). Surface `org_name` only for display purposes. |
0 commit comments