TL;DR: If your AWS Cognito User Pool is completely ignoring your tracked devices and prompting for MFA anyway, your ChallengeRequiredOnNewDevice setting is likely false. Furthermore, calling native device confirmations from the frontend might silently fail if your User-Agent contains spaces. We fixed this by moving device registration entirely to the backend and cleaning up stale device tokens natively.
The Problem / Why This Matters
AWS Cognito offers a highly requested feature called Device Tracking, which allows you to "remember" a user's device and use that cryptographic trust to seamlessly bypass Multi-Factor Authentication (MFA) on future logins.
For mission-critical applications like Workmax's Payroll system, seamless MFA is absolutely essential for the user experience. Payroll administrators often log into the platform dozens of times per week to approve timesheets, run payment cycles, or audit tax codes. While security—and strict MFA enforcement—is non-negotiable for sensitive financial data, repeatedly forcing an administrator to grab their 2FA authenticator app on a secure, company-issued laptop they use every single day introduces immense friction and alert fatigue. "Remembering" devices bridges this gap, providing maximum security on unverified browsers while getting out of the way for trusted daily workflows.
However, we recently encountered a bizarre edge case: despite the frontend correctly caching the DeviceKey and DevicePassword, Cognito was consistently rejecting the session and halting the authentication flow with a SOFTWARE_TOKEN_MFA prompt. When we tried verifying the devices internally, our devices were completely missing from the AWS console.
Why was AWS completely ignoring our fully-functional code? The issue was actually a combination of three obscure architectural bugs spread across the frontend, the Lambda handlers, and AWS CloudFormation.
The Solution / How We Did It
Step 1: Solving the Silent InvalidParameterException
Originally, our frontend application explicitly triggered an API call to a /confirm-device Lambda using the ConfirmDeviceCommand from the AWS SDK. We were passing navigator.userAgent (e.g., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...) as the DeviceName to help users identify their sessions.
The Bug: AWS Cognito has a strict, completely undocumented regex assigned to the DeviceName property ([\w]+). Because the User-Agent string contains empty spaces, Cognito was throwing an opaque InvalidParameterException, aborting the confirmation entirely. Because the device was never confirmed, Cognito refused to honor it during the next login.
The Fix: We deprecated the /confirm-device endpoint entirely. Instead, we intercepted the generic onSuccess callback natively within our Node.js /login Lambda using amazon-cognito-identity-js.
// Inside our login.ts Lambda proxy
if ((cognitoUser as any).deviceKey && !payload.deviceKey) {
cognitoUser.setDeviceStatusRemembered({
onFailure: (err) => resolveSession(),
onSuccess: () => resolveSession()
});
}
By allowing the backend to organically confirm the session via the SDK natively, it automatically safely populated empty defaults for DeviceName (resulting in Node.js/24 in our backend context) and bypassed the regex collision entirely.
Step 2: Fixing the CDK ChallengeRequiredOnNewDevice Property
Once the devices were successfully appearing as "Remembered: Yes" in the AWS Console, we assumed MFA would finally bypass. However, the MFA prompt continued to interrupt every login.
It turns out that Cognito User Pools have an incredibly unintuitive parameter mapping mechanism. In our deployment script, we had provisioned the User Pool with the following mapping:
DeviceConfiguration: {
ChallengeRequiredOnNewDevice: false,
DeviceOnlyRememberedOnUserPrompt: true
}
The Bug: Counter-intuitively, setting ChallengeRequiredOnNewDevice to false instructs AWS Cognito to indiscriminately challenge every single login attempt on all devices. It completely turns off the MFA suppression engine.
The Fix: We created a database migration script to loop through our existing tenant User Pools and update this property.
await cognitoService.updateUserPool({
DeviceConfiguration: {
ChallengeRequiredOnNewDevice: true, // MUST be true for MFA suppression
DeviceOnlyRememberedOnUserPrompt: true
},
UserPoolId: company.userpoolId
});
When this evaluates to true, Cognito finally permits remembered devices to suppress the MFA barrier natively via the DEVICE_PASSWORD_VERIFIER fallback.
Step 3: Evicting Stale Device Keys
Finally, we noticed a collision when a new user attempted to log into an interface that previously hosted an older user's active session.
The frontend naturally attached the old user's deviceKey cookie to the payload. AWS Cognito appropriately flagged that the token didn't belong to the new user and returned a ResourceNotFoundException. Because the authentication was rejected with an HTTP 400, the frontend halted its execution and left the stale cookie locked in the browser—permanently locking the new user out of the application with a frustrating Device key not found crash loop.
The Fix: We injected a reactive garbage-collection trap gracefully inside the Angular UI's error interceptors:
this.loginService.doLogin({ ...details, deviceKey }).subscribe({
error: (error): void => {
// Clear stale device key if backend reports it as invalid
if (error?.error?.deviceKeyInvalid) {
UserService.clearDeviceKey();
// Automatically retry without the stale attached device key
this.doLogin(details);
return;
}
// Handle standard errors...
}
});
Now, if a collision is detected, the frontend effortlessly scrubs the dead token and automatically replays the login stream in the background—completing the authentication transparently.
Results
By implementing these structural patches, we transformed a brittle, unpredictable MFA configuration into a seamless, enterprise-grade authentication pipeline.
- 100% Reliability: Device tracking now functions flawlessly across Web and Mobile targets natively.
- Zero Frontend Friction: Eliminating the separate
/confirm-devicesequence significantly reduced latency and frontend API state-complexity.
Trade-offs and Limitations
- Backend Device Names: By delegating device confirmations to the Node.js Lambda, all devices inherently display as
Node.jsrather than unique browser signatures (like Chrome vs Safari). If user-friendly device auditing is a strict requirement for your platform, you will need to manually pass a securely formatted Regex-compliantDeviceNamestring down from your frontend into your backend payload to override the Node.js default. - Lost Lambda Permissions: When hot-patching Cognito pools, invoking
update-user-poolfrom the CLI dynamically wipes unreferenced attributes like Lambda triggers. Always remember to appendResource-Based Policies(aws lambda add-permission) again securely if linking triggers programmatically.
Conclusion
MFA suppression through AWS Cognito is incredibly powerful, but its implementation details are riddled with silent undocumented constraints and unintuitive parameter semantics. If you're building a Serverless proxy authentication flow, confirm your devices iteratively on your backend handlers, triple-check your User Pool DeviceConfiguration flags, and always plan for cryptographic collision recycling on your client-side boundaries.


