Skip to content

Commit 399485c

Browse files
authored
Merge branch 'main' into feat/passwordless-support
2 parents ef30738 + ba0a2a5 commit 399485c

8 files changed

Lines changed: 78 additions & 25 deletions

File tree

README.md

Lines changed: 2 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
```

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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ from auth0_server_python.error import CustomTokenExchangeError
201201
result = await auth0.request_session_transfer_token(
202202
subject_token=subject_token, # your proof of which customer to impersonate
203203
subject_token_type="urn:acme:customer-subject",
204-
organization=None, # optional; forwarded to the redirect
204+
organization=None, # optional, sent on the mint request (separate from the redirect)
205205
store_options={"request": request, "response": None},
206206
)
207207

@@ -216,7 +216,14 @@ return RedirectResponse(redirect_url) # your framework performs the redire
216216

217217
> **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.
218218
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. It must be an **unexpired, asymmetrically-signed JWT** (RS256 or PS256) - an Auth0 session ID token satisfies this; an HS256 or expired token is rejected by the server.
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.
220227
>
221228
> ```python
222229
> result = await auth0.request_session_transfer_token(
@@ -229,15 +236,15 @@ return RedirectResponse(redirect_url) # your framework performs the redire
229236
230237
### Target: forward the STT to `/authorize`
231238
232-
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 the STT was issued in an org context) straight through:
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:
233240
234241
```python
235242
from auth0_server_python.auth_types import StartInteractiveLoginOptions
236243
237244
url = await auth0.start_interactive_login(
238245
StartInteractiveLoginOptions(authorization_params={
239246
"session_transfer_token": request.query_params["session_transfer_token"],
240-
# "organization": org, # when the STT was issued in an org context
247+
# "organization": org, # when you want the target login org-scoped
241248
}),
242249
store_options={"request": request, "response": None},
243250
)

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

src/auth0_server_python/auth_server/server_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2882,6 +2882,7 @@ async def request_session_transfer_token(
28822882
28832883
Raises:
28842884
CustomTokenExchangeError: If no actor can be resolved or the exchange fails
2885+
InvalidArgumentError: If organization is provided but blank
28852886
"""
28862887
try:
28872888
# Validate the subject up front - before any session read/refresh/network.
@@ -2896,6 +2897,9 @@ async def request_session_transfer_token(
28962897
"subject_token_type cannot be empty or whitespace-only"
28972898
)
28982899

2900+
if organization is not None and not organization.strip():
2901+
raise InvalidArgumentError("organization", "organization must not be blank")
2902+
28992903
actor_token, actor_token_type = await self._resolve_actor_token(
29002904
actor_token, actor_token_type, store_options)
29012905

@@ -2924,7 +2928,7 @@ async def request_session_transfer_token(
29242928
token_type=response.token_type,
29252929
scope=response.scope,
29262930
)
2927-
except (CustomTokenExchangeError, ApiError):
2931+
except (CustomTokenExchangeError, InvalidArgumentError, ApiError):
29282932
raise
29292933
except Exception as e:
29302934
raise CustomTokenExchangeError(

src/auth0_server_python/tests/test_server_client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4227,6 +4227,51 @@ def test_build_session_transfer_redirect_rejects_blank_organization():
42274227
"https://app.example.com/auth/login", _stt_result(), organization=" ")
42284228

42294229

4230+
@pytest.mark.asyncio
4231+
async def test_request_session_transfer_token_forwards_organization_on_mint(mocker):
4232+
"""A provided organization is forwarded onto the mint request."""
4233+
client, post_mock = _stt_client(mocker)
4234+
4235+
await client.request_session_transfer_token(
4236+
subject_token="subj", subject_token_type="urn:acme:sub", actor_token="a",
4237+
organization="org_abc123",
4238+
)
4239+
4240+
assert post_mock.post.call_args[1]["data"]["organization"] == "org_abc123"
4241+
4242+
4243+
@pytest.mark.asyncio
4244+
async def test_request_session_transfer_token_omits_organization_when_absent(mocker):
4245+
"""No organization passed → the parameter is absent from the mint request, not empty."""
4246+
client, post_mock = _stt_client(mocker)
4247+
4248+
await client.request_session_transfer_token(
4249+
subject_token="subj", subject_token_type="urn:acme:sub", actor_token="a",
4250+
)
4251+
4252+
assert "organization" not in post_mock.post.call_args[1]["data"]
4253+
4254+
4255+
@pytest.mark.asyncio
4256+
async def test_request_session_transfer_token_rejects_blank_organization_before_refresh(mocker):
4257+
"""A blank organization is rejected before the expired session is refreshed or persisted."""
4258+
client, post_mock = _stt_client(mocker)
4259+
client._state_store.get.return_value = {"id_token": "stale", "refresh_token": "rt"}
4260+
usable = mocker.patch.object(client, "_is_id_token_usable")
4261+
refresh = mocker.patch.object(client, "get_token_by_refresh_token")
4262+
4263+
with pytest.raises(InvalidArgumentError):
4264+
await client.request_session_transfer_token(
4265+
subject_token="subj", subject_token_type="urn:acme:sub",
4266+
organization=" ",
4267+
)
4268+
4269+
refresh.assert_not_called()
4270+
usable.assert_not_called()
4271+
client._state_store.set.assert_not_called()
4272+
post_mock.post.assert_not_called()
4273+
4274+
42304275
@pytest.mark.asyncio
42314276
async def test_request_session_transfer_token_surfaces_server_issued_token_type(mocker):
42324277
"""A non-STT issued_token_type is surfaced verbatim, never fabricated as the STT URN."""

0 commit comments

Comments
 (0)