Skip to content

fix(predictions): fix Face Liveness with temporary credentials on iOS 26 (URL signing + Sendable credentials) - #4248

Merged
thisisabhash merged 2 commits into
mainfrom
fix/url-signing-sigv4
Jul 27, 2026
Merged

fix(predictions): fix Face Liveness with temporary credentials on iOS 26 (URL signing + Sendable credentials)#4248
thisisabhash merged 2 commits into
mainfrom
fix/url-signing-sigv4

Conversation

@thisisabhash

@thisisabhash thisisabhash commented Jul 13, 2026

Copy link
Copy Markdown
Member

Issue

Fixes aws-amplify/amplify-ui-swift-liveness#225

FaceLiveness fails with FaceLivenessDetectionError.unknown (Rekognition returns UnrecognizedClientException, websocket close code 4005) when the caller supplies temporary credentials via a custom AWSCredentialsProvider. Long-term credentials are unaffected.

Root Cause

Two independent bugs contributed to signing failures with temporary credentials:

1. SigV4 URL signing

The canonical query string was built from url.query, which drops/round-trips query items incorrectly, and the query-item split on = used the default (unbounded) maxSplits, which corrupted values that themselves contain = (e.g. base64 padding in a X-Amz-Security-Token). This produced an invalid signature.

  • Build the canonical query from URLComponents.queryItems so existing items are preserved verbatim.
  • Split each key=value pair with maxSplits: 1 so values containing = survive percent-encoding intact.

2. Actor-isolated AWSTemporaryCredentials conformance

credential(from:) in LivenessCredentials.swift extracts the session token with:

sessionToken: (credentials as? AWSTemporaryCredentials)?.sessionToken

When a caller declares their credential type in a target that uses SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (an increasingly common default in new Xcode projects) and the type does not opt into Sendable/nonisolated, its conformance to AWSTemporaryCredentials becomes actor-isolated to MainActor. The as? cast is performed from a nonisolated async context, so it fails and returns nil, silently dropping the session token.

This was confirmed with a minimal reproduction: a MainActor-isolated credential struct cast from a nonisolated function returns nil for the session token, even though the value carries it.

Making AWSCredentials inherit from Sendable prevents the conformance from becoming actor-isolated, so the cast succeeds from any isolation context. This is also semantically correct — credentials are returned from an async provider and cross concurrency boundaries, so they should be Sendable.

Changes

  • SigV4Signer.swift: build canonical query from URLComponents.queryItems; use maxSplits: 1 when splitting query pairs.
  • AuthAWSCredentialsProvider.swift: public protocol AWSCredentials: Sendable.

Testing

  • Added SigV4URLSigningTestCase covering query strings with =-containing values and session tokens.
  • Verified end-to-end against a reproduction app supplying a custom temporary-credentials provider: session token is now signed correctly and the liveness stream initializes.

Credit

The Sendable fix and root-cause analysis of the actor-isolated conformance were contributed by @nenseso via #4253.

General Checklist

  • Added new tests to cover change, if needed
  • Build succeeds with all target using Swift Package Manager
  • All unit tests pass
  • All integration tests pass
  • Security oriented best practices and standards are followed (e.g. using input sanitization, principle of least privilege, etc)
  • Documentation update for the change if required
  • PR title conforms to conventional commit style
  • New or updated tests include Given When Then inline code documentation and are named accordingly testThing_condition_expectation()
  • If breaking change, documentation/changelog update with migration instructions

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@thisisabhash
thisisabhash requested a review from a team as a code owner July 13, 2026 20:21
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.96%. Comparing base (8741e90) to head (2fa6e71).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4248      +/-   ##
==========================================
+ Coverage   66.91%   66.96%   +0.04%     
==========================================
  Files        1151     1151              
  Lines       43870    43874       +4     
==========================================
+ Hits        29357    29379      +22     
+ Misses      14513    14495      -18     
Flag Coverage Δ
API_plugin_unit_test 68.33% <ø> (-0.03%) ⬇️
AWSPluginsCore 68.07% <ø> (ø)
Amplify 47.49% <ø> (+0.01%) ⬆️
Amplify_Foundation_Bridge_unit_test 62.28% <ø> (ø)
Amplify_Foundation_unit_test 67.64% <ø> (ø)
Analytics_plugin_unit_test 83.43% <ø> (ø)
Auth_plugin_unit_test 72.46% <ø> (+0.08%) ⬆️
DataStore_plugin_unit_test 82.78% <ø> (+0.04%) ⬆️
Firehose_plugin_unit_test 53.15% <ø> (ø)
Geo_plugin_unit_test 73.39% <ø> (ø)
Kinesis_plugin_unit_test 52.17% <ø> (ø)
Logging_plugin_unit_test 64.86% <ø> (ø)
Predictions_plugin_unit_test 34.09% <100.00%> (+0.19%) ⬆️
PushNotifications_plugin_unit_test 85.66% <ø> (ø)
RecordCache_unit_test 76.40% <ø> (ø)
Storage_plugin_unit_test 78.67% <ø> (ø)
unit_tests 66.96% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

FaceLiveness fails with FaceLivenessDetectionError.unknown on iOS 26 when using temporary credentials, while direct long-term credentials work

3 participants