Skip to content

Commit 9255731

Browse files
committed
add examples
1 parent d1683a7 commit 9255731

4 files changed

Lines changed: 98 additions & 4 deletions

File tree

EXAMPLES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Allowing clock skew for token validation](#allow-a-clock-skew-for-token-validation)
77
- [Changing the OAuth response_type](#changing-the-oauth-response_type)
88
- [HTTP logging](#http-logging)
9+
- [IPSIE session_expiry (upstream IdP session ceiling)](#ipsie-session_expiry-upstream-idp-session-ceiling)
910

1011
## Including additional authorization parameters
1112

@@ -231,3 +232,39 @@ Once you have created the instance of the `AuthenticationController`, you can en
231232
```java
232233
authController.setLoggingEnabled(true);
233234
```
235+
236+
## IPSIE session_expiry (upstream IdP session ceiling)
237+
238+
When an enterprise connection has **"Use ID Token for Session Expiry"**
239+
(`id_token_session_expiry_supported: true`) enabled, Auth0 adds a `session_expiry` claim to
240+
the ID token: an absolute Unix timestamp (seconds) that caps how long the session may live,
241+
independent of the `exp` token lifetime.
242+
243+
This library does not own a session. It reads and validates the claim at login, exposing it
244+
via `Tokens.getSessionExpiresAt()` (seconds, or `null` when absent) and
245+
`Tokens.isSessionExpired()`. Persisting the value and enforcing the ceiling is the
246+
application's job.
247+
248+
Persist it at login alongside the tokens (`null` means "no ceiling", store as-is):
249+
250+
```java
251+
Tokens tokens = authenticationController.handle(request, response);
252+
request.getSession().setAttribute("sessionExpiresAt", tokens.getSessionExpiresAt());
253+
```
254+
255+
On every session read, rebuild a `Tokens` and check the ceiling. When it returns `true`,
256+
drop the session and fall through to your existing redirect-to-login path:
257+
258+
```java
259+
HttpSession session = request.getSession();
260+
Tokens tokens = new Tokens(null, null, null, "Bearer", null, null, null,
261+
(Long) session.getAttribute("sessionExpiresAt"));
262+
263+
if (tokens.isSessionExpired()) {
264+
session.invalidate();
265+
response.sendRedirect("/login");
266+
}
267+
```
268+
269+
`isSessionExpired()` applies a 30s negative leeway for clock skew; pass
270+
`isSessionExpired(0)` for an exact comparison.

src/main/java/com/auth0/AuthenticationController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,17 @@ public Tokens handle(HttpServletRequest request, HttpServletResponse response) t
360360
return requestProcessor.process(request, response);
361361
}
362362

363+
// TODO(IPSIE Req 3 — refresh-token ceiling): this branch has no renew/refresh-token API, so the
364+
// session_expiry ceiling is only stamped at login (see RequestProcessor#withSessionExpiry). When
365+
// the refresh-token grant (renewAuth) is merged from the MRRT work, it must:
366+
// 1. Gate the refresh: refuse to exchange grant_type=refresh_token once the persisted ceiling
367+
// has passed (Tokens#isSessionExpired()) and surface a0.session_expired instead of calling
368+
// /oauth/token — the renewed access token must never outlive the IdP session ceiling.
369+
// 2. Preserve the ceiling: a refresh response without a fresh session_expiry claim must carry
370+
// forward the original sessionExpiresAt rather than dropping it to null (no ceiling). Only a
371+
// newly-emitted, valid session_expiry should replace it.
372+
// Until then, enforcement is login-time only; the example-app demonstrates the gate manually.
373+
363374
/**
364375
* Pre builds an Auth0 Authorize Url with the given redirect URI using a random state and a random nonce if applicable.
365376
*

src/main/java/com/auth0/RequestProcessor.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class RequestProcessor {
4949
private static final String KEY_FORM_POST = "form_post";
5050
private static final String KEY_MAX_AGE = "max_age";
5151

52+
// Upper bound for a valid session_expiry (Unix seconds). Anything at/above this is treated as
53+
// "no ceiling": it is almost certainly a milliseconds-since-epoch value emitted by mistake,
54+
// which would otherwise read as a date thousands of years out and silently disable enforcement.
55+
// Per the IPSIE Decision Log, reject anything >= 10,000,000,000.
56+
private static final long MAX_SESSION_EXPIRY_SECONDS = 10_000_000_000L;
57+
5258
private final DomainProvider domainProvider;
5359
private final String responseType;
5460
private final String clientId;
@@ -314,13 +320,16 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
314320
* session ceiling on subsequent reads.
315321
* <p>
316322
* The claim is an integer Unix timestamp (seconds since epoch). When it is absent the tokens
317-
* are returned unchanged (no ceiling). As a lockout guard, if the ceiling is already in the
318-
* past relative to the token's {@code iat}, the login is rejected rather than producing an
319-
* already-expired session.
323+
* are returned unchanged (no ceiling). The value is developer-controlled (it may be stamped by a
324+
* Post-Login Action), so it is validated rather than trusted: a non-numeric value, or one large
325+
* enough to be milliseconds-since-epoch ({@code >= 10_000_000_000}), is treated as "no ceiling"
326+
* rather than silently disabling enforcement with a date thousands of years out. As a lockout
327+
* guard, if the ceiling is already in the past relative to the token's {@code iat}, the login is
328+
* rejected rather than producing an already-expired session.
320329
*
321330
* @param tokens the merged tokens whose ID token is inspected.
322331
* @return the same tokens augmented with {@code sessionExpiresAt}, or {@code tokens} unchanged
323-
* when no {@code session_expiry} claim is present.
332+
* when no usable {@code session_expiry} claim is present.
324333
* @throws IdentityVerificationException if {@code session_expiry <= iat}.
325334
*/
326335
private Tokens withSessionExpiry(Tokens tokens) throws IdentityVerificationException {
@@ -341,6 +350,13 @@ private Tokens withSessionExpiry(Tokens tokens) throws IdentityVerificationExcep
341350
return tokens;
342351
}
343352

353+
// Range guard: reject milliseconds-since-epoch (or any absurdly large value). A value
354+
// accidentally emitted in milliseconds would read as a date ~thousands of years out and
355+
// silently switch off enforcement, so treat anything at/above this bound as "no ceiling".
356+
if (sessionExpiresAt >= MAX_SESSION_EXPIRY_SECONDS) {
357+
return tokens;
358+
}
359+
344360
// Lockout guard: a session that is already past its ceiling at login must not be persisted.
345361
Date issuedAt = decoded.getIssuedAt();
346362
if (issuedAt != null && sessionExpiresAt <= Math.floorDiv(issuedAt.getTime(), 1000L)) {

src/test/java/com/auth0/RequestProcessorTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,36 @@ public void shouldThrowWhenSessionExpiryIsAtOrBeforeIssuedAt() throws Exception
574574
assertThat(e.isSessionExpiryError(), is(true));
575575
}
576576

577+
@Test
578+
public void shouldIgnoreSessionExpiryWhenValueIsInMilliseconds() throws Exception {
579+
when(mockDomainProvider.getDomain(any())).thenReturn(DOMAIN);
580+
581+
long iat = nowSeconds() - 60;
582+
// An Action that forgot to convert to seconds: a millisecond-scale value reads as a date
583+
// thousands of years out and would silently disable enforcement. Treat as "no ceiling".
584+
long millisecondValue = (nowSeconds() + 3600) * 1000L;
585+
String idToken = signedIdToken(iat, millisecondValue);
586+
587+
Map<String, Object> params = new HashMap<>();
588+
params.put("code", "abc123");
589+
params.put("state", "1234");
590+
MockHttpServletRequest request = getRequest(params);
591+
request.setCookies(new Cookie("com.auth0.state", "1234"));
592+
593+
when(mockTokenHolder.getIdToken()).thenReturn(idToken);
594+
when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder);
595+
when(mockTokenRequest.execute()).thenReturn(mockTokenResponse);
596+
when(mockAuthAPI.exchangeCode(eq("abc123"), anyString())).thenReturn(mockTokenRequest);
597+
598+
RequestProcessor handler = createDefaultRequestProcessor();
599+
RequestProcessor spy = spy(handler);
600+
doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString());
601+
602+
Tokens tokens = spy.process(request, response);
603+
604+
assertThat(tokens.getSessionExpiresAt(), is(nullValue()));
605+
}
606+
577607
// --- AuthorizeUrl Building Tests ---
578608

579609
@Test

0 commit comments

Comments
 (0)