Skip to content

Commit b4e7945

Browse files
Merge remote-tracking branch 'origin/main' into chore/migrate-rl-scanner
2 parents 62d55c0 + 1c98878 commit b4e7945

13 files changed

Lines changed: 861 additions & 101 deletions

File tree

.version

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

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.0b9](https://github.com/auth0/auth0-api-python/tree/1.0.0b9) (2026-04-30)
4+
[Full Changelog](https://github.com/auth0/auth0-api-python/compare/1.0.0b8...1.0.0b9)
5+
6+
**Added**
7+
- feat: add On Behalf Of Token Exchange support [\#88](https://github.com/auth0/auth0-api-python/pull/88) ([kishore7snehil](https://github.com/kishore7snehil))
8+
39
## [1.0.0b8](https://github.com/auth0/auth0-api-python/tree/1.0.0b8) (2026-04-09)
410
[Full Changelog](https://github.com/auth0/auth0-api-python/compare/1.0.0b7...1.0.0b8)
511

EXAMPLES.md

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,121 @@
22

33
This document provides examples for using the `auth0-api-python` package to validate Auth0 tokens in your API.
44

5+
## On Behalf Of Token Exchange
6+
7+
Use `get_token_on_behalf_of()` when your API receives an `Auth0` access token for itself and needs
8+
to exchange it for another `Auth0` access token targeting a downstream API while preserving the same
9+
user identity. This is especially useful for `MCP` servers and other intermediary APIs that need to
10+
call downstream APIs on behalf of the user.
11+
12+
The following example verifies the incoming access token for your API, exchanges it for a token for the downstream API, and then calls the downstream API with the exchanged token.
13+
14+
```python
15+
import asyncio
16+
import httpx
17+
18+
from auth0_api_python import ApiClient, ApiClientOptions
19+
20+
async def exchange_on_behalf_of():
21+
api_client = ApiClient(ApiClientOptions(
22+
domain="your-tenant.auth0.com",
23+
audience="https://mcp-server.example.com",
24+
client_id="<AUTH0_CLIENT_ID>",
25+
client_secret="<AUTH0_CLIENT_SECRET>"
26+
))
27+
28+
incoming_access_token = "incoming-auth0-access-token"
29+
30+
claims = await api_client.verify_access_token(access_token=incoming_access_token)
31+
32+
result = await api_client.get_token_on_behalf_of(
33+
access_token=incoming_access_token,
34+
audience="https://calendar-api.example.com",
35+
scope="calendar:read calendar:write"
36+
)
37+
38+
async with httpx.AsyncClient() as client:
39+
downstream_response = await client.get(
40+
"https://calendar-api.example.com/events",
41+
headers={"Authorization": f"Bearer {result['access_token']}"}
42+
)
43+
44+
downstream_response.raise_for_status()
45+
46+
return {
47+
"user": claims["sub"],
48+
"data": downstream_response.json(),
49+
}
50+
51+
asyncio.run(exchange_on_behalf_of())
52+
```
53+
54+
> [!TIP] Production notes:
55+
> - Pass the raw access token to `get_token_on_behalf_of()`. Do not pass the full `Authorization` header or include the `Bearer ` prefix.
56+
> - Verify the incoming token for your API before exchanging it so your application rejects invalid or mis-targeted tokens early.
57+
> - The downstream `audience` must match an API identifier configured in your Auth0 tenant.
58+
> - `get_token_on_behalf_of()` only returns access-token-oriented fields. It does not expose `id_token` or `refresh_token`.
59+
60+
In the current implementation, `get_token_on_behalf_of()` forwards the incoming access token as
61+
the [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693#section-2.1) `subject_token` and relies on Auth0 to handle any DPoP-specific behavior for that token.
62+
63+
## Inspecting Delegation After Token Verification
64+
65+
When a downstream API or `MCP` server receives an access token that may have been issued through
66+
delegation, it can verify the token first and then inspect the `act` claim to identify the current
67+
actor for authorization and the full delegation chain for audit or attribution.
68+
69+
```python
70+
import asyncio
71+
import logging
72+
73+
from auth0_api_python import (
74+
ApiClient,
75+
ApiClientOptions,
76+
get_current_actor,
77+
get_delegation_chain,
78+
)
79+
80+
logger = logging.getLogger(__name__)
81+
82+
async def inspect_delegated_token():
83+
api_client = ApiClient(ApiClientOptions(
84+
domain="your-tenant.auth0.com",
85+
audience="https://calendar-api.example.com"
86+
))
87+
88+
access_token = "delegated-auth0-access-token"
89+
90+
claims = await api_client.verify_access_token(access_token=access_token)
91+
92+
current_actor = get_current_actor(claims)
93+
delegation_chain = get_delegation_chain(claims)
94+
95+
if current_actor != "mcp_server_client_id":
96+
raise PermissionError("unexpected actor")
97+
98+
logger.info(
99+
"delegated request",
100+
extra={
101+
"user_sub": claims["sub"],
102+
"current_actor": current_actor,
103+
"delegation_chain": delegation_chain,
104+
},
105+
)
106+
107+
return {
108+
"user_sub": claims["sub"],
109+
"current_actor": current_actor,
110+
"delegation_chain": delegation_chain,
111+
}
112+
113+
asyncio.run(inspect_delegated_token())
114+
```
115+
116+
Only the outermost `act.sub` represents the current actor and should be used for authorization
117+
decisions. Nested `act` values represent prior actors and are better suited for logging, audit, or
118+
attribution.
119+
5120
## Bearer Authentication
6121

7122
Bearer authentication is the standard OAuth 2.0 token authentication method.
@@ -157,4 +272,4 @@ async def verify_dpop_token(access_token, dpop_proof, http_method, http_url):
157272
"token_claims": token_claims,
158273
"proof_claims": proof_claims
159274
}
160-
```
275+
```

README.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,92 @@ except ApiError as e:
212212

213213
More info: https://auth0.com/docs/authenticate/custom-token-exchange
214214

215+
#### On Behalf Of Token Exchange
216+
217+
Use `get_token_on_behalf_of()` when your API receives an `Auth0` access token for itself and needs
218+
to exchange it for another `Auth0` access token targeting a downstream API while preserving the
219+
same user identity. This is especially useful for `MCP` servers and other intermediary APIs that
220+
need to call downstream APIs on behalf of the user.
221+
222+
The following example verifies the incoming access token for your API, exchanges it for a token for the downstream API, and then calls the downstream API with the exchanged token.
223+
224+
```python
225+
import httpx
226+
227+
async def handle_calendar_request(incoming_access_token: str):
228+
await api_client.verify_access_token(access_token=incoming_access_token)
229+
230+
result = await api_client.get_token_on_behalf_of(
231+
access_token=incoming_access_token,
232+
audience="https://calendar-api.example.com",
233+
scope="calendar:read calendar:write"
234+
)
235+
236+
async with httpx.AsyncClient() as client:
237+
downstream_response = await client.get(
238+
"https://calendar-api.example.com/events",
239+
headers={"Authorization": f"Bearer {result['access_token']}"}
240+
)
241+
242+
downstream_response.raise_for_status()
243+
244+
return downstream_response.json()
245+
```
246+
247+
The OBO wrapper reuses the existing RFC 8693 exchange support and fixes both token-type parameters
248+
to Auth0 access-token exchange. In the current implementation, the SDK forwards the incoming access
249+
token as the `subject_token` and relies on Auth0 to handle any DPoP-specific behavior for that token.
250+
The OBO result only includes access-token-oriented fields. It does not expose `id_token` or
251+
`refresh_token`.
252+
253+
#### Inspecting Delegation After Token Verification
254+
255+
When a downstream API or `MCP` server receives an access token that may have been issued through
256+
delegation, it can verify the token first and then inspect the `act` claim to identify the current
257+
actor for authorization and the full delegation chain for logging or audit.
258+
259+
```python
260+
import logging
261+
262+
from auth0_api_python import (
263+
ApiClient,
264+
ApiClientOptions,
265+
get_current_actor,
266+
get_delegation_chain,
267+
)
268+
269+
logger = logging.getLogger(__name__)
270+
271+
api_client = ApiClient(ApiClientOptions(
272+
domain="<AUTH0_DOMAIN>",
273+
audience="<AUTH0_AUDIENCE>",
274+
))
275+
276+
async def authorize_delegated_request(access_token: str):
277+
claims = await api_client.verify_access_token(access_token=access_token)
278+
279+
current_actor = get_current_actor(claims)
280+
delegation_chain = get_delegation_chain(claims)
281+
282+
if current_actor != "mcp_server_client_id":
283+
raise PermissionError("unexpected actor")
284+
285+
logger.info(
286+
"delegated request",
287+
extra={
288+
"user_sub": claims["sub"],
289+
"current_actor": current_actor,
290+
"delegation_chain": delegation_chain,
291+
},
292+
)
293+
294+
return claims
295+
```
296+
297+
Only the outermost `act.sub` represents the current actor and should be used for authorization
298+
decisions. Nested `act` values represent prior actors in the delegation chain and are better suited
299+
for logging, audit, or attribution.
300+
215301
#### Requiring Additional Claims
216302

217303
If your application demands extra claims, specify them with `required_claims`:
@@ -353,4 +439,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
353439
</p>
354440
<p align="center">
355441
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-api-python/LICENSE"> LICENSE</a> file for more info.
356-
</p>
442+
</p>

0 commit comments

Comments
 (0)