Repository navigation
feat(auth): add AmplifyCognitoASF, an experimental public API for user context data - #4403
Open
thisisabhash wants to merge 2 commits into
Open
thisisabhash wants to merge 2 commits into
thisisabhash wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4403 +/- ##
==========================================
+ Coverage 76.04% 76.09% +0.05%
==========================================
Files 1362 1365 +3
Lines 57864 57933 +69
==========================================
+ Hits 44003 44086 +83
+ Misses 13861 13847 -14
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
cadivus
previously approved these changes
Oct 28, 2026
…r context data Replaces AWSCognitoIdentityProviderASF from the AWS SDK for iOS for apps that send Cognito requests themselves: CognitoUserContextData.encodedData(userPoolId: userPoolClientId:username:deviceId:) in the new AmplifyCognitoASF product, public under @_spi(AmplifyExperimental). A nil username sends "unknown"; a nil device ID sends the identifier for vendor, or none where there is none. - The engine's encoder and device and app readers move into the new target under AmplifyClients/Internal/, so the API and Amplify's own requests share one implementation. CognitoUserPoolASF becomes an adapter over the encoder. - Amplify's own requests are unchanged: UserContextDataFormatTests pinned the engine's output before the move and passes unchanged after it. - The product depends only on AmplifyAvailability and ships a privacy manifest with the Auth plugin's collected-data entry. - AmplifyCognitoASFTests run in the AWSCognitoAuthPlugin scheme, which now also reports the module's coverage; test-group paths, SwiftLint scope, engine_paths.txt and rename_table.json follow the move.
…context data
Makes a one-time setup the main path, as asked in review. CognitoUserContextData
is now a Sendable struct made once with the user pool and app client IDs, and
encodedData(username:deviceId:) replaces the static
encodedData(userPoolId:userPoolClientId:username:deviceId:) the previous commit
added.
- interceptorProvider(deviceId:), in AmplifyCognitoClient, returns an
HttpInterceptorProvider for the AWS SDK for Swift's CognitoIdentityProviderClient.
It fills userContextData on that app client's InitiateAuth,
RespondToAuthChallenge, SignUp, ConfirmSignUp, ForgotPassword,
ConfirmForgotPassword and ResendConfirmationCode requests, for the user each
names ("unknown" when none, as on a refresh-token sign-in). A non-empty value
the caller set wins, and an ipAddress is kept. The device ID is the app's
closure's, else the identifier for vendor. The data is made once per call, and
the hook never throws.
- AmplifyCognitoClient re-exports AmplifyCognitoASF with its SPI, so one
@_spi(AmplifyExperimental) import names both, and declares AmplifyCognitoASF and
ClientRuntime as direct dependencies.
- UserContextDataInterceptorTests drive a real CognitoIdentityProviderClient over
a capturing HTTP engine; UserContextDataInterceptorImportTests checks the
re-export. The README and AGENTS.md describe the interceptor.
harsh62
approved these changes
Nov 2, 2026
harsh62
left a comment
Contributor
There was a problem hiding this comment.
Thanks for turning this around. The interceptor and the IDs-once plain function are exactly what I was after. Looks good.
Non-blocking:
- An empty-string
deviceIdis sent as an emptyDeviceIdrather than falling back to the vendor ID (deviceId ?? ...inCognitoUserContextData.swift). That applies to both the plain function and the interceptor's closure. Treating""as nil would be safer. - The doc example in
CognitoUserContextData+InterceptorProvider.swiftusestry await CognitoIdentityProviderClientConfig(region:), but that initializer isn'tasync. The README correctly has plaintry. - Still open from my notes: how was "nil
deviceIdsends the identifier for vendor" confirmed against the AWS SDK for iOS, givenAWSCognitoIdentityProviderASFshipped only as a binary? A one-line answer here is fine.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue #
None. The gap comes from the AWS SDK for iOS reaching end of support on 2026-08-01.
Description
Problem. Apps that send Cognito requests themselves, through the AWS SDK for Swift or their own backend, used the AWS SDK for iOS's
AWSCognitoIdentityProviderASF.userContextData(...)to fillUserContextData.EncodedData. Cognito threat protection, which was called advanced security features (ASF), uses that data to assess the request. Amplify Swift builds the same fields for its own requests, but only inpackagecode inInternalAWSCognitoAuth, so those apps had nothing to migrate to.Change.
CognitoUserContextData, public under@_spi(AmplifyExperimental)likeAmplifyCognitoClient, is made once for a user pool and app client, and has two ways in:The plain function behaves like the legacy API. A nil
usernamesends"unknown", and a nildeviceIdsends the identifier for vendor, or noDeviceIdwhere there is none, as on macOS. It differs in two ways:strlenfor a non-ASCII client ID.async. The legacy API is synchronous.The result is timestamped and signed, so build it for each request. Two of the values it sends also differ from the legacy API's; see the quirks under What stays the same.
The interceptor,
interceptorProvider(deviceId:), is anHttpInterceptorProviderfor the AWS SDK for Swift'sCognitoIdentityProviderClient. InmodifyBeforeSerializationit fillsuserContextDatawith the plain function's data:InitiateAuth,RespondToAuthChallenge,SignUp,ConfirmSignUp,ForgotPassword,ConfirmForgotPassword,ResendConfirmationCode), when the request'sClientIdis the configured app client's. A request for another app client, or with noClientId, and every other operation go out unchanged.USERNAMEinAuthParametersorChallengeResponses, elseUsername. A refresh-token sign-in names none, so it sends"unknown".deviceIdclosure, given the request's username, else the identifier for vendor, on all seven calls.encodedDatais sent as it is; otherwise anipAddressis kept.AmplifyCognitoClient, which already depends on the Cognito SDK and the encoder, and which now re-exportsAmplifyCognitoASFwith its SPI, so one import names both.One implementation for the API and for Amplify's own requests. The encoder and the device and app readers move out of the engine into the new target,
AmplifyClients/Internal/AmplifyCognitoASF/, and the engine depends on it:ASFAppInfo,ASFDeviceInfoandEngineDeviceInfomove verbatim, except for one doc paragraph. So do theASFDeviceBehaviorandASFAppInfoBehaviorprotocols.CognitoUserPoolASF's keys and its context-data, payload, HMAC, envelope and time-zone methods move toUserContextDataEncoderasstaticmembers, together withASFError.timeZoneOffetis renamedtimeZoneOffset.UserContextDataEncoder.encodedData(username:contextData:userPoolId:clientId:)chains the payload, signature and envelope for both callers.CognitoUserPoolASFkeepsuserContextData(for:deviceInfo:appInfo:configuration:), unchanged in signature, and becomes an adapter over the encoder.CognitoUserContextData'sinit(userPoolId:userPoolClientId:)andencodedData(username:deviceId:)are public. The rest ispackage.What stays the same.
UserContextDataFormatTests(keys, values, envelope and signature) are in a file this PR adds. They ran againstmainbefore anything moved, and they pass unchanged.fix(auth), so that this PR changes nothing Amplify sends. The new API has them too:PhoneTypeandDeviceFingerprintcarry the whole 256-byteutsname.machinebuffer, trailing NULs included. Fixing this changes what Cognito sees from every device that uses Amplify today. The AWS SDK for iOS sends the machine name without the NULs, so an app that moves from it to this API sends Cognito different values in these two fields.import AmplifyCognitoASF.DeviceIdkey fromUserContextDataEncoderinstead ofCognitoUserPoolASF.AmplifyCognitoASFTests.EngineDeviceInfoTestsgains checks of the real reader's language and fingerprint.Packaging.
AmplifyCognitoASFdepends only on the in-packageAmplifyAvailabilityC module, and the engine's dependency onAmplifyAvailabilitymoves to it. An app that uses only this product compiles neither Amplify nor the AWS SDK.aws-sdk-swift(1.7.60),smithy-swift(0.242.0) andSQLite.swift(0.15.4) still apply to the app.PrivacyInfo.xcprivacywith the Auth plugin's collected-data entry (other data types, not linked, no tracking, analytics) and no required-reason APIs.AWSCognitoAuthPlugin,AmplifyCognitoClient,AWSPinpointAnalyticsPluginandAWSPinpointPushNotificationsPlugin. Its entry repeats the one in the Auth plugin's and the client's manifests.AmplifyCognitoClientnow declaresAmplifyCognitoASF(re-exported) and smithy-swift'sClientRuntime(the interceptor protocols) as direct dependencies. Both were already in its closure, so it compiles nothing new.CI and repository wiring.
AmplifyCognitoASFTestsruns in theAWSCognitoAuthPluginscheme, which also listsAmplifyCognitoASFas a coverage target.Amplify-Packagebuilds both and runsAmplifyCognitoASFTestsin its test action, as it doesAmplifyBigIntegerTests.AWSCognitoAuthPluginUnitTestsandAmplifyCognitoClientTestsnow depend onAmplifyCognitoASF, because their test doubles conform to its protocols.unit_test_groups.jsonandinteg_test_groups.json, the new folder joins the paths of the Auth plugin and Cognito client groups, asAmplifyBigIntegerdoes..swiftlint.ymllints the newSources, so the moved code keeps its lint coverage.scripts/cognito-engine/engine_paths.txtis regenerated, which drops the three moved files.rename_table.jsonnow mapsAmplify.DeviceInfotoAmplifyCognitoASF.EngineDeviceInfo.AmplifyClients/AGENTS.mddescribes the module and where its interceptor and tests live, and lists it among the engine's dependencies in place ofAmplifyAvailability.AGENTS.mdadds it to the modules SwiftLint lints.AmplifyCognitoClient's README gains a "User context data for your own requests" section. The interceptor's tests joinAmplifyCognitoClientTests, so they need no new scheme or test group.What to review closely.
scripts/cognito-engine/rename_table.json: itsAmplify.DeviceInfoentry now points toAmplifyCognitoASF.AWSCognitoAuthPluginscheme: it gains only theAmplifyCognitoASFTeststestable and theAmplifyCognitoASFcoverage target, and its launch environment doesn't change.engine_paths.txtand theAmplify-Packagescheme are team-owned.Amplify,AWSPluginsCoreorInternalAmplifyCredentialsimport, a required-reason API, or an error match:check_engine_deps.pystill walks the new target as part of the engine's dependency closure, so a declared dependency on a forbidden module there still fails it. It also greps the target's sources for anAmplifyCognitoClientimport, as it does every non-test target's. But it greps only the engine's and the client's sources for the other forbidden imports.check_engine_privacy_manifest.pyscans only the engine's sources for required-reason APIs.error_match_gate.shscans only the plugin's and the engine's sources. The module can't name the error types it looks for anyway, since it depends only onAmplifyAvailability.import Amplifythere would still fail the client scheme's build.AmplifyClients/Internal/AmplifyCognitoASF/Sourcesin a follow-up, or ask for that change in this PR.AmplifyClients/Internal/, next to the other modules the engine shares, and is the only product there. Keep it there, or move it toAmplifyClients/AmplifyCognitoASF/, beside the client products, before it ships?AmplifyCognitoASF, after the AWS SDK for iOS'sAWSCognitoIdentityProviderASF.CognitoUserContextData.encodedData, is named for theUserContextDataTypefield it fills.interceptorProvider(deviceId:), returningsome HttpInterceptorProvider & Sendable."unknown", where the legacy user-pool layer named the signed-in user; the request'sDEVICE_KEYisn't used, since only two of the seven calls carry it.@_spi(AmplifyExperimental) import AmplifyCognitoClientnow also namesCognitoUserContextData; a plainimport AmplifyCognitoClientstill doesn't.Testing.
AmplifyCognitoASFTests(13 tests on macOS, 12 elsewhere):initmakes four tests fail.AWSCognitoAuthPluginUnitTests:UserContextDataFormatTestsholds the pin above, and checks that the public API's context data equals the engine's for the same device.EngineDeviceInfoTestsalso checks the real reader's language and fingerprint format.UserContextDataEncoderTestsinAmplifyCognitoASFTests.ASFCognitoTests.swift(three time-zone tests) is deleted, andCognitoUserPoolASFTestsloses its twocalculateSecretHashtests. They become three tests:hashKeytest.AmplifyCognitoClientTests,UserContextDataInterceptorTests(22 tests on macOS, 20 elsewhere): most drive a realCognitoIdentityProviderClientwhose HTTP engine keeps every request, and decode the bodies:"unknown"; the device ID closure answers on all seven calls, else the vendor ID is sent;@_spion the type, can't be caught by XCTest; a plain-import probe confirms the SPI holds.UserContextDataInterceptorImportTestscompiles only while the client re-exports the module.xcodebuildbuilds theAWSCognitoAuthPluginscheme for iOS, watchOS, tvOS and visionOS, and theAmplifyCognitoClientscheme, tests included, for all four. On an iOS simulator, the interceptor, ASF and format tests pass.AmplifyCognitoASFproduct builds justAmplifyAvailabilityandAmplifyCognitoASF. With a plainimport,CognitoUserContextDatais not in scope, and thepackageencoder is never visible.check_engine_deps.py --stage final,check_engine_privacy_manifest.py,error_match_gate.sh check S9andgen_engine_paths.py --check), SwiftFormat 0.60.1 with--swiftversion 6.0and SwiftLint 0.54.0 are all clean, with no new warnings.Amplify-Packagescheme makesscripts/python/affected_targets.pyselect all groups. No CI user pool has threat protection, so none of them exercises Cognito's use of the data.General Checklist
AmplifyCognitoASFsuites. The other changed module, the engine, is tested in the Auth plugin's suiteGiven When Theninline code documentationand are named accordingly. The new tests use sentence-style names instead, astestThing_condition_expectation()ASFDeviceSeamTestsandEngineDeviceInfoTestsdoIf breaking change, documentation/changelog update with migration instructionsNot a breaking change: the API is additive and experimentalBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.