Skip to content

feat: Support IPSIE session_expiry claim#245

Open
tanya732 wants to merge 6 commits into
v2from
feat/add-ipsie-support
Open

feat: Support IPSIE session_expiry claim#245
tanya732 wants to merge 6 commits into
v2from
feat/add-ipsie-support

Conversation

@tanya732

@tanya732 tanya732 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds support for the IPSIE SL1 session_expiry ID token claim, which represents an absolute upper bound (Unix timestamp in seconds) on a session's lifetime.

The claim is asserted by the upstream IdP and emitted by Auth0 when the enterprise connection has id_token_session_expiry_supported: true.

Since this library is a stateless core (it does not own or manage application sessions), it provides the primitives required to enforce the session ceiling while leaving session persistence and redirect behavior to the application.

Specifically, this PR:

  • Reads and validates the session_expiry claim during the Authorization Code flow.
  • Stores the validated value on Tokens.
  • Exposes:
    • Tokens.getSessionExpiresAt()
    • Tokens.isSessionExpired()
    • Tokens.isSessionExpired(long leeway)
  • Allows applications to persist the value and check isSessionExpired() whenever a session is read, integrating with their existing redirect-to-login flow.

Changes

Tokens

  • Added an optional sessionExpiresAt field.
  • Added a new 8-argument constructor.
  • Added:
    • getSessionExpiresAt()
    • isSessionExpired()
    • isSessionExpired(long leeway)
  • Introduced a default 30-second clock-skew tolerance via DEFAULT_SESSION_EXPIRY_LEEWAY.
  • serialVersionUID remains unchanged, so previously serialized sessions deserialize with sessionExpiresAt == null, preserving backward compatibility.

RequestProcessor

Reads session_expiry from the verified ID token after token merge and validates it before stamping it onto Tokens.

Validation rules:

  • Non-numeric values are ignored ("no ceiling").
  • Millisecond-scale values (>= 10_000_000_000) are ignored to prevent a Post-Login Action that accidentally emits milliseconds from silently disabling enforcement.
  • If session_expiry <= iat, login fails instead of creating an already-expired session.

IdentityVerificationException

Added:

  • a0.session_expiry_in_past error code
  • isSessionExpiryError() helper

Documentation

Updated EXAMPLES.md with an IPSIE session_expiry section covering:

  • Persisting the value at login
  • Enforcing it on session reads
  • Default leeway behavior

Tests

Added coverage for:

  • RequestProcessorTest
    • Claim is stamped correctly
    • Missing claim
    • Past-iat validation failure
    • Millisecond guard
  • TokensTest

Semantics

  • null means no session ceiling and is never considered expired, making rollout backward compatible.
  • All comparisons use integer Unix seconds.
  • session_expiry is independent of both:
    • the token exp claim
    • application-defined idle or absolute session timeouts

Out of Scope / Follow-up

Refresh token enforcement

This library does not currently expose a refresh-token API, so the session ceiling is enforced only:

  • during login, and
  • when reading an existing session.

A TODO has been added to AuthenticationController documenting the intended behavior once the MRRT renewal flow is implemented:

  1. Reject grant_type=refresh_token requests once the session ceiling has passed, returning session_expired.
  2. Preserve the original login-time session_expiry across refreshes (write once; never re-derive it from refresh responses).

Documentation

Cross-linking from the session management documentation is not included in this PR.

@tanya732
tanya732 marked this pull request as ready for review July 3, 2026 10:35
@tanya732
tanya732 requested a review from a team as a code owner July 3, 2026 10:35
* @return {@code true} if a ceiling is present and {@code now >= sessionExpiresAt - leeway},
* {@code false} otherwise.
*/
public boolean isSessionExpired(long leewaySeconds) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check the ceiling on a session read, the app has to build a whole Tokens with seven nulls just to call this one method , the example does exactly that (new Tokens(null, null, null, "Bearer", null, null, null, sessionExpiresAt)). It's a lot of setup for a single comparison, and it puts a "has this expired" check on a class that otherwise just holds token values.

