Skip to content

Commit 2dc377b

Browse files
authored
Merge branch 'main' into docs/claude-md
2 parents 6a8e544 + ba0a2a5 commit 2dc377b

14 files changed

Lines changed: 818 additions & 26 deletions

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0b13
1+
1.0.0b14

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## [1.0.0b14](https://github.com/auth0/auth0-server-python/tree/1.0.0b14) (2026-07-29)
4+
[Full Changelog](https://github.com/auth0/auth0-server-python/compare/1.0.0b13...1.0.0b14)
5+
6+
**Added**
7+
- feat: add Session Transfer Token support for CTE impersonation via session transfer [\#139](https://github.com/auth0/auth0-server-python/pull/139) ([kishore7snehil](https://github.com/kishore7snehil))
8+
39
## [1.0.0b13](https://github.com/auth0/auth0-server-python/tree/1.0.0b13) (2026-07-21)
410
[Full Changelog](https://github.com/auth0/auth0-server-python/compare/1.0.0b12...1.0.0b13)
511

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pip install auth0-server-python
2323
If you’re using Poetry:
2424

2525
```shell
26-
poetry install auth0-server-python
26+
poetry add auth0-server-python
2727
```
2828

2929
### 2. Create the Auth0 SDK client
@@ -40,7 +40,7 @@ auth0 = ServerClient(
4040
client_secret='<AUTH0_CLIENT_SECRET>',
4141
secret='<AUTH0_SECRET>',
4242
authorization_params= {
43-
redirect_uri: '<AUTH0_REDIRECT_URI>',
43+
'redirect_uri': '<AUTH0_REDIRECT_URI>',
4444
}
4545
)
4646
```
@@ -147,6 +147,11 @@ response = await auth0.custom_token_exchange(
147147
print(response.access_token)
148148
```
149149

150+
Building on token exchange, the SDK also supports:
151+
152+
- **[Delegation and Impersonation](examples/CustomTokenExchange.md#3-actor-tokens-delegation)** - exchange with an `actor_token` so the issued tokens record who is acting on whose behalf (the `act` claim).
153+
- **[Impersonation via Session Transfer (STT)](examples/CustomTokenExchange.md#8-impersonation-via-session-transfer-stt)** - mint a Session Transfer Token to log an agent into a target app as a customer, via `request_session_transfer_token()` and `build_session_transfer_redirect()`.
154+
150155
For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
151156

152157
### 5. Multiple Custom Domains (MCD)

examples/ClientInitiatedBackChannelLogin.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Before using backchannel authentication:
1515
### Initiating Backchannel Authentication
1616

1717
```python
18-
from auth0_server_python import ServerClient
18+
from auth0_server_python.auth_server import ServerClient
1919

2020
# Initialize the Auth0 client
2121
auth0 = ServerClient(
@@ -101,7 +101,7 @@ Read more above in [Configuring the Store](./ConfigureStore.md).
101101

102102
from fastapi import FastAPI, Request
103103
from fastapi.responses import JSONResponse
104-
from auth0_server_python import ServerClient
104+
from auth0_server_python.auth_server import ServerClient
105105

106106
app = FastAPI()
107107

examples/ConfigureStore.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ If you’re using `auth0-fastapi`, you already have:
3737
2. **CookieTransactionStore** – stores short-lived transaction data (PKCE code_verifier) in another encrypted cookie.
3838

3939
```python
40-
from store.abstract import StateStore, TransactionStore
40+
from auth0_server_python.store import StateStore, TransactionStore
4141

4242
class StatelessStateStore(StateStore):
4343
def __init__(self, secret: str, cookie_name: str = "_a0_session"):
@@ -98,9 +98,10 @@ A **stateful** approach stores only a **session ID** in the cookie, while the ac
9898
### 3.1. Redis-Based Example
9999
Let’s walk through a `RedisStateStore` that inherits from `StateStore`:
100100
```python
101+
import json
101102
import aioredis
102103
from typing import Any, Dict, Optional
103-
from store.abstract import StateStore
104+
from auth0_server_python.store import StateStore
104105

105106
class RedisStateStore(StateStore):
106107
"""
@@ -133,7 +134,6 @@ class RedisStateStore(StateStore):
133134
# encrypted_data = self.encrypt(session_id, state)
134135
# await self.redis_client.set(session_id, encrypted_data)
135136
# For demo, let's store it as JSON without encryption:
136-
import json
137137
await self.redis_client.set(session_id, json.dumps(state))
138138

139139
# Now set a cookie in the response with just the session_id
@@ -166,7 +166,6 @@ class RedisStateStore(StateStore):
166166
return None
167167

168168
# If you used self.encrypt(...) on set, call self.decrypt(...) here.
169-
import json
170169
return json.loads(raw_data)
171170

172171
async def delete(
@@ -206,7 +205,6 @@ class RedisStateStore(StateStore):
206205
for k in keys:
207206
raw_data = await self.redis_client.get(k)
208207
if raw_data:
209-
import json
210208
session_data = json.loads(raw_data)
211209
# If your session_data stores an internal dict with `sid` or `sub`
212210
internal = session_data.get("internal", {})
@@ -240,9 +238,10 @@ Now your user’s session data is in **Redis**, and only a minimal session ID is
240238
If you prefer a **SQL database** for session data, here’s a `PostgresStateStore` example using [asyncpg](https://github.com/MagicStack/asyncpg).
241239

242240
```python
241+
import json
243242
import asyncpg
244243
from typing import Any, Dict, Optional
245-
from store.abstract import StateStore
244+
from auth0_server_python.store import StateStore
246245

247246
class PostgresStateStore(StateStore):
248247
"""
@@ -277,7 +276,6 @@ class PostgresStateStore(StateStore):
277276
# Optionally encrypt `state`:
278277
# encrypted_data = self.encrypt(session_id, state)
279278
# For simplicity, store as JSON
280-
import json
281279
data_json = json.dumps(state)
282280

283281
# Insert or update the session
@@ -322,7 +320,6 @@ class PostgresStateStore(StateStore):
322320
if not row:
323321
return None
324322
# If you used encryption, do self.decrypt(...)
325-
import json
326323
return json.loads(row["session_data"])
327324

328325
async def delete(

examples/CustomTokenExchange.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,99 @@ Use standard URNs when possible:
179179
"urn:company:legacy-token"
180180
```
181181

182+
## 8. Impersonation via Session Transfer (STT)
183+
184+
Custom Token Exchange can also mint a **Session Transfer Token (STT)** instead of an API access token. An STT lets an initiator app (for example a support console) log an agent into a target web app **as** a customer, with the agent recorded in the `act` claim - so a support engineer can reproduce a customer's exact experience without their password.
185+
186+
This is a two-role, two-hop flow:
187+
188+
- **Initiator** (the agent's app) mints the STT and redirects with it. This is where the new SDK methods live.
189+
- **Target** (the customer's app) forwards the STT to `/authorize` on a normal interactive login, which establishes the impersonated session.
190+
191+
The STT is opaque, single-use, and short-lived (~60s). The SDK requests it and helps build the redirect - it never decodes or stores it.
192+
193+
### Initiator: request an STT and build the redirect
194+
195+
```python
196+
from auth0_server_python.auth_server.server_client import ServerClient
197+
from auth0_server_python.error import CustomTokenExchangeError
198+
199+
# Mint the STT. The audience (urn:{domain}:session_transfer), grant type, and the actor are
200+
# set by the SDK - the actor is sourced from the logged-in agent's session.
201+
result = await auth0.request_session_transfer_token(
202+
subject_token=subject_token, # your proof of which customer to impersonate
203+
subject_token_type="urn:acme:customer-subject",
204+
organization=None, # optional, sent on the mint request (separate from the redirect)
205+
store_options={"request": request, "response": None},
206+
)
207+
208+
# result.session_transfer_token is the opaque, one-shot STT (~60s). Never store it.
209+
redirect_url = auth0.build_session_transfer_redirect(
210+
"https://customer-app.example.com/auth/login", result, organization=None
211+
)
212+
return RedirectResponse(redirect_url) # your framework performs the redirect
213+
```
214+
215+
`SessionTransferTokenResult` carries `session_transfer_token`, `issued_token_type` (the session-transfer URN - the field to branch on), `expires_in`, and an informational `token_type` (`N_A`). There is no `act` on this result; `act` appears later, on the target session.
216+
217+
> **NOTE**: An actor is mandatory - an STT is only issued when the Action set one. By default the SDK sources the actor from the logged-in agent's session ID token, refreshing it when expired. If the agent is not logged in (no usable session ID token and none can be refreshed), the call fails client-side with `ACTOR_UNAVAILABLE` before any network request.
218+
219+
> **NOTE**: To use your own actor token instead of the session, pass `actor_token` (and optionally `actor_token_type`, which defaults to the ID token URN). An explicit `actor_token` takes precedence and the session is not read at all. When `actor_token_type` is the ID token URN (the default), Auth0 validates the token, so it must be:
220+
>
221+
> - Signed with RS256 or PS256 (HS256 is rejected, it uses a shared secret).
222+
> - Unexpired, and carrying `sub`, `iss`, `exp`, and `iat`.
223+
> - Issued to the same client making the exchange (its `aud` must be that client's ID).
224+
> - Belonging to a user who still exists and is not blocked.
225+
>
226+
> An Auth0 ID token from the agent's own session on this client satisfies all of these. A token that fails any of them is rejected by the server.
227+
>
228+
> ```python
229+
> result = await auth0.request_session_transfer_token(
230+
> subject_token=subject_token,
231+
> subject_token_type="urn:acme:customer-subject",
232+
> actor_token=agent_id_token, # explicit override - session is not used
233+
> store_options={"request": request, "response": None},
234+
> )
235+
> ```
236+
237+
### Target: forward the STT to `/authorize`
238+
239+
On the target, the STT rides through your normal login. `start_interactive_login` forwards arbitrary authorization parameters to `/authorize`, so your login route just passes `session_transfer_token` (and `organization`, when you want the target login org-scoped) straight through:
240+
241+
```python
242+
from auth0_server_python.auth_types import StartInteractiveLoginOptions
243+
244+
url = await auth0.start_interactive_login(
245+
StartInteractiveLoginOptions(authorization_params={
246+
"session_transfer_token": request.query_params["session_transfer_token"],
247+
# "organization": org, # when you want the target login org-scoped
248+
}),
249+
store_options={"request": request, "response": None},
250+
)
251+
return RedirectResponse(url)
252+
```
253+
254+
After the callback completes, read the acting party off the session user - the same way as the [Actor Tokens (Delegation)](#3-actor-tokens-delegation) section above:
255+
256+
```python
257+
session = await auth0.get_session(store_options={"request": request, "response": None})
258+
act = (session or {}).get("user", {}).get("act")
259+
if act:
260+
print(f"Impersonated by: {act['sub']}") # drive an impersonation banner, etc.
261+
```
262+
263+
> **NOTE**: Both clients need one-time configuration through the Auth0 Dashboard or Management API. The issuing (initiator) client must be allowed to create session transfer tokens. The redeeming (target) client must be allowed to accept delegated-access sessions and to receive the token as a query parameter. See the [Auth0 documentation](https://auth0.com/docs/authenticate/custom-token-exchange) for the exact client settings.
264+
265+
> **NOTE**: `build_session_transfer_redirect` attaches a single-use credential to `target_login_url`, so that URL must be a trusted, app-controlled value - never one derived from untrusted input (such as a user-supplied `returnTo`), which could leak the token to an attacker host.
266+
267+
> **NOTE**: The impersonation session is hard-capped at 2 hours and cannot mint a refresh token (`offline_access` is dropped when an actor is present). To continue past that, re-run the flow.
268+
269+
### STT error codes
270+
271+
- `ACTOR_UNAVAILABLE`: no usable actor token (client-side; raised before any network call)
272+
- `SETACTOR_REQUIRED`: an STT was requested but the Action did not call `setActor` (server 400)
273+
- `SESSION_TRANSFER_DISABLED`: the session-transfer feature is not enabled for the tenant/client (server 400)
274+
182275
## Additional Resources
183276

184277
- [Auth0 Custom Token Exchange Documentation](https://auth0.com/docs/authenticate/custom-token-exchange)

examples/MultipleCustomDomains.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ See [Security Best Practices](#security-best-practices) for important guidance o
1919
For applications with a single Auth0 domain:
2020

2121
```python
22-
from auth0_server_python import ServerClient
22+
from auth0_server_python.auth_server import ServerClient
2323

2424
client = ServerClient(
2525
domain="login.yourapp.com", # Static string
@@ -34,7 +34,7 @@ client = ServerClient(
3434
For MCD support, provide a domain resolver function that receives a `DomainResolverContext`:
3535

3636
```python
37-
from auth0_server_python import ServerClient
37+
from auth0_server_python.auth_server import ServerClient
3838
from auth0_server_python.auth_types import DomainResolverContext
3939

4040
# Map your app hostnames to Auth0 custom domains

examples/RetrievingData.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
The SDK's `get_user()` can be used to retrieve the current logged-in user:
66

77
```python
8-
user = await serverClient.get_user();
8+
user = await server_client.get_user()
99
```
1010

1111
### Passing Store Options
1212

13-
Just like most methods, `getUser` accept an argument that is used to pass to the configured Transaction and State Store:
13+
Just like most methods, `get_user` accept an argument that is used to pass to the configured Transaction and State Store:
1414

1515
```python
1616
store_options = {
@@ -27,7 +27,7 @@ Read more above in [Configuring the Store](./ConfigureStore.md).
2727
The SDK's `get_session()` can be used to retrieve the current session data:
2828

2929
```python
30-
session = await serverClient.get_session();
30+
session = await server_client.get_session()
3131
```
3232

3333
### Passing Store Options
@@ -58,7 +58,7 @@ In order to do this, the SDK needs access to a Refresh Token. By default, the SD
5858

5959
### Passing Store Options
6060

61-
Just like most methods, `getAccessToken` accept an argument that is used to pass to the configured Transaction and State Store:
61+
Just like most methods, `get_access_token` accept an argument that is used to pass to the configured Transaction and State Store:
6262

6363
```python
6464
store_options = {
@@ -225,7 +225,7 @@ token = await server_client.get_access_token(audience="https://api.example.com")
225225

226226
# Avoid unless necessary: Dynamic scopes increase session size
227227
token = await server_client.get_access_token(
228-
audience="https://api.example.com"
228+
audience="https://api.example.com",
229229
scope="openid profile email read:products write:products admin:all"
230230
)
231231
```
@@ -244,7 +244,7 @@ access_token_for_google = await server_client.get_access_token_for_connection(co
244244
```
245245

246246
- `connection`: The connection for which an access token should be retrieved, e.g. `google-oauth2` for Google.
247-
- `loginHint`: Optional login hint to inform which connection account to use, can be useful when multiple accounts for the connection exist for the same user.
247+
- `login_hint`: Optional login hint to inform which connection account to use, can be useful when multiple accounts for the connection exist for the same user.
248248

249249
The SDK will cache the token internally, and return it from the cache when not expired. When no token is found in the cache, or the token is expired, calling `get_access_token_for_connection()` will call Auth0 to retrieve a new token and update the cache.
250250

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "auth0-server-python"
3-
version = "1.0.0b13"
3+
version = "1.0.0b14"
44
description = "Auth0 server-side Python SDK"
55
readme = "README.md"
66
authors = ["Auth0 <support@okta.com>"]

0 commit comments

Comments
 (0)