Skip to content

feat: surface IPSIE session_expiry claim across Android, iOS and web - #904

Open
utkrishtsahu wants to merge 4 commits into
mainfrom
feat/ipse-session-expiry
Open

feat: surface IPSIE session_expiry claim across Android, iOS and web#904
utkrishtsahu wants to merge 4 commits into
mainfrom
feat/ipse-session-expiry

Conversation

@utkrishtsahu

@utkrishtsahu utkrishtsahu commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

Adds support for the IPSIE session_expiry claim — an absolute Unix-seconds ceiling asserted by an upstream IdP (over an enterprise connection with "Use ID Token for Session Expiry" / id_token_session_expiry_supported: true enabled). The claim acts as a hard ceiling on the local session: the app session can never outlive the upstream IdP session.

Enforcement itself lives in the native SDKs; this change bumps to the versions that ship it, surfaces the value through the Dart Credentials model, and carries it across the platform-channel boundary on all three platforms.

Public API

  • Credentials (auth0_flutter_platform_interface): new optional, nullable DateTime? sessionExpiry field, wired through the constructor, fromMap, and toMap. Additive and non-breaking — null means "no ceiling" and is never treated as expired.
    final credentials = await auth0.credentialsManager.credentials();
    final sessionExpiry = credentials.sessionExpiry; // null when the claim is absent
    if (sessionExpiry != null) {
      print('Upstream IdP session ends at: $sessionExpiry');
    }
  • Once the ceiling passes, the native SDK treats the stored credentials as expired and skips refresh, so credentials() raises the existing CredentialsManagerException (isNoCredentialsFound) — no new error type or handling code required.

Serialization

  • Darwin (iOS/macOS): Extensions.swift decodes session_expiry (Unix seconds → ISO-8601) in Credentials.asDictionary(); new sessionExpiry case in Properties.swift.
  • Android: new shared Credentials.toMap() extension (CredentialsExtensions.kt) reading the claim from user.getExtraInfo(), used by the login, get, and renew handlers (deduplicates three copy-pasted maps).
  • Web: credentials_extension.dart decodes the claim from the ID token.

Dependency bumps (minor line that introduced the feature, not the next major)

  • Auth0.swift → 2.24.1 (all podspecs + Package.swift)
  • auth0-spa-js → 2.22.0 (example web/index.html)
  • Auth0.Android already at 3.21.0 (≥ 3.20.0) — no change needed

Docs: README.md note + new "Session expiry from an upstream IdP" section in EXAMPLES.md.

📎 References

SDK-10259

🎯 Testing

Automated (added in this PR)

  • Platform interface (credentials_test.dart): sessionExpiry present/absent via fromMap; ISO-8601 UTC output and null omission via toMap.
  • Web (credentials_extension_test.dart): claim decoded from a signed test token; null when absent.
  • Android (GetCredentialsRequestHandlerTest / RenewCredentialsRequestHandlerTest): claim presence/absence, and preservation across renew (a refresh must not extend the ceiling); JwtTestUtils extended with numeric claims.

Run:

# platform interface
cd auth0_flutter_platform_interface && flutter test test/credentials_test.dart
# web
cd auth0_flutter && flutter test test/web/extensions/credentials_extension_test.dart --platform chrome
# android
cd auth0_flutter/example/android && ./gradlew :auth0_flutter:testDebugUnitTest

Manual (end-to-end)

  1. Configure an enterprise connection with an upstream IdP and enable "Use ID Token for Session Expiry" (id_token_session_expiry_supported: true).
  2. Log in on iOS, Android, and web; confirm credentials.sessionExpiry is non-null and matches the session_expiry claim in the ID token.
  3. Call renewCredentials() and confirm the ceiling is unchanged (not pushed later).
  4. Wait for the ceiling to pass, then call credentials() — confirm it raises CredentialsManagerException with isNoCredentialsFound, and that a fresh login succeeds.

Summary by CodeRabbit

  • New Features

    • Added support for upstream identity provider session expiry ceilings.
    • Credentials now expose an optional sessionExpiry timestamp when provided.
    • Session expiry is preserved across login, retrieval, renewal, and web authentication flows.
  • Documentation

    • Added guidance for configuring upstream session expiry and handling expired sessions.
    • Documented the web requirement for Auth0 SPA SDK 2.22.0 or later.
  • Bug Fixes

    • Improved consistency of credential data across Android, iOS, macOS, and web platforms.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • auth0_flutter/example/ios/Tests/Mocks.swift is excluded by !**/example/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 17dc10c1-ccdf-41ca-bedb-84d17e96a6ce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds Credentials.sessionExpiry support across the Dart, Android, iOS, macOS, and web implementations. ID-token session_expiry claims are converted to UTC values, serialized through credential responses, tested, and documented.

Changes

Upstream IdP session expiry

Layer / File(s) Summary
Credential session expiry contract
auth0_flutter_platform_interface/lib/src/credentials.dart, auth0_flutter_platform_interface/test/credentials_test.dart, auth0_flutter/darwin/.../Properties.swift
Adds optional Credentials.sessionExpiry support with UTC map serialization and deserialization.
Android credential propagation
auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/..., auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/...
Centralizes Android credential mapping, extracts numeric session_expiry, and covers login, retrieval, and renewal responses.
iOS, macOS, and web extraction
auth0_flutter/darwin/..., auth0_flutter/ios/..., auth0_flutter/macos/..., auth0_flutter/lib/src/web/..., auth0_flutter/test/web/...
Extracts the claim on native and web platforms, updates Auth0 dependencies to 2.24.1, and adds web tests.
Session expiry documentation
auth0_flutter/EXAMPLES.md, auth0_flutter/README.md
Documents the hard session ceiling, login handling, field access, and the auth0-spa-js 2.22.0+ requirement.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IdToken
  participant PlatformSDK
  participant CredentialsMapper
  participant FlutterCredentials
  IdToken->>PlatformSDK: provide session_expiry claim
  PlatformSDK->>CredentialsMapper: extract and convert Unix seconds
  CredentialsMapper->>FlutterCredentials: serialize sessionExpiry
Loading

Suggested reviewers: sanchitmehtagit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: surfacing the session_expiry claim across Android, iOS/macOS, and web.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ipse-session-expiry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@auth0_flutter_platform_interface/lib/src/credentials.dart`:
- Around line 57-58: Update the shared Credentials documentation near the
expiresAt description to refer to the “underlying platform SDK” rather than only
the native SDK, and explicitly distinguish native SDK expiration enforcement
from web renewal enforcement handled by auth0-spa-js in the web credentials
extension.
- Around line 91-93: Update the sessionExpiry conversion in the credentials
parsing flow to validate result['sessionExpiry'] with an is String check before
parsing or casting. Add a private helper if appropriate that returns null for
null, parses accepted strings to UTC DateTime, and throws a clear
FormatException for other value types; use this helper from the sessionExpiry
assignment.

In
`@auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt`:
- Around line 15-17: Update Credentials.toMap and the related
Instant.ofEpochSecond serialization path to avoid java.time APIs on minSdk 21,
or enable and configure core-library desugaring in the Android build settings.
Ensure credential date formatting/storage remains unchanged across supported API
levels, and apply the same compatibility fix to both referenced call sites.

In `@auth0_flutter/lib/src/web/extensions/credentials_extension.dart`:
- Around line 20-26: Update the sessionExpiryClaim local in the credentials
parsing flow to explicitly use the Object? type instead of relying on dynamic
inference from claims['session_expiry']; retain the existing is num guard and
DateTime conversion unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 83e4b42d-350f-4113-bd36-da4a951e8476

📥 Commits

Reviewing files that changed from the base of the PR and between 51d4a78 and a5b14b8.

⛔ Files ignored due to path filters (1)
  • auth0_flutter/example/web/index.html is excluded by !**/example/**
📒 Files selected for processing (20)
  • auth0_flutter/EXAMPLES.md
  • auth0_flutter/README.md
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt
  • auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt
  • auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.kt
  • auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.kt
  • auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.kt
  • auth0_flutter/darwin/auth0_flutter.podspec
  • auth0_flutter/darwin/auth0_flutter/Package.swift
  • auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift
  • auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift
  • auth0_flutter/ios/auth0_flutter.podspec
  • auth0_flutter/lib/src/web/extensions/credentials_extension.dart
  • auth0_flutter/macos/auth0_flutter.podspec
  • auth0_flutter/test/web/extensions/credentials_extension_test.dart
  • auth0_flutter_platform_interface/lib/src/credentials.dart
  • auth0_flutter_platform_interface/test/credentials_test.dart

Comment on lines +57 to +58
/// underlying native SDK treats the credentials as expired once this time is
/// reached and will not renew them past it, regardless of [expiresAt].

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document web enforcement separately.

These shared Credentials docs say the underlying native SDK enforces the ceiling, but auth0_flutter/lib/src/web/extensions/credentials_extension.dart relies on auth0-spa-js for web renewal. Use “underlying platform SDK” and distinguish native enforcement from web enforcement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter_platform_interface/lib/src/credentials.dart` around lines 57 -
58, Update the shared Credentials documentation near the expiresAt description
to refer to the “underlying platform SDK” rather than only the native SDK, and
explicitly distinguish native SDK expiration enforcement from web renewal
enforcement handled by auth0-spa-js in the web credentials extension.

