feat: surface IPSIE session_expiry claim across Android, iOS and web#904
feat: surface IPSIE session_expiry claim across Android, iOS and web#904utkrishtsahu wants to merge 2 commits into
Conversation
WalkthroughAdds ChangesUpstream IdP session expiry
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
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
auth0_flutter/example/web/index.htmlis excluded by!**/example/**
📒 Files selected for processing (20)
auth0_flutter/EXAMPLES.mdauth0_flutter/README.mdauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.ktauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.ktauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.ktauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.ktauth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.ktauth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.ktauth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.ktauth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.ktauth0_flutter/darwin/auth0_flutter.podspecauth0_flutter/darwin/auth0_flutter/Package.swiftauth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swiftauth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swiftauth0_flutter/ios/auth0_flutter.podspecauth0_flutter/lib/src/web/extensions/credentials_extension.dartauth0_flutter/macos/auth0_flutter.podspecauth0_flutter/test/web/extensions/credentials_extension_test.dartauth0_flutter_platform_interface/lib/src/credentials.dartauth0_flutter_platform_interface/test/credentials_test.dart
| /// underlying native SDK treats the credentials as expired once this time is | ||
| /// reached and will not renew them past it, regardless of [expiresAt]. |
There was a problem hiding this comment.
📐 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.
| sessionExpiry: result['sessionExpiry'] == null | ||
| ? null | ||
| : DateTime.parse(result['sessionExpiry'] as String).toUtc(), |
There was a problem hiding this comment.
🩺 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
| fun Credentials.toMap(): Map<String, Any?> { | ||
| val scopes = this.scope?.split(" ") ?: listOf() | ||
| val formattedDate = this.expiresAt.toInstant().toString() |
There was a problem hiding this comment.
🩺 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/mainRepository: 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/kotlinRepository: 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 || trueRepository: 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
| final sessionExpiryClaim = claims['session_expiry']; | ||
| final sessionExpiry = sessionExpiryClaim is num | ||
| ? DateTime.fromMillisecondsSinceEpoch( | ||
| sessionExpiryClaim.toInt() * 1000, | ||
| isUtc: true, | ||
| ) | ||
| : null; |
There was a problem hiding this comment.
🎯 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'
doneRepository: 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'
fiRepository: 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.
| 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 |
There was a problem hiding this comment.
credentials exposes sessionExpiresAt can we not use it directly?
| } | ||
| ``` | ||
|
|
||
| > ⚠️ **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). |
There was a problem hiding this comment.
should we mention IPSIE as EA?
📋 Changes
Adds support for the IPSIE
session_expiryclaim — 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: trueenabled). 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
Credentialsmodel, and carries it across the platform-channel boundary on all three platforms.Public API
Credentials(auth0_flutter_platform_interface): new optional, nullableDateTime? sessionExpiryfield, wired through the constructor,fromMap, andtoMap. Additive and non-breaking —nullmeans "no ceiling" and is never treated as expired.credentials()raises the existingCredentialsManagerException(isNoCredentialsFound) — no new error type or handling code required.Serialization
Extensions.swiftdecodessession_expiry(Unix seconds → ISO-8601) inCredentials.asDictionary(); newsessionExpirycase inProperties.swift.Credentials.toMap()extension (CredentialsExtensions.kt) reading the claim fromuser.getExtraInfo(), used by the login, get, and renew handlers (deduplicates three copy-pasted maps).credentials_extension.dartdecodes the claim from the ID token.Dependency bumps (minor line that introduced the feature, not the next major)
Package.swift)web/index.html)Docs:
README.mdnote + new "Session expiry from an upstream IdP" section inEXAMPLES.md.📎 References
SDK-10259
🎯 Testing
Automated (added in this PR)
credentials_test.dart):sessionExpirypresent/absent viafromMap; ISO-8601 UTC output andnullomission viatoMap.credentials_extension_test.dart): claim decoded from a signed test token;nullwhen absent.GetCredentialsRequestHandlerTest/RenewCredentialsRequestHandlerTest): claim presence/absence, and preservation across renew (a refresh must not extend the ceiling);JwtTestUtilsextended with numeric claims.Run:
Manual (end-to-end)
id_token_session_expiry_supported: true).credentials.sessionExpiryis non-null and matches thesession_expiryclaim in the ID token.renewCredentials()and confirm the ceiling is unchanged (not pushed later).credentials()— confirm it raisesCredentialsManagerExceptionwithisNoCredentialsFound, and that a fresh login succeeds.Summary by CodeRabbit
New Features
sessionExpirytimestamp when provided.Documentation
Bug Fixes