Skip to content

Commit 65bae24

Browse files
committed
SDK-8833: Added Example docs, refined org errors and reverted some comments
1 parent 52504fe commit 65bae24

5 files changed

Lines changed: 369 additions & 28 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and
173173

174174
For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
175175

176+
### 6. Organizations
177+
178+
To restrict login to a specific organization, set `organization` at client initialization (dedicated-org) or per login (multi-org):
179+
180+
```python
181+
from auth0_server_python.auth_server.server_client import ServerClient
182+
from auth0_server_python.auth_types import StartInteractiveLoginOptions
183+
184+
# Dedicated-org: every login enforces this organization
185+
auth0 = ServerClient(
186+
domain='<AUTH0_DOMAIN>',
187+
client_id='<AUTH0_CLIENT_ID>',
188+
client_secret='<AUTH0_CLIENT_SECRET>',
189+
secret='<AUTH0_SECRET>',
190+
organization='org_abc123',
191+
authorization_params={"redirect_uri": "<AUTH0_REDIRECT_URI>"}
192+
)
193+
194+
# Multi-org: pass organization per login
195+
authorization_url = await auth0.start_interactive_login(
196+
StartInteractiveLoginOptions(organization="org_xyz789"),
197+
store_options={"request": request, "response": response}
198+
)
199+
```
200+
201+
The SDK validates the `org_id` (or `org_name`) claim in the returned token automatically — no extra code needed at callback. For invitation flows, per-org error handling, and reading org data from the session, see [examples/Organizations.md](examples/Organizations.md).
202+
176203
## Feedback
177204

178205
### Contributing

examples/Organizations.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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

Comments
 (0)