Would a small static helper that takes the stored value directly, something like Tokens.isSessionExpired(Long sessionExpiresAt, long leeway)be easier to call? WDYT?

return false;
}
long nowSeconds = Math.floorDiv(System.currentTimeMillis(), 1000L);
return nowSeconds >= sessionExpiresAt - leewaySeconds;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSessionExpired(long) documents leeway as non-negative but doesn't enforce it, and a negative value flips the comparison to sessionExpiresAt + x , pushing the ceiling later than wall-clock rather than earlier, which is the opposite of what an absolute upper bound should do.

Comment thread EXAMPLES.md

This library does not own a session. It reads and validates the claim at login, exposing it
via `Tokens.getSessionExpiresAt()` (seconds, or `null` when absent) and
`Tokens.isSessionExpired()`. Persisting the value and enforcing the ceiling is the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows how to persist and check the ceiling, but it doesn't mention that login itself can now fail: when the claim is at or before the token's issued-at time, handle() throws and isSessionExpiryError() returns true. For an app just turning on the connection option, that's a new error out of handle() they weren't catching before.

Could we add a short note here on catching that case and sending the user back to login, so the login-time behavior sits next to the read-time check?

}

@Test
public void shouldIgnoreSessionExpiryWhenValueIsInMilliseconds() throws Exception {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These cover missing, past-iat, and the millisecond case, but the plain malformed case isn't here: a session_expiry that isn't a number (a string, say) comes back as null from asLong() and should fall through to "no ceiling". Since the claim is developer-set, that's the input most likely to actually show up, and nothing yet checks that it fails open instead of locking someone out.

The zero/negative case is worth settling too, those are numbers, so they get past the range check and hit the at-or-before-iat throw, i.e. a lockout rather than "no ceiling". Shall we add a malformed-value test and decide which behavior we want there?


// Lockout guard: a session that is already past its ceiling at login must not be persisted.
Date issuedAt = decoded.getIssuedAt();
if (issuedAt != null && sessionExpiresAt <= Math.floorDiv(issuedAt.getTime(), 1000L)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The login guard here compares the ceiling against iat with no leeway, while the read-time check applies the 30s skew leeway against now. A ceiling landing a few seconds after iat clears this guard and gets stored, but the very first isSessionExpired() read then treats it as expired because the default leeway pulls it back, so the session is saved at login and bounced on the next read.

Should the login guard use the same leeway. Also, if a verified token somehow has no iat, this guard is skipped and any below-cutoff ceiling gets stored.

Comment thread EXAMPLES.md

When an enterprise connection has **"Use ID Token for Session Expiry"**
(`id_token_session_expiry_supported: true`) enabled, Auth0 adds a `session_expiry` claim to
the ID token: an absolute Unix timestamp (seconds) that caps how long the session may live,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section explains the claim is in seconds but doesn't warn about what happens when it isn't. Since the value comes from a customer Post-Login Action, the most common mistake is emitting milliseconds (a getTime() without the /1000), and the code treats anything at or above 10,000,000,000 as "no ceiling". So a milliseconds value doesn't push expiry thousands of years out, it quietly switches enforcement off, which is easy to ship without noticing.

Could we add a short warning that the claim must be an integer number of seconds, and that an out of range value is ignored rather than enforced? Refer here: Emitting the claim.

Comment thread EXAMPLES.md

This library does not own a session. It reads and validates the claim at login, exposing it
via `Tokens.getSessionExpiresAt()` (seconds, or `null` when absent) and
`Tokens.isSessionExpired()`. Persisting the value and enforcing the ceiling is the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing worth spelling out that a session owning SDK gets for free: since this library has no refresh API, an app that refreshes tokens itself has to run the same isSessionExpired() check before a refresh exchange, otherwise it can renew a token past the ceiling and defeat the point. The isSessionExpired() Javadoc mentions this, but the guide is where a developer wiring up their own refresh would actually look.

Shall we add a line here noting the ceiling should be checked before any refresh, not just on session reads?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants