You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* PoC: enforce upstream IdP session_expiry ceiling for enterprise connections
* feat: reject already-expired sessions at login and generalize epoch claim extraction
Add a login-time lockout guard to complete_interactive_login: when the upstream
IdP asserts a session_expiry ceiling already in the past at login (compared
against the ID token iat with the same 30s leeway as read-time enforcement),
raise the new flow-agnostic SessionExpiredError instead of persisting an
already-expired session. A missing claim stays a no-op, preserving existing
behavior.
Generalize extract_session_expiry into extract_epoch_claim(claims, name),
reused for both session_expiry and iat, and rename the ceiling predicates to
is_session_ceiling_reached (read-time) and is_session_ceiling_in_past (login).
Document the login rejection in the README, RetrievingData guide, and the
ipsie-webapp example.
* refactor: read session_expiry/iat as raw claims and trust the platform boundary
The signature-verified, Auth0-issued ID token has already been validated
upstream (the platform refuses to emit a malformed session_expiry), so the
SDK reads it like every other operational claim instead of running a bespoke
validator. Removes State.extract_epoch_claim and reads session_expiry/iat
with a plain .get() at both extraction sites; the None guards in the ceiling
comparison functions still deliver the absent/null "no ceiling" safe default.
Production-reachable inputs (absent/null, clean integer) are unchanged; only
unreachable malformed values change behavior, now failing closed rather than
being silently accepted.
* fix: clean up spent transaction on login lockout and handle iat=0
Delete the transaction before raising SessionExpiredError at the login
guard — the authorization code was already exchanged, so the transaction
is spent and cannot be reused. Also treat iat=0 as a valid issued-at
reference (use `is not None` rather than truthiness) in the ceiling
lapsed check.
* refactor: validate session_expiry in UserClaims and link Action docs
Move the session_expiry plausibility check (type, range, ms-vs-seconds
upper bound) into the UserClaims pydantic validator so both login
extraction paths read an already-sanitized value. Point the example doc
to official Auth0 docs for the Post-Login Action setup instead of
embedding the snippet.
* Fix formatting in server_client.py and ensure newline at end of test_server_client.py
* fix: read session_expiry only from verified ID token and unify ceiling error
Address PoC review findings carried into the implementation:
- Stop reading session_expiry/iat from the unverified userinfo branch in
complete_interactive_login; derive the ceiling only from the
signature-and-issuer-verified ID token. Add a regression test that drives
the userinfo branch and asserts no ceiling is persisted.
- Raise the flow-agnostic SessionExpiredError at the get_access_token
ceiling check so login and read-time enforcement use one error type.
- Import field_validator in auth_types so the UserClaims sanitizer loads.
- Correct the refresh-path comment in helpers to describe carrying the
pinned ceiling forward rather than a non-existent overwrite guard.
Copy file name to clipboardExpand all lines: README.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -177,6 +177,12 @@ The SDK handles per-domain OIDC discovery, JWKS fetching, issuer validation, and
177
177
178
178
For more details and examples, see [examples/MultipleCustomDomains.md](examples/MultipleCustomDomains.md).
179
179
180
+
### 6. Session Expiry from the Upstream IdP
181
+
182
+
For enterprise connections, the upstream identity provider can cap how long a user's session lives. When the connection is configured to honor it, Auth0 includes a `session_expiry` claim in the ID token, and the SDK enforces this ceiling on every session read. Once it is reached, `get_user()` and `get_session()` return `None`, and `get_access_token()` raises an `AccessTokenError` with code `session_expired`. If the asserted ceiling is already in the past at login, `complete_interactive_login()` raises a `SessionExpiredError` instead of persisting an already-expired session.
183
+
184
+
For more details and examples, see [examples/RetrievingData.md](examples/RetrievingData.md#session-expiry-from-the-upstream-idp).
Read more above in [Configuring the Store](./ConfigureStore.md).
72
72
73
+
## Session Expiry from the Upstream IdP
74
+
75
+
For enterprise connections, the upstream identity provider can impose a ceiling on how long the user's session may live. This ceiling is delivered to the SDK as a `session_expiry` claim (an absolute Unix timestamp, **in seconds**) on the ID token. The SDK reads this value at login, stores it with the session, and enforces it on every subsequent read.
76
+
77
+
### Emitting the claim
78
+
79
+
The `session_expiry` claim is set by a Post-Login Action on your tenant, and **must** be an absolute Unix timestamp in **seconds**, not milliseconds. For the canonical Action setup, see the [Auth0 documentation](https://auth0.com/docs) (will be adding the link to the session_expiry Action guide once published).
80
+
81
+
> [!WARNING]
82
+
> `session_expiry` is interpreted as **seconds** since the Unix epoch (per RFC 7519 `NumericDate`). A common mistake is emitting milliseconds (e.g. `getTime()` without `/ 1000`). The SDK rejects implausibly large values (anything at or above `10_000_000_000`, ≈ year 2286) as malformed and treats them as **no ceiling**, so a milliseconds value will silently disable enforcement rather than expiring the session ~55,000 years from now. Always divide by 1000.
83
+
>
84
+
> Because the claim is authored by your Action (untrusted input), the SDK **fails open** on any malformed value — a non-numeric, zero, negative, boolean, or millisecond value is treated as "no ceiling" and login proceeds normally. Only a clean, future, seconds timestamp is enforced.
85
+
86
+
Once the ceiling is reached, the read methods behave as follows:
87
+
88
+
-`get_user()` returns `None`, as if no session exists.
89
+
-`get_session()` returns `None`, as if no session exists.
90
+
-`get_access_token()` raises an `AccessTokenError` with code `session_expired`.
91
+
92
+
`get_access_token_for_connection()` (Token Vault) is **not** gated by the session ceiling — connection tokens follow the upstream IdP's own `expires_in`, so they remain retrievable from cache even after the session ceiling has passed.
93
+
94
+
```python
95
+
from auth0_server_python.error import AccessTokenError, AccessTokenErrorCode
if error.code == AccessTokenErrorCode.SESSION_EXPIRED:
101
+
# The upstream session ceiling has been reached; start a new login.
102
+
...
103
+
```
104
+
105
+
When the ceiling is reached, the SDK deletes the stored session before returning, so the next request starts clean.
106
+
107
+
If the upstream IdP asserts a ceiling that is already in the past at login time, `complete_interactive_login()` raises a `SessionExpiredError` rather than persisting an already-expired session:
108
+
109
+
```python
110
+
from auth0_server_python.error import SessionExpiredError
# The session was already past its ceiling on arrival; start a new login.
116
+
...
117
+
```
118
+
119
+
> [!NOTE]
120
+
> **Upgrading:** with this feature enabled, `get_user()` and `get_session()` can return `None` for a user who was previously logged in, once the upstream ceiling passes. Applications that assumed these always return a value after login should add a null check and route the user back through login.
121
+
122
+
The `session_expiry` value is also surfaced through the user claims, so you can read it without triggering enforcement:
123
+
124
+
```python
125
+
user =await server_client.get_user(store_options=store_options)
126
+
session_expires_at = (user or {}).get("session_expiry")
127
+
```
128
+
129
+
> [!NOTE]
130
+
> Enforcement applies a small negative leeway (30 seconds) to account for clock skew, so a session is treated as expired slightly before the exact `session_expiry` timestamp. The refresh-token grant preserves the original ceiling - refreshing an access token does not extend the upstream session.
131
+
73
132
## Multi-Resource Refresh Tokens (MRRT)
74
133
75
134
Multi-Resource Refresh Tokens allow using a single refresh token to obtain access tokens for multiple audiences, simplifying token management in applications that interact with multiple backend services.
0 commit comments