Comment on lines +91 to +93
sessionExpiry: result['sessionExpiry'] == null
? null
: DateTime.parse(result['sessionExpiry'] as String).toUtc(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the dynamic value before casting.

A non-null, non-String value reaches as String and throws at the credential boundary. Validate with an is String check and fail explicitly for malformed values rather than using an unchecked cast.

As per path instructions, Dart code must use is checks or pattern matching before casts.

Suggested guard
-        sessionExpiry: result['sessionExpiry'] == null
-            ? null
-            : DateTime.parse(result['sessionExpiry'] as String).toUtc(),
+        sessionExpiry: _parseSessionExpiry(result['sessionExpiry']),

Add a private helper that returns null for null, accepts only String, and throws a clear FormatException otherwise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter_platform_interface/lib/src/credentials.dart` around lines 91 -
93, Update the sessionExpiry conversion in the credentials parsing flow to
validate result['sessionExpiry'] with an is String check before parsing or
casting. Add a private helper if appropriate that returns null for null, parses
accepted strings to UTC DateTime, and throws a clear FormatException for other
value types; use this helper from the sessionExpiry assignment.

Source: Path instructions

Comment on lines +15 to +17
fun Credentials.toMap(): Map<String, Any?> {
val scopes = this.scope?.split(" ") ?: listOf()
val formattedDate = this.expiresAt.toInstant().toString()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

rg -n \
  'coreLibraryDesugaring|isCoreLibraryDesugaringEnabled|compileOptions|minSdk' \
  auth0_flutter/android -g '*.gradle' -g '*.gradle.kts' -g '*.properties'

rg -n 'java\.time\.Instant|\.toInstant\(' \
  auth0_flutter/android/src/main

Repository: auth0/auth0-flutter

Length of output: 2740


🏁 Script executed:

#!/bin/bash
set -eu

printf '--- build files ---\n'
git ls-files 'auth0_flutter/android/*.gradle*' 'auth0_flutter/android/settings.gradle*' 'auth0_flutter/android/gradle.properties' | sort | xargs -r -n1 sh -c 'echo "### $0"; cat -n "$0"'

printf '\n--- relevant Kotlin snippets ---\n'
for f in auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MfaExtensions.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/SaveCredentialsRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetApiCredentialsRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithSMSCodeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/RenewApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasspassCredentialExchangeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithEmailCodeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasswordlessLoginWithOtpApiRequestHandler.kt
do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    sed -n '1,120p' "$f" | cat -n
  fi
done

printf '\n--- occurrences summary ---\n'
rg -n 'Date|toInstant|Instant' auth0_flutter/android/src/main/kotlin

Repository: auth0/auth0-flutter

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- build files ---'
git ls-files 'auth0_flutter/android/*.gradle*' 'auth0_flutter/android/settings.gradle*' 'auth0_flutter/android/gradle.properties' | sort | xargs -r -n1 sh -c 'echo "### $0"; cat -n "$0"'

printf '%s\n' ''
printf '%s\n' '--- relevant JavaTime occurrences ---'
for f in auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MfaExtensions.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/SaveCredentialsRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetApiCredentialsRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithSMSCodeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/RenewApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/CustomTokenExchangeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithEmailCodeApiRequestHandler.kt \
         auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasswordlessLoginWithOtpApiRequestHandler.kt
do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    sed -n '1,120p' "$f" | cat -n
  fi
done

printf '%s\n' ''
printf '%s\n' '--- occurrences summary ---'
rg -n 'Date|toInstant|Instant' auth0_flutter/android/src/main/kotlin || true

Repository: auth0/auth0-flutter

Length of output: 47531


Remove minSdk 21 Java Time calls or enable core-library desugaring.

Credentials.toMap() calls Date.toInstant() and java.time.Instant.ofEpochSecond() for credential serialization, but build.gradle only sets minSdkVersion 21 and has no core-library desugaring configured. Add coreLibraryDesugaringEnabled = true or replace these with API-21-safe formatting/storage paths.

Also applies to: 36-38

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt`
around lines 15 - 17, Update Credentials.toMap and the related
Instant.ofEpochSecond serialization path to avoid java.time APIs on minSdk 21,
or enable and configure core-library desugaring in the Android build settings.
Ensure credential date formatting/storage remains unchanged across supported API
levels, and apply the same compatibility fix to both referenced call sites.

Source: Path instructions

Comment on lines +20 to +26
final sessionExpiryClaim = claims['session_expiry'];
final sessionExpiry = sessionExpiryClaim is num
? DateTime.fromMillisecondsSinceEpoch(
sessionExpiryClaim.toInt() * 1000,
isUtc: true,
)
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant Dart file and any relevant project config.
printf '--- files ---\n'
git ls-files | rg '(^|/)credentials_extension\.dart$|auth0_flutter/|analysis_options' || true

printf '\n--- credentials_extension outline ---\n'
ast-grep outline auth0_flutter/lib/src/web/extensions/credentials_extension.dart || true

printf '\n--- credentials_extension content ---\n'
cat -n auth0_flutter/lib/src/web/extensions/credentials_extension.dart

printf '\n--- find claims parameter declarations/imports ---\n'
rg -n "credentials_extension|sessionExpiryClaim|session_expiry|claims" auth0_flutter/lib/src/web -S || true

printf '\n--- analyze options ---\n'
fd -a 'analysis_options.*yaml|pubspec.yaml' auth0_flutter | sort | xargs -r sh -c 'echo "### $1"; cat -n "$1"' sh {}

Repository: auth0/auth0-flutter

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files ---'
git ls-files | rg '(^|/)credentials_extension\.dart$|^auth0_flutter/|analysis_options' || true

printf '%s\n' '--- credentials_extension content ---'
if [ -f auth0_flutter/lib/src/web/extensions/credentials_extension.dart ]; then
  cat -n auth0_flutter/lib/src/web/extensions/credentials_extension.dart
else
  fd -a 'credentials_extension.dart' . | sed -n '1,20p'
fi

printf '%s\n' '--- find claims/session_expiry usages ---'
rg -n "credentials_extension|sessionExpiryClaim|session_expiry|claims\[" auth0_flutter/lib/src/web -S || true

printf '%s\n' '--- analyze/config files ---'
fd -a 'analysis_options.*yaml|pubspec.yaml' . | sort | sed -n '1,60p' | while IFS= read -r f; do
  echo "### $f"
  cat -n "$f" | sed -n '1,220p'
done

Repository: auth0/auth0-flutter

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' "--- relevant file contents ---"
if [ -f auth0_flutter/lib/src/web/extensions/credentials_extension.dart ]; then
  cat -n auth0_flutter/lib/src/web/extensions/credentials_extension.dart
else
  file="$(fd 'credentials_extension.dart' . | head -n 1 || true)"
  if [ -n "${file:-}" ]; then cat -n "$file"; fi
fi

printf '%s\n' "--- analyzer config for infer_dynamic ---"
cat -n auth0_flutter/analysis_options.yaml | sed -n '1,220p' | rg -n "strict-|explicit-callback-types|static" -C 3 || true

printf '%s\n' "--- Dart availability and static type probe if tool exists ---"
if command -v dart >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  cat > "$tmp/strict_inference.dart" <<'HEREDOC'
void main() {
  final claims = <String, Object?>{};
  final sessionExpiryClaim = claims['session_expiry'];
  final sessionExpiry = sessionExpiryClaim is num
      ? DateTime.fromMillisecondsSinceEpoch(sessionExpiryClaim.toInt() * 1000, isUtc: true)
      : null;
  print(sessionExpiry);
}
HEREDOC
  dart analyze --strict-inference "$tmp/strict_inference.dart" 2>&1 || true
else
  echo 'dart command not available'
fi

Repository: auth0/auth0-flutter

Length of output: 2671


Avoid inferred dynamic for the JWT claim.

This local is typed dynamic because of strict inference from the map lookup. Type it as Object?; the existing is num guard still promotes it safely.

Proposed fix
-    final sessionExpiryClaim = claims['session_expiry'];
+    final Object? sessionExpiryClaim = claims['session_expiry'];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final sessionExpiryClaim = claims['session_expiry'];
final sessionExpiry = sessionExpiryClaim is num
? DateTime.fromMillisecondsSinceEpoch(
sessionExpiryClaim.toInt() * 1000,
isUtc: true,
)
: null;
final Object? sessionExpiryClaim = claims['session_expiry'];
final sessionExpiry = sessionExpiryClaim is num
? DateTime.fromMillisecondsSinceEpoch(
sessionExpiryClaim.toInt() * 1000,
isUtc: true,
)
: null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth0_flutter/lib/src/web/extensions/credentials_extension.dart` around lines
20 - 26, Update the sessionExpiryClaim local in the credentials parsing flow to
explicitly use the Object? type instead of relying on dynamic inference from
claims['session_expiry']; retain the existing is num guard and DateTime
conversion unchanged.

Source: Path instructions

* UTC string. Returns `null` when the claim is absent or not numeric.
*/
private fun Credentials.sessionExpiryIso(): String? {
val claim = user.getExtraInfo()["session_expiry"] as? Number ?: return null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

credentials exposes sessionExpiresAt can we not use it directly?

Comment thread auth0_flutter/EXAMPLES.md
}
```

> ⚠️ **Upgrade note:** once this feature is enabled on your connection, `credentials()` can raise a "no credentials" error for a user who was previously logged in, when the ceiling is reached. If your app assumed `credentials()` always resolves after login, make sure it handles this case (it already does if you catch `CredentialsManagerException` as shown above).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we mention IPSIE as EA?

Comment thread auth0_flutter/EXAMPLES.md
final credentials = await auth0.credentialsManager.credentials();
// Use the credentials
} on CredentialsManagerException catch (e) {
if (e.isNoCredentialsFound) {

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 guide tells developers to branch on isNoCredentialsFound when the ceiling is reached, but that check doesn't actually match the ceiling error on mobile. On Android the native SDK raises a distinct SESSION_EXPIRED error whose message ("The session has reached the session_expiry ceiling…") comes through as the exception code, and on iOS it raises its own session-expired case that the platform channel maps to UNKNOWN. isNoCredentialsFound only matches NO_CREDENTIALS, so on both platforms this catch quietly falls through and the app never runs its re-login path, which is the whole point of the feature.

Can we surface the session-expired case as its own signal (say an isSessionExpired on the exception) and have the example branch on that? Right now there's nothing in the Dart layer that maps it.

// here we surface the value so app code can read
// `credentials.sessionExpiry`.
final sessionExpiryClaim = claims['session_expiry'];
final sessionExpiry = sessionExpiryClaim is num

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There's no upper-bound check on the claim here before turning it into a date. The native layers treat a session_expiry at or above 10,000,000,000 as "no ceiling" (the value is almost certainly milliseconds, since a real seconds value that large is year 2286+), so enforcement returns null for it. Here we'd still build a far-future DateTime and hand it back, so credentials.sessionExpiry disagrees with what's actually enforced. The claim is author-set in a Post-Login Action, so a forgotten seconds conversion is a real case.

Shall we apply the same bound the native SDKs use so a millisecond value reads as "no ceiling" here too? The same gap exists on the Android and iOS decoders.

* UTC string. Returns `null` when the claim is absent or not numeric.
*/
private fun Credentials.sessionExpiryIso(): String? {
val claim = user.getExtraInfo()["session_expiry"] as? Number ?: return null

@kishore7snehil kishore7snehil Jul 28, 2026

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 reads session_expiry off the current ID token's claims, but the native Credentials already exposes sessionExpiresAt, and that property returns the value pinned at the initial login, which is the value the credentials manager actually enforces. The two can diverge: after a refresh whose ID token omits the claim (or re-emits a different one), the pinned ceiling still enforces while the live token no longer carries it, so what we surface here would drift from what the SDK is enforcing. It also means the ms-vs-seconds bound the native side applies is bypassed.

Can we read sessionExpiresAt directly instead of decoding the claim ourselves? That keeps the surfaced value in step with enforcement and picks up the native validation for free.

// upstream IdP. Surfaced to the Dart layer as an ISO-8601 string so app
// code can read `credentials.sessionExpiry`; enforcement itself is done
// by the native CredentialsManager. Absent claim => omit (no ceiling).
if let sessionExpiry = (jwt.body["session_expiry"] as? NSNumber)?.doubleValue {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as on Android: this decodes the claim off the current ID token, but the native Credentials already exposes sessionExpiresAt, which reflects the value pinned at login (the one the CredentialsManager enforces) and applies the millisecond range check. Decoding the live token here can drift from the enforced ceiling after a refresh and skips that range check.

Can we read the native sessionExpiresAt here instead of parsing jwt.body ourselves?

/// enabled, or the session predates the feature). A `null` value means
/// **no ceiling** and must never be treated as an already-expired session.
///
/// [Read more about upstream IdP session expiry](https://auth0.com/docs).

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 links to the docs root rather than a page about the feature.

let testIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiYmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5"
+ "jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGljdHVyZSIsInVwZGF0ZWRfYXQiOiIyMDI"
+ "yLTA0LTE1VDAzOjE1OjUxLjc4N1oifQ.mFq-johzLTFQUAl9pjgQraTM6I8AGfcEcWBg0Ah2vss"
+ "yLTA0LTE1VDAzOjE1OjUxLjc4N1oiLCJzZXNzaW9uX2V4cGlyeSI6MzI1MDM2ODAwMDB9.sig"

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 adds session_expiry to the shared token, but there's no iOS test asserting the value actually lands in the credentials map, unlike the Android, web, and Dart sides which each got present/absent coverage. Worth a toMap/asDictionary case in the extensions tests so the iOS mapping is pinned too. (The value baked in here, 32503680000, is also a 13-ish digit value that the native range check would reject as milliseconds, so it's not a great stand-in for a real ceiling.)

Comment thread auth0_flutter/EXAMPLES.md

> 💡 You do not need to call `credentialsManager.storeCredentials()` afterward. The Credentials Manager automatically persists the renewed credentials.

### 🕒 Session expiry from an upstream IdP

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this section carry an availability note? This SDK is targeted later than the ones that shipped the feature first, and the underlying native support went out as early access, so it may be worth telling developers where this stands rather than reading as fully GA.

'scopes': scopes.toList(),
'userProfile': user.toMap(),
'tokenType': tokenType,
'sessionExpiry': sessionExpiry?.toUtc().toIso8601String(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

toMap always writes the sessionExpiry key, so when it's null the map carries sessionExpiry: null, while the Android side omits the key entirely when there's no claim. fromMap reads both fine, so nothing breaks, but the two sides disagree on shape if anything ever checks for key presence.

"tokenType" to credentials.type
)
)
result.success(credentials.toMap())

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 path now goes through the shared toMap(), but there's no test covering sessionExpiry on the web-auth login result the way GetCredentials and RenewCredentials got. Worth a present/absent case here too so the login path is pinned like the others.

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.

3 participants