Migrating 30k bank users to a new system presented many challenges:
1. The legacy system was a "black box" that stored usernames, hashed passwords, and customer data in an unstructured way, making migration difficult.
2. Early attempts worked for test users but failures occurred at scale due to unforeseen issues like an SMS quota being exceeded and validation rules not matching legacy data.
3. User experience problems arose from unclear language about migrating vs new accounts, and missing validation on legacy usernames.
4. Security issues emerged when engineers tried to help users with forgotten passwords, exposing data from the legacy system.
The migration process required flexibility to address unexpected problems at both the
2. Anna Skawińska
Node.js Team Manager,
Senior Node.js Developer @TSH
also: mom of 2, wife, self-taught musician,
constant learner and doer, dad joke professional
23. ✅ may be transparent to the user
✅ first sign in: custom authentication against pre-migrated logins
✅ ValidationData, ClientMetadata: user could add phone, email address…
❌ 2021: Password Policy applied on legacy passwords
✅ 2022: Password Policy no longer applied on legacy passwords!
Migrate User Lambda trigger?
28. How to do it securely?
● What if it leaks out?
● Oh, it’s just temp!
USERNAME PASSWORD DATE_OF_BIRTH NEW_CUSTOMER_ID
AA_SMITH
0x49FE91D153D79C57FA3EC25E1BB762EE9527313E7F02060E281AA36F5B6AE4E2
01-01-1985 0010E00001IfPmYQAV
AA_SMITH
0x49FE91D153D79C57FA3EC25E1BB762EE9527313E7F02060E281AA36F5B6AE4E2
06-06-1980 0010E00001GgxM2QAJ
superCoolHashingFunction!
PK NEW_CUSTOMER_ID
a883a161f49e38d70bc17e0915d2faf0da58aaef7352f2204fdc916969d36cc69f9d
374cf764e210687633001bd6ac25d2bcbaf695e0e6ebb20893fa1f5603ac 0010E00001GgxM2QAJ
● This Dynamo table stays for the verifyLegacyLogin Lambda
29. SuperCoolHashingFunction
async generateHash(username: string, passwordHash: string, dateOfBirth: string) {
// So that identical passwords have unique hash:
const salt = `${username}#${dateOfBirth}`;
const hash = await this.hashWithSalt(passwordHash, salt);
// So that a leaked hash table can't be reverse engineered:
const pepperedHash = await this.hashWithSalt(hash, this.options.passwordSalt);
return pepperedHash;
}
31. ● check with test data:
○ generate fake credentials + date of birth, store in DDB, automate
○ worked (repeatedly) ✅
● check with a couple of “friendly” customers (knowing their passwords upfront)
○ worked ✅
So far, so good
33. SMS Quota
● 7.5k people * $0.1189 / SMS ≈ $900
● quota at the time?
● $100…
● raised to $1000 right away
● enough for how long..?
34. UX failure #1: “Sign up” vs “Migrate”
● target group: 60+
● missed the “already have an account?” question
● solution: “sign in” is now on a different landing page than
“sign up”
35. UX failure #2 - no validation on username
● no prior idea of what usernames look like
● (black box)
● temp migration table only accessed by the Bank’s internal
employee
36. Security failure
● Customer: “password doesn’t work”
● …Engineers: added “reveal password” feature
● Customer: “password doesn’t work”
● CTO + Engineer on the line
● Customer, CTO, Engineer: “password doesn’t work”
● guessing game, reverse engineering legacy system…
● …
● …legacy system cropped long passwords
● solution: why not crop, too 🙈 (migration step only)
39. Expect a peak right after announcement
● expect a massive peak on the first day / week after the announced migration
● calculate the monthly quotas accordingly
● you can lower them after the first month
40. Lambda autoscaling worked like a charm
● there was no need for provisioned concurrency
● peak traffic gracefully handled
41. Migration Lambda Trigger - weak passwords work now!
● The Out-of-the-box AWS Cognito functionality would work now
● you can forget this presentation now