Skip to content

Commit b3b8320

Browse files
authored
fix(auth): Consistent device metadata keychain key across auth flows (#4196)
* fix(auth): Use original user input as device metadata keychain key The device metadata keychain key was derived from SignedInData.username, which is parsed from the JWT access token. Cognito returns different username claims depending on the auth flow: USER_PASSWORD_AUTH returns the internal sub-style UUID while USER_SRP_AUTH returns the user alias (email, phone, etc). This caused device metadata stored during one flow to be unretrievable during another, resulting in duplicate device IDs on every login when switching between auth flows. Thread the original user-typed username (inputUsername) through SignedInData, RespondToAuthChallenge, and the parseResponse/ sendRespondToAuth helper chain so that ConfirmDevice always stores device metadata under the same key that InitializeSignInFlow will use for retrieval on subsequent logins. The inputUsername field is optional on both structs to maintain backwards compatibility with existing keychain-persisted data. * test(auth): Add unit and integration tests for device key persistence fix Unit tests (12 tests): - SignedInData Codable backwards compatibility (decode without inputUsername) - SignedInData and AmplifyCredentials round-trip with inputUsername - RespondToAuthChallenge inputUsername encode/decode - parseResponse threading of inputUsername to confirmDevice, finalizeSignIn, challenge events - VerifyPasswordSRP forwarding inputUsername through confirmDevice - VerifySignInChallenge forwarding challenge.inputUsername through confirmDevice Integration tests (2 tests): - Device ID persists across sign-out/sign-in cycles (no duplicates) - Device count remains stable across multiple sign-in cycles * test(auth): Add cross-flow integration tests for device key persistence Add integration tests that explicitly switch between USER_PASSWORD_AUTH and USER_SRP_AUTH flows to verify device key persists across flow changes: - testDeviceKeyPersistsFromUserPasswordToUserSRP - testDeviceKeyPersistsFromUserSRPToUserPassword - testDeviceKeyStableAcrossAlternatingAuthFlows (4 cycles alternating) - testDeviceKeyPersistsAcrossSignOutSignIn (same flow baseline) Each test uses AWSAuthSignInOptions(authFlowType:) to explicitly select the auth flow per sign-in attempt. * chore(auth): Add integration test to Xcode project target membership Add DeviceKeyPersistenceIntegrationTests.swift to all 3 AuthIntegrationTests build phases in the AuthHostApp Xcode project so the tests are discoverable by xcodebuild test-without-building. * chore: Fix trailing whitespace in RESTRequestUtilsTests Pre-existing swiftformat violation on blank lines. * fix(auth): Address review feedback on inputUsername - Add inputUsername to SignedInData.debugDictionary - Change SignedInData.inputUsername from var to let (has explicit init) - Keep RespondToAuthChallenge.inputUsername as var since it relies on synthesized memberwise init with implicit nil default * fix(auth): Normalize device metadata keychain key to lowercase Cognito normalizes usernames to lowercase in JWT claims, but the user-typed input preserves original casing. This caused a keychain key mismatch when storing device metadata with inputUsername (mixed case) and retrieving with the JWT username (lowercase). Lowercase the username in generateDeviceMetadataKey — the single funnel point for all device metadata store/retrieve/remove operations.
1 parent d5ca7e8 commit b3b8320

13 files changed

Lines changed: 741 additions & 13 deletions

File tree

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/ConfirmDevice.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ struct ConfirmDevice: Action {
5858
environment: environment
5959
)
6060

61-
// Save the device metadata to keychain
61+
// Save the device metadata to keychain using the original user input
62+
// to ensure consistent keychain key across different auth flows
6263
let credentialStoreClient = (environment as? AuthEnvironment)?.credentialsClient
64+
let deviceMetadataUsername = signedInData.inputUsername ?? signedInData.username
6365
_ = try await credentialStoreClient?.storeData(
64-
data: .deviceMetadata(signedInData.deviceMetadata, signedInData.username))
66+
data: .deviceMetadata(signedInData.deviceMetadata, deviceMetadataUsername))
6567
logVerbose(
6668
"Successfully stored the device metadata in the keychain ",
6769
environment: environment

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/DeviceSRPAuth/VerifyDevicePasswordSRP.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ struct VerifyDevicePasswordSRP: Action {
7979
request: request,
8080
for: username,
8181
signInMethod: .apiBased(.userSRP),
82+
inputUsername: inputUsername,
8283
environment: userPoolEnv
8384
)
8485
logVerbose(

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SRPAuth/InitiateAuthSRP.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ struct InitiateAuthSRP: Action {
145145
logVerbose("\(#fileID) InitiateAuth response success", environment: environment)
146146
if case .customChallenge = response.challengeName {
147147
let parameters = response.challengeParameters
148+
let inputUsername = username
148149
let username = parameters?["USERNAME"] ?? username
149150
let respondToAuthChallenge = RespondToAuthChallenge(
150151
challenge: .customChallenge,
151152
availableChallenges: [],
152153
username: username,
153154
session: response.session,
154-
parameters: parameters
155+
parameters: parameters,
156+
inputUsername: inputUsername
155157
)
156158
return SignInEvent(eventType: .receivedChallenge(respondToAuthChallenge))
157159
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/SRPAuth/VerifyPasswordSRP.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ struct VerifyPasswordSRP: Action {
8080
request: request,
8181
for: username,
8282
signInMethod: .apiBased(.userSRP),
83+
inputUsername: inputUsername,
8384
environment: userPoolEnv
8485
)
8586
logVerbose(

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/VerifySignInChallenge.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ struct VerifySignInChallenge: Action {
9797
request: input,
9898
for: username,
9999
signInMethod: signInMethod,
100+
inputUsername: challenge.inputUsername,
100101
environment: userpoolEnv
101102
)
102103
logVerbose(
@@ -202,7 +203,8 @@ struct VerifySignInChallenge: Action {
202203
availableChallenges: [],
203204
username: challenge.username,
204205
session: challenge.session,
205-
parameters: [:]
206+
parameters: [:],
207+
inputUsername: challenge.inputUsername
206208
)
207209

208210
let event = SignInEvent(eventType: .receivedChallenge(newChallenge))
@@ -220,7 +222,8 @@ struct VerifySignInChallenge: Action {
220222
availableChallenges: [],
221223
username: challenge.username,
222224
session: challenge.session,
223-
parameters: ["MFAS_CAN_SETUP": "[\"\(confirmSignEventData.answer)\"]"]
225+
parameters: ["MFAS_CAN_SETUP": "[\"\(confirmSignEventData.answer)\"]"],
226+
inputUsername: challenge.inputUsername
224227
)
225228

226229
let event: SignInEvent

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignIn/WebAuthn/FetchCredentialOptions.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ struct FetchCredentialOptions: Action {
5757
availableChallenges: [],
5858
username: username,
5959
session: response.session,
60-
parameters: response.challengeParameters
60+
parameters: response.challengeParameters,
61+
inputUsername: respondToAuthChallenge.inputUsername
6162
)
6263
let event = WebAuthnEvent(
6364
eventType: .assertCredentials(options, .init(

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ struct AWSCognitoAuthCredentialStore {
148148
for username: String,
149149
with configuration: AuthConfiguration
150150
) -> String {
151-
return "\(storeKey(for: authConfiguration)).\(username).\(deviceMetadataKey)"
151+
return "\(storeKey(for: authConfiguration)).\(username.lowercased()).\(deviceMetadataKey)"
152152
}
153153

154154
private func generateASFDeviceKey(

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/RespondToAuthChallenge.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ struct RespondToAuthChallenge: Equatable {
2121

2222
let parameters: [String: String]?
2323

24+
var inputUsername: String?
25+
2426
}
2527

2628
extension RespondToAuthChallenge {

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/SignedInData.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ struct SignedInData {
1515
let deviceMetadata: DeviceMetadata
1616
let cognitoUserPoolTokens: AWSCognitoUserPoolTokens
1717
var isRefreshTokenExpired: Bool?
18+
let inputUsername: String?
1819

1920
init(
2021
signedInDate: Date,
2122
signInMethod: SignInMethod,
2223
deviceMetadata: DeviceMetadata = .noData,
23-
cognitoUserPoolTokens: AWSCognitoUserPoolTokens
24+
cognitoUserPoolTokens: AWSCognitoUserPoolTokens,
25+
inputUsername: String? = nil
2426
) {
2527
let user = try? TokenParserHelper.getAuthUser(accessToken: cognitoUserPoolTokens.accessToken)
2628
self.userId = user?.userId ?? "unknown"
@@ -30,6 +32,7 @@ struct SignedInData {
3032
self.deviceMetadata = deviceMetadata
3133
self.cognitoUserPoolTokens = cognitoUserPoolTokens
3234
self.isRefreshTokenExpired = false
35+
self.inputUsername = inputUsername
3336
}
3437
}
3538

@@ -46,7 +49,8 @@ extension SignedInData: CustomDebugDictionaryConvertible {
4649
"signInMethod": signInMethod,
4750
"deviceMetadata": deviceMetadata,
4851
"tokens": cognitoUserPoolTokens,
49-
"refreshTokenExpired": isRefreshTokenExpired ?? false
52+
"refreshTokenExpired": isRefreshTokenExpired ?? false,
53+
"inputUsername": inputUsername.masked()
5054
]
5155
}
5256
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Helpers/UserPoolSignInHelper.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,18 @@ struct UserPoolSignInHelper: DefaultLogger {
7373
request: RespondToAuthChallengeInput,
7474
for username: String,
7575
signInMethod: SignInMethod,
76+
inputUsername: String? = nil,
7677
environment: UserPoolEnvironment
7778
) async throws -> StateMachineEvent {
7879

7980
let client = try environment.cognitoUserPoolFactory()
8081
let response = try await client.respondToAuthChallenge(input: request)
81-
let event = parseResponse(response, for: username, signInMethod: signInMethod)
82+
let event = parseResponse(
83+
response,
84+
for: username,
85+
signInMethod: signInMethod,
86+
inputUsername: inputUsername
87+
)
8288
return event
8389
}
8490

@@ -87,7 +93,8 @@ struct UserPoolSignInHelper: DefaultLogger {
8793
for username: String,
8894
signInMethod: SignInMethod,
8995
presentationAnchor: AuthUIPresentationAnchor? = nil,
90-
srpStateData: SRPStateData? = nil
96+
srpStateData: SRPStateData? = nil,
97+
inputUsername: String? = nil
9198
) -> StateMachineEvent {
9299

93100
if let authenticationResult = response.authenticationResult,
@@ -104,7 +111,8 @@ struct UserPoolSignInHelper: DefaultLogger {
104111
signedInDate: Date(),
105112
signInMethod: signInMethod,
106113
deviceMetadata: authenticationResult.deviceMetadata,
107-
cognitoUserPoolTokens: userPoolTokens
114+
cognitoUserPoolTokens: userPoolTokens,
115+
inputUsername: inputUsername ?? username
108116
)
109117

110118
switch signedInData.deviceMetadata {
@@ -121,7 +129,8 @@ struct UserPoolSignInHelper: DefaultLogger {
121129
availableChallenges: response.availableChallenges ?? [],
122130
username: username,
123131
session: response.session,
124-
parameters: parameters
132+
parameters: parameters,
133+
inputUsername: inputUsername ?? username
125134
)
126135

127136
switch challengeName {

0 commit comments

Comments
 (0)