Cognito Custom Attributes Not Showing in the ID Token: App Client Read Permissions, the custom: Prefix, Access Tokens, Strings Only, and the Default That Lets Users Edit Their Own
If an Amazon Cognito custom attribute is not showing in your ID token or in GetUser, the usual cause is the app client's read permissions: each app client has its own list of attributes it may read and write, and a custom attribute that is not on that list is left out. Open App clients, Attribute permissions and check Read for it, or add it to ReadAttributes with the full name, including the prefix: custom:plan, not plan. Two more facts explain most of the remaining questions. Custom attributes never appear in the access token on their own; adding them takes a pre token generation Lambda trigger, and access token customization requires the Essentials or Plus feature plan. And the default works the other way round from what people expect: an app client created without a WriteAttributes list lets users change every attribute themselves. In our test, a signed-in user set custom:role to admin on their own account with nothing but their access token. This guide covers adding attributes, permissions, tokens, the string-only rule, the update that resets your app client, and every error.
Jake's booking app needed to know which customers were on the shop's repair plan, so Ethan added a custom attribute, custom:plan, to the Cognito user pool. The admin console showed it on every user. The app's ID token did not. "It's right there in the console," Jake said. "The console is allowed to see it," Ethan said. "The app has to be told it's allowed too."
New to Cognito? An Amazon Cognito user pool is a user directory with sign-up and sign-in; each app that uses it is an app client. After sign-in, Cognito returns an ID token (who the user is, with their attributes) and an access token (what they may do, used to call APIs). Standard attributes such as email come built in, and you can add up to 50 custom attributes of your own, which always carry the prefix custom:. If identity and permissions in AWS are new, our plain-English IAM guide explains the AWS side; Cognito handles your app's users.
How to add a custom attribute to a Cognito user pool
- In the Cognito console, open User pools, choose the pool, then Sign-up (attributes are listed there) and Add custom attributes.
- Enter a name of up to 20 characters (Cognito stores it as
custom:plus that name), choose the type (the console offers String and Number; the API also allows Boolean and DateTime), optional length or value limits, and whether it is mutable. - Save, then open App clients, choose each app client that needs the attribute, and set its read and write permissions under Attribute permissions (next section).
With the AWS CLI:
aws cognito-idp add-custom-attributes --user-pool-id us-east-1_EXAMPLE \
--custom-attributes Name=plan,AttributeDataType=String,Mutable=true,StringAttributeConstraints="{MinLength=1,MaxLength=32}"
Before you click save, read the next section on what you cannot undo: custom attributes are permanent.
Custom attributes are permanent
Once a custom attribute exists, you cannot remove it, rename it, or change its type, mutability or length limits. The API reflects this: it has AddCustomAttributes and nothing to delete or rename one. (DeleteUserAttributes and AdminDeleteUserAttributes clear a user's value, not the attribute.) A pool can hold 50 custom attributes, so a misnamed or wrongly typed one uses a slot for good. Other rules to plan around:
- They cannot be required. Only standard attributes can be required at sign-up, and those are set when the pool is created.
- Immutable means "write once, at creation". An immutable custom attribute can only get a value when the user is created; nobody, including admins, can change it afterwards.
- Maximum length is 2,048 characters, whatever you set as the limit.
If a mistake is already live, add a correctly named new attribute, copy values across with AdminUpdateUserAttributes, switch your code and leave the old one unused. Test new attributes in a separate development pool first.
Why a custom attribute is not in the ID token (and how to fix it)
Every app client has two lists: attributes it may read and attributes it may write. Cognito only puts attributes into an app client's ID token, and only returns them from GetUser, if that client can read them. So a custom attribute can be on the user, visible in the console and in AdminGetUser, and still be absent from your app's token. Check, in this order:
- The app client's read permissions. Console: App clients, the client, Attribute permissions, Edit, check Read for the attribute, save. Do it for every app client that needs it; a pool with a web app and a mobile app has two lists.
- The name in your code. Claims use the full name,
custom:plan. Code that readsclaims['plan']finds nothing. - Whether the user has a value. An attribute with no value for that user is simply absent from the token. Check with
aws cognito-idp admin-get-user. - Which token you are reading. Custom attributes go into the ID token, not the access token (see access tokens).
- Whether the token is fresh. A token issued before you changed permissions or values keeps its old claims until the user signs in again or refreshes.
Where a custom attribute shows up, and what controls it:
| Place | Shows custom attributes? | Controlled by |
|---|---|---|
Console and AdminGetUser | Always | Your IAM permissions |
| ID token | Yes, if the app client can read them and the user has a value | App client ReadAttributes |
GetUser (with the user's access token) | Yes, if the app client can read them | App client ReadAttributes |
| Access token | No, unless a trigger adds them | Pre token generation trigger (V2_0) |
| userInfo endpoint | Depends on scopes | Access token scopes and ReadAttributes |
New custom attributes start without read and write permissions on existing app clients that already have explicit lists, so adding the attribute and forgetting step 1 is the classic sequence. To see a client's lists from the CLI:
aws cognito-idp describe-user-pool-client --user-pool-id us-east-1_EXAMPLE --client-id 1example23456789 \
--query "UserPoolClient.[ReadAttributes,WriteAttributes]"
If both come back empty, the client is on the defaults (read and write everything), because DescribeUserPoolClient returns the lists only when they differ from the default. In our test, a client created with no lists showed None for both.
Ethan checked custom:plan under the web app client's read permissions, signed out and back in, and the token carried "custom:plan": "pro". "That's all?" Jake asked. "That's the fix," Ethan said. "The next part is the bit that worried me."
The write trap: users can edit their own custom attributes by default
When an app client has no WriteAttributes list, it has write access to every standard and custom attribute, and users can change their own values with UpdateUserAttributes, using nothing but the access token your app gave them. That is fine for custom:favorite_shop. It is a privilege escalation for custom:role, custom:plan or custom:tenant_id, if your API, your identity pool or your code makes decisions from those values.
We reproduced it with a user pool mock: the user signed in through an app client with default permissions, called update_user_attributes with their own access token to set custom:role to admin, and the change went through; admin_get_user then showed custom:role = admin. The same request from any client works the same way with any token the user can obtain, so one permissive app client is enough.
How the two lists behave:
| App client setting | Read | Self-service write (UpdateUserAttributes) |
|---|---|---|
| No lists (the default for a new client) | All standard and custom attributes | All attributes, including custom:role |
Explicit ReadAttributes / WriteAttributes | Only the listed attributes | Only the listed attributes; others return NotAuthorizedException |
| A custom attribute added later, client has explicit lists | Not readable until you add it | Not writable until you add it |
Admin APIs (AdminUpdateUserAttributes) | Not limited by the lists; controlled by IAM | |
The fix:
- For every app client in the pool, set an explicit
WriteAttributeslist that leaves out every attribute used for authorization. User attributes are shared across all app clients, so one client with default write access undoes the others. - Set those attributes only from trusted code: an admin tool or backend using
AdminUpdateUserAttributeswith IAM credentials. - Better still, keep authorization data outside user attributes, in Cognito groups (which appear as
cognito:groupsand users cannot change) or in your own database.
If a user tries to write an attribute their app client cannot write, Cognito returns NotAuthorizedException, which is the behavior you want for anything that grants access.
The reset trap: update-user-pool-client wipes what you leave out
The natural way to add a read permission from the command line is dangerous:
# DANGER: resets every other setting of this app client to its default
aws cognito-idp update-user-pool-client --user-pool-id us-east-1_EXAMPLE --client-id 1example23456789 \
--read-attributes email custom:plan
UpdateUserPoolClient is a full replacement: if you don't provide a value for a setting, Cognito sets it to its default. That one-line command also clears callback URLs, OAuth flows and scopes, explicit auth flows and token lifetimes, and sign-in breaks for everyone on that client. The safe pattern is read, modify, write back:
import boto3
def set_read_attributes(pool_id, client_id, read_attrs):
c = boto3.client('cognito-idp')
cfg = c.describe_user_pool_client(UserPoolId=pool_id, ClientId=client_id)['UserPoolClient']
for k in ('ClientSecret', 'CreationDate', 'LastModifiedDate'): # output-only fields
cfg.pop(k, None)
cfg['ReadAttributes'] = read_attrs
c.update_user_pool_client(**cfg)
Run it in a development pool first, and compare describe-user-pool-client before and after. In CloudFormation or the CDK the template already holds the full configuration, which is one more reason to manage app clients as code.
Getting custom attributes into the access token
Access tokens carry scopes and identifiers, not user attributes. In our test, the ID token held all three custom attributes and the access token held none. That surprises people who authorize API calls with access tokens, for example in API Gateway. You have three options:
- Use the ID token where your API accepts it, if all you need is to know who the user is and their attributes.
- Use a pre token generation Lambda trigger to add claims to the access token. Version 1 (
V1_0) of the trigger, available on every plan, can only customize the ID token. Access token customization uses theV2_0event (andV3_0for machine-to-machine), which needs a user pool on the Essentials or Plus feature plan. - Look the value up in your backend from the user's
sub, which is often the cleanest choice for authorization data that changes.
A V2_0 trigger that copies custom:plan into the access token, in Python:
def handler(event, context):
plan = event['request']['userAttributes'].get('custom:plan', 'none')
event['response']['claimsAndScopeOverrideDetails'] = {
'accessTokenGeneration': {'claimsToAddOrOverride': {'plan': plan}}
}
return event
Attach it under Extensions (Lambda triggers) as a pre token generation trigger with event version 2. To check the new claim on your API side, decode and verify the token as shown in our Cognito JWT verification guide. Our Lambda guide covers creating the function; it must allow Cognito to invoke it.
Custom attribute values are always strings
Whatever type you choose, Cognito stores and returns every custom attribute value as a string. A number attribute set to 42 comes back as "42" in the token, and you must send it as "42". In our test, passing an integer failed before the request left the machine:
ParamValidationError: Parameter validation failed: Invalid type for parameter UserAttributes[0].Value, value: 43, type: <class 'int'>, valid types: <class 'str'>
Booleans work the same way ("true", not true). Parse values back to numbers or booleans in your code, and remember that string comparison sorts "100" before "42".
Custom attributes at sign-up and in Lambda triggers
- Sign-up API: pass custom attributes in
UserAttributesofSignUp, with thecustom:prefix and string values. The app client needs write permission for them. - Pre sign-up trigger: it can validate the incoming attributes and auto-confirm or auto-verify the user, but it cannot change attribute values. To set an attribute based on sign-up data, use a post confirmation trigger that calls
AdminUpdateUserAttributes. - Admin-created users: pass custom attributes to
AdminCreateUser; this is also the only time an immutable attribute can be set. - Bulk changes: loop over users with
ListUsers(it is paginated) andAdminUpdateUserAttributes. Our boto3 client guide covers the client setup.
Testing tip: mocks may not enforce attribute permissions
We built our test with moto, a library that mocks AWS in Python. It reproduced the default behavior faithfully: no permission lists in DescribeUserPoolClient, custom attributes in the ID token but not the access token, a successful self-service write through a default client, and the string-only rule. It did not enforce an app client's explicit lists: a client with ReadAttributes=['email', 'custom:plan'] and WriteAttributes=['email'] still received custom:role in its ID token, and still let the user write it. Real Cognito leaves the attribute out and returns NotAuthorizedException for the write. So a mock is good for testing your token-parsing code, but test your attribute permissions against a real development user pool, where a wrong list fails the way it will in production.
A checklist for custom attributes
- Name and type every attribute carefully before you add it; you cannot change either later.
- Give each app client an explicit
ReadAttributeslist with exactly what it needs. - Give each app client an explicit
WriteAttributeslist that excludes every attribute used for authorization. - Read claims with the full
custom:name and parse strings to their real types. - Add claims to access tokens with a
V2_0pre token generation trigger, or look them up in your backend. - Change app clients by reading the full configuration first, or through infrastructure as code.
- Test permissions against a real development pool, not only a mock.
Errors you may see with custom attributes
InvalidParameterException: Attribute does not exist in the schema
The name is not a custom attribute in this pool: it is misspelled, missing the custom: prefix, or was never added. Check the pool's attributes and use the full name.
NotAuthorizedException when updating an attribute
The app client has no write permission for that attribute. That is correct for protected attributes; for others, add them to the client's WriteAttributes, or update from your backend with AdminUpdateUserAttributes.
Invalid type for parameter UserAttributes[0].Value
The value is a number or boolean. Send it as a string.
The attribute is on the user but not in the token
The app client cannot read it, the code looks for the name without custom:, or the token predates the change. Fix the read permissions and sign in again.
Sign-in broke for every user after a CLI change
An update-user-pool-client call without the full configuration reset the client's other settings. Restore them from your template or a saved describe-user-pool-client output.
Cognito custom attributes: frequently asked questions
Why is my Cognito custom attribute not in the ID token?
Usually the app client lacks read permission for it. Add it to the client's ReadAttributes (or check Read under Attribute permissions), use the full name custom:name, and sign in again for a fresh token.
How do I add a custom attribute to a Cognito user pool?
In the console under Sign-up, Add custom attributes, or with aws cognito-idp add-custom-attributes. Then grant each app client read and write permissions as needed.
Can I delete or rename a Cognito custom attribute?
No. Custom attributes are permanent: you cannot remove, rename or change their type, mutability or length limits. Add a new attribute if you need a different one.
How do I add a custom attribute to the Cognito access token?
Use a pre token generation Lambda trigger with the V2_0 event, which needs the Essentials or Plus feature plan, and add the claim through claimsAndScopeOverrideDetails accessTokenGeneration.
Can users change their own Cognito custom attributes?
Yes, if their app client can write them, and an app client with no WriteAttributes list can write all attributes. Exclude authorization attributes from WriteAttributes on every app client.
How many custom attributes can a Cognito user pool have?
Up to 50, each with a name of up to 20 characters and values of up to 2,048 characters.
Why are Cognito number attributes returned as strings?
Cognito stores and transmits every custom attribute value as a string, whatever its type. Send "42" and parse it back to a number in your code.
Can a custom attribute be required at sign-up?
No. Only standard attributes can be required, and only when the pool is created. Validate custom attributes yourself, for example in a pre sign-up trigger.
What is the custom: prefix in Cognito?
Cognito names every custom attribute custom: plus the name you chose, and you must use the full name in code, tokens, API calls and rules.
What does an immutable custom attribute mean?
It can only be given a value when the user is created, and cannot be changed afterwards by anyone.
Why doesn't DescribeUserPoolClient show ReadAttributes?
It returns ReadAttributes and WriteAttributes only when they differ from the default. Empty means the client can read and write every attribute.
Does update-user-pool-client keep my other settings?
No. Any setting you leave out is reset to its default, so pass the full existing configuration with your change, or manage the client in CloudFormation or the CDK.
How do I mutate custom attributes in the pre sign-up trigger?
You cannot change attributes there. Use a post confirmation trigger that calls AdminUpdateUserAttributes, or set them when you create the user.
How do I update Cognito user attributes from a server-side Node.js or Python app?
Call AdminUpdateUserAttributes with IAM credentials from your backend, passing the full custom: names and string values. It does not depend on the app client's write list.
Should I use custom attributes or Cognito groups for roles?
Groups for roles and permissions: users cannot change their group membership, and groups appear as cognito:groups in tokens. Use custom attributes for profile data.
Do custom attributes appear in the userInfo endpoint?
The userInfo response follows the scopes in the access token and the app client's read permissions, so a readable custom attribute can appear there when the profile scope allows it.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Security – Specialty (SCS-C03) • Solutions Architect – Associate (SAA-C03).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. A developer added the custom attribute custom:plan to a Cognito user pool. AdminGetUser shows the value, but the app's ID token does not contain it. What is the most likely fix?
Grant the app client read permission for custom:plan (ReadAttributes), then sign in again to get a new token.
2. Users of a Cognito-backed app were able to give themselves an admin role stored in custom:role. How should this be prevented?
Remove custom:role from the WriteAttributes of every app client so only backend code with AdminUpdateUserAttributes can change it, or move roles to Cognito groups.
3. An API authorizes requests with Cognito access tokens and needs the user's subscription plan as a claim. How can the claim be added?
Use a pre token generation Lambda trigger with the V2_0 event (Essentials or Plus feature plan) to add the claim to the access token.
4. Setting a Number-type Cognito custom attribute to 42 from the SDK fails with a type validation error. What should the developer send?
The string "42"; Cognito stores and transmits all custom attribute values as strings.
5. After running update-user-pool-client to change only the readable attributes, users can no longer sign in through that app client. Why?
UpdateUserPoolClient resets every setting that is not supplied to its default, so callback URLs and auth flows were cleared; pass the full existing configuration with the change.
6. An application must set a custom attribute based on data submitted at sign-up, after the user confirms the account. Which trigger is appropriate?
A post confirmation Lambda trigger that calls AdminUpdateUserAttributes; the pre sign-up trigger cannot change attribute values.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
By Friday, the booking app showed the repair-plan badge for the right customers, and Ethan had set explicit read and write lists on all three of the pool's app clients. Jake tried to change his own plan to "lifetime" through the app's profile page. It said no. "Good," Jake said. "Now I can sleep," Ethan said. If your results differ from what this page describes, please tell us. Guides like this stay right because readers write in.
📌 If you keep one line from this page
A custom attribute reaches an app only if that app client may read it, and users can rewrite it themselves unless the client's write list leaves it out, so set both lists explicitly on every app client.
custom: prefix · strings only · 50 per pool · permanent · access tokens need a V2_0 trigger · update-user-pool-client resets omitted settings · October 24, 2026.
Revision note. Written October 24, 2026, from the Cognito API model in boto3 1.43.118 and tests against moto 5.2.4. "Locks keep honest people honest," the old saying goes; an explicit write list is the lock.