A field guide to
sub,username, alias mode, and UsernameAttributes - and how to build a user pool you won't regret later.
Amazon Cognito is a powerful service, but it has one sharp edge that trips up nearly every team that integrates it for the first time: the word username means at least two different things depending on where you encounter it. Get this wrong at design time, and you're looking at a migration later. Get it right, and everything else snaps into place.
This post unpacks the internals of how Cognito manages user identity, walks through the two fundamental user pool configurations, and gives you a concrete recommendation for greenfield projects.
The "username" field has a split personality
The field called username exists in two completely different contexts, and they behave differently.
Context 1 - Internal identity store. The value Cognito uses in its backend to index a user's record. Depending on pool configuration, this is either a value you provide at sign-up or a UUID Cognito generates automatically.
Context 2 - SDK / API parameter. The username field in calls like InitiateAuth or SignUp. The value you pass here is resolved differently based on how your user pool is configured. It might accept an email, a phone number, or a custom username string.
This duality is the root of most Cognito confusion. Let's build up the full picture from the ground level.
Key user attributes in Cognito
When a user is created in a Cognito User Pool, Cognito stores a set of attributes on their profile. The most important ones to understand are:
| Attribute | Description |
|---|---|
sub |
A UUID assigned by Cognito at user creation. Immutable, never changes, even if the user updates their email or phone. The gold-standard identity anchor. |
username |
A unique identifier string for the user within the pool. Behavior depends on pool configuration: either a custom value you supply, or a UUID Cognito auto-generates. |
email |
The user's email address. Can be marked as verified. Used as a sign-in identifier in certain pool configurations. |
phone_number |
The user's phone number in E.164 format. Can be verified. Used as a sign-in identifier in certain configurations. |
preferred_username |
A display-friendly handle (e.g. @handle). Can be configured as an alias to allow sign-in. Not the same as the internal username. |
sub vs username - the critical distinction
sub is Cognito's internal, permanent user identifier. Think of it as the primary key in Cognito's database. It is assigned at user creation, never modified, and persists for the life of the account. You should store sub as the foreign key in your own databases when linking records to a Cognito user.
username, on the other hand, is the login handle - the string used to look up a user in the pool. Its behavior is where the configuration complexity lives. In some pool setups it's something the user chooses (like a handle or their email address); in others, it's a UUID that Cognito auto-generates and the user never sees at all.
Rule of thumb: Never store
usernameas a foreign key in your external databases. Always usesub. Email addresses change, phone numbers change, and even usernames can change.subdoesn't.
The two user pool modes
When you create a User Pool, you choose which attributes act as sign-in identifiers. The AWS Console presents this as a list of checkboxes: email, phone number, and username. Under the hood, your selection flips the pool into one of two fundamentally different modes.
Both are valid patterns, but they have meaningfully different trade-offs, and you cannot change the mode after pool creation.
Case A: Alias mode (username is selected)
In this configuration, users provide a username string at sign-up, this becomes the primary login handle stored in Cognito's backend. If you additionally select email or phone number as sign-in options, those become aliases.
What's an alias? An alias is an attribute value that can be entered in the username field of Cognito's sign-in APIs as an alternative to the actual username. Cognito resolves it under the hood and maps it to the correct user record. The API doesn't expose a separate "alias" field — you just put the alias value in the username parameter.
When you create this pool via the API or SDK, the configuration looks like this:
{
"UsernameAttributes": [],
"AliasAttributes": ["email", "phone_number"]
}
So a user might sign up with username john_doe, email john@example.com, and phone +14155550100. At sign-in time, all three values are valid inputs for the username parameter, Cognito normalizes them behind the scenes.
Important: Cognito enforces uniqueness on verified aliases. If email or phone number is enabled as an alias and a user has verified theirs, no other account can claim that same email or phone, even as a regular attribute. This prevents silent conflicts during sign-up.
Case B: UsernameAttributes mode (username not selected)
This is the cleaner architecture. When you don't select username as a sign-in identifier, Cognito automatically assigns a UUID as the internal username for every user. The user never sees or knows this UUID, they sign in using their email or phone number, which you designate as the actual login identifiers via UsernameAttributes.
{
"UsernameAttributes": ["email", "phone_number"],
"AliasAttributes": []
}
The terminology difference between the two parameters is subtle but important:
| Parameter | What it does | Where it applies |
|---|---|---|
AliasAttributes |
Designates attributes as alternative login handles alongside an existing user-defined username | Case A (alias mode) |
UsernameAttributes |
Designates attributes as the primary login identifier, replacing a user-defined username entirely | Case B (UUID mode) |
These two parameters are mutually exclusive. Setting both at the same time is invalid.
All the combinations at a glance- Very important
Depending on which sign-in identifiers you select in the Console, here are the possible configurations:
| Selection | Mode | Internal username | Valid sign-in values |
|---|---|---|---|
| Username only | Alias | User-defined string | Username only |
| Username + email | Alias | User-defined string | Username or email |
| Username + phone | Alias | User-defined string | Username or phone |
| Email only | UsernameAttributes | Auto UUID | Email only |
| Phone only | UsernameAttributes | Auto UUID | Phone only |
| Email + phone | UsernameAttributes | Auto UUID | Email or phone |
| Username + email + phone | Alias | User-defined string | Username, email, or phone |
Mental model: quick reference
| Concept | What it is |
|---|---|
sub |
Immutable UUID. Always the true identity anchor. Store this in your DB. |
username (alias mode) |
User-defined login handle stored in Cognito backend. |
username (UsernameAttributes mode) |
Auto-generated UUID. User never sees it. |
| Alias | Alternative value accepted in the username API parameter. Resolves to the actual user. |
AliasAttributes |
Pool config. Adds email/phone as aliases alongside a user-defined username. |
UsernameAttributes |
Pool config. Makes email/phone the primary identifier; internal username becomes a UUID. |
Choosing the right configuration
UsernameAttributes with email or phone works cleanly for most cases, but selecting both email and phone together introduces a subtle inconsistency: at sign-up, the user can provide either one, which means some users have an email on file and others have only a phone. Your application then has to handle this uneven data everywhere.
The option that avoids this is AliasAttributes: ["email", "phone_number"] with a UUID you supply as the username at sign-up. Both email and phone are always collected, both work for sign-in, and the internal identity is a stable UUID you control.
| Configuration | Consistent data | Both email + phone | Hosted UI works | Custom UI required |
|---|---|---|---|---|
UsernameAttributes: ["email"] |
Yes | No | Yes | No |
UsernameAttributes: ["phone_number"] |
Yes | No | Yes | No |
UsernameAttributes: ["email", "phone_number"] |
No, user picks one | Yes | Yes | No |
AliasAttributes: ["email", "phone_number"] + UUID as username |
Yes | Yes | No | Yes |
The last option is the most robust if you need both email and phone as sign-in identifiers. The trade-off is that Hosted UI will render a username field (since the pool is in alias mode), so you need a custom sign-up UI to pass the UUID programmatically and keep the experience clean for the user.
Note: The
AliasAttributes+ manual UUID approach is only possible via the API or SDK. There is no way to configure this cleanly through the AWS Console alone.
One more thing: how USERNAME is resolved at sign-in
Regardless of which mode your pool uses, InitiateAuth always accepts input through a field named USERNAME. What happens after you pass that value is where the two modes diverge.
In UsernameAttributes mode, Cognito does a straight attribute scan, it looks for a user whose stored email or phone_number attribute matches the string you passed, then maps it to the internal UUID username before proceeding with auth.
USERNAME: "john@example.com"
→ scan users where attribute email = "john@example.com"
→ found → resolve to internal UUID
→ proceed with auth
In AliasAttributes mode, Cognito resolves in two steps:
- Exact match on the internal
usernamefield first. - If no match, fall through to an alias attribute scan.
USERNAME: "a1b2c3d4-uuid"
→ exact match on username → proceed with auth
USERNAME: "john@example.com"
→ no exact username match
→ scan alias attributes where email = "john@example.com"
→ found → proceed with auth
This two-step resolution is exactly why the AliasAttributes + manual UUID pattern works so cleanly, sign-in with UUID hits step 1 immediately, sign-in with email or phone falls through to step 2. Both paths work with no ambiguity.
UsernameAttributes mode |
AliasAttributes mode |
|
|---|---|---|
| Lookup strategy | Attribute scan only | Exact username match first, then alias scan |
Arbitrary string as USERNAME |
Fails - no attribute match | Works if it matches the stored username |
Email / phone as USERNAME |
Works | Works if declared as alias |
Quick test for your configuration: Open the Cognito console, navigate to your User Pool, and check under Sign-in experience → Sign-in identifiers. If you see "Email" and/or "Phone number" listed without "Username", you're in UsernameAttributes mode. If "Username" is listed, you're in alias mode.
The pool configuration you pick at creation time is permanent, it cannot be changed after the pool exists. Spend the five extra minutes getting this right on day one.
