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 4 commits into
Conversation
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 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?
| final credentials = await auth0.credentialsManager.credentials(); | ||
| // Use the credentials | ||
| } on CredentialsManagerException catch (e) { | ||
| if (e.isNoCredentialsFound) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
This links to the docs root rather than a page about the feature.
| let testIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiYmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5" | ||
| + "jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGljdHVyZSIsInVwZGF0ZWRfYXQiOiIyMDI" | ||
| + "yLTA0LTE1VDAzOjE1OjUxLjc4N1oifQ.mFq-johzLTFQUAl9pjgQraTM6I8AGfcEcWBg0Ah2vss" | ||
| + "yLTA0LTE1VDAzOjE1OjUxLjc4N1oiLCJzZXNzaW9uX2V4cGlyeSI6MzI1MDM2ODAwMDB9.sig" |
There was a problem hiding this comment.
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.)
|
|
||
| > 💡 You do not need to call `credentialsManager.storeCredentials()` afterward. The Credentials Manager automatically persists the renewed credentials. | ||
|
|
||
| ### 🕒 Session expiry from an upstream IdP |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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.
📋 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