- Newest
- Most votes
- Most comments
Both answers your team received are partly right. The confusion comes from a missing component rather than a limitation of the trigger.
The Pre Token Generation trigger cannot grant IAM permissions. It only shapes claims in the JWTs your user pool issues. A user-pool token is an OIDC token, not a set of SigV4 AWS credentials, so no amount of claim customization lets the desktop app call S3 directly.
But the architecture you described is supported. The missing piece is a Cognito identity pool, which is the component that exchanges a user-pool token for temporary AWS credentials. AWS documents this as an intended use of the trigger — with ID token customization you can "make a change at runtime to the IAM role that your user requests from an identity pool" (Pre token generation Lambda trigger).
WinForms app
└─ user-pool sign-in ──► Pre Token Generation Lambda ──► SQL lookup
│ injects claim, e.g. "dept": "legal"
▼
ID token ◄── the same token you already send to your API
│
▼
Identity pool: GetId + GetCredentialsForIdentity
claim ──► session tag (principal tag)
│
▼
temporary AWS credentials ──► S3
The exchange happens inside the AWS SDK using the ID token you already hold. No second sign-in, no explicit AssumeRole in your client — which meets your requirement of keeping one Cognito session.
Check your feature plan first
Before you build this, confirm which feature plan your user pool is on (Cognito console → your user pool → Overview, or DescribeUserPool and read UserPoolTier).
The Pre Token Generation trigger requires Essentials or Plus. Pools that had Advanced Security Features enabled on or before 22 November 2024 and remain on Lite keep access to event versions one and two, but a Lite pool without that history cannot use the trigger at all.
This is not a blocker either way — it just decides which of the paths below you take. Skip to "If you're on Lite" below if the trigger isn't available to you; the identity pool half of the architecture is unchanged and works on every plan.
Scoping the S3 access
Option A — ABAC with principal tags. The best fit for per-user or per-department prefixes.
In the identity pool, select your user-pool identity provider, then under Attributes for access control choose custom mappings and map the claim carrying your attribute (dept) to a tag key (dept). Cognito applies it as a session tag, and a single IAM role covers every user:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::my-bucket", "Condition": { "StringLike": { "s3:prefix": "${aws:PrincipalTag/dept}/*" } } }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::my-bucket/${aws:PrincipalTag/dept}/*" } ] }
The role's trust policy must grant both sts:AssumeRoleWithWebIdentity and sts:TagSession to the cognito-identity.amazonaws.com principal. Omitting sts:TagSession is the most common reason this fails on first attempt. Full walkthrough and example trust policy: Using attributes for access control.
Option B — RBAC with role selection. Use this if your tiers need genuinely different policies rather than different prefixes. Have the Lambda set preferredRole and iamRolesToOverride in groupOverrideDetails, then set the identity pool's authenticated role selection to Choose role from token. Those claims are writable in every trigger event version. Alternatively, rule-based mapping can match any claim to a role, but you're limited to 25 rules per provider, so it doesn't scale to many attribute values. See Using role-based access control.
Start with Option A. It avoids creating a new role per attribute value.
If you're on Lite and can't use the trigger
The trigger is a convenience for getting an external attribute into the token at sign-in time. It isn't the only way, and both alternatives below work on any feature plan because groups and custom attributes appear in the ID token natively.
Sync the attribute into an immutable custom attribute. Add custom:dept to your user pool schema with Mutable: false, and have your provisioning process (or a scheduled job reading the same SQL database) write it via AdminUpdateUserAttributes. It then appears in the ID token and both Option A and Option B work against it unchanged. The trade-off is staleness — the value updates on your sync schedule rather than at each sign-in.
Use Cognito groups. If your SQL attributes reduce to a handful of access tiers, create a user pool group per tier with an IAM role attached, and manage membership with AdminAddUserToGroup from your existing system. Cognito populates cognito:preferred_role and cognito:roles natively, and Option B works with no Lambda involved. This is the simplest path if your tiers are few and stable.
Or move the decision to your API — see the last section.
Upgrading to Essentials is of course also an option; check Cognito pricing against your MAU count before deciding, since the trigger may not justify the tier on its own.
Things to get right whichever path you take
Sanitize the values. Attribute values become IAM policy variables, and * and ? are wildcards in Resource elements and StringLike conditions. A user whose dept resolves to * could match far more than intended. Validate before the value reaches the token.
Never source authorization from a user-writable attribute. This is the important one. If you use a custom attribute, define it Mutable: false and remove it from your app client's WriteAttributes, otherwise any user can change their own dept via UpdateUserAttributes and escalate their S3 access. AWS calls this out for both ABAC and RBAC. Reading from your SQL database inside a Lambda avoids the problem entirely, which is a point in favour of the trigger if you can use it.
Credential lifetime. Identity pool credentials last up to an hour, so a permission change won't take effect until the client refreshes. If you use the trigger, it does fire on token refresh (TokenGeneration_RefreshTokens), so refreshed sessions pick up new values.
Lambda latency, if you use the trigger. It sits in the critical path of every token issuance with a 5-second timeout. If your SQL database is in a VPC, account for cold starts and connection setup, and consider caching or RDS Proxy.
On the .NET client
Use the credentials provider in Amazon.CognitoIdentity to feed the user-pool ID token into the identity pool exchange, or call GetId and GetCredentialsForIdentity directly. Note that AWS SDK for .NET V3 has reached end-of-support, so target V4 if you can.
A desktop app is a public client, so don't embed any secret. That's fine here — S3 authorization is enforced server-side by IAM against the session tag or role, never by the client.
A simpler alternative worth weighing
Your API already validates the user-pool token, so it could keep AWS credentials off end-user machines entirely by issuing pre-signed S3 URLs, or by calling AssumeRole with a session policy on the user's behalf. Smaller blast radius, one auditable place for authorization decisions, and no dependency on your Cognito feature plan. The cost is more API round-trips. If your files are large or numerous, the identity pool approach is the better trade.
answered a month ago
No, a Pre Token Generation trigger cannot grant IAM permissions. It only edits the claims in your ID and access tokens, and those are OIDC tokens, not AWS credentials. So on its own it can never get the desktop app into S3.
What you want does work though. The missing piece is a Cognito Identity Pool. That is what takes the user-pool token and swaps it for real, temporary AWS credentials. The trigger still helps: the claims it writes are what the Identity Pool uses to pick the role and scope the access. And no, the user does not sign in twice, and the app does not call AssumeRole itself. You reuse the same ID token you already send to your company API.
The flow looks like this:
App signs in -> User Pool -> Pre Token Gen Lambda looks up the user in SQL and adds claims to the ID token
App -> Identity Pool (GetId + GetCredentialsForIdentity, passing that ID token)
Identity Pool -> STS (AssumeRoleWithWebIdentity + TagSession) -> temporary creds (~1 hour)
App -> S3, where the IAM policy checks s3:prefix against the user's tag
The Lambda. It runs during sign-in, so it can read your SQL database and the user never touches the result. If you want per-prefix control, add the attributes as custom claims:
{ "response": { "claimsAndScopeOverrideDetails": { "idTokenGeneration": { "claimsToAddOrOverride": { "dept": "legal", "s3_prefix": "projects/legal/" } } } } }
If your users just fall into a few groups (read-only, read-write, admin), it is simpler to let the trigger pick the role directly with iamRolesToOverride and preferredRole. Those land in the cognito:roles / cognito:preferred_role claims, which is how the trigger points the Identity Pool at the right role.
Quick cost note: the ID-token customization you need (V1_0) is on every Cognito tier, including Lite. You do not need the access-token version that requires Essentials or Plus, because the Identity Pool reads the ID token.
Getting credentials. This is the part that was missing. Two SDK calls, shown in C#:
// Once per user, then cache the IdentityId var identityId = (await cognitoClient.GetIdAsync(new GetIdRequest { IdentityPoolId = "us-east-1:IDENTITY_POOL_ID", Logins = new Dictionary<string, string> { { "cognito-idp.us-east-1.amazonaws.com/us-east-1_USERPOOLID", idToken } } })).IdentityId; // Again whenever the creds expire, roughly hourly var creds = (await cognitoClient.GetCredentialsForIdentityAsync(new GetCredentialsForIdentityRequest { IdentityId = identityId, Logins = new Dictionary<string, string> { { "cognito-idp.us-east-1.amazonaws.com/us-east-1_USERPOOLID", idToken } } })).Credentials; // AccessKeyId, SecretKey, SessionToken -> good for about an hour
The IAM role. The trust policy has to allow sts:TagSession as well as the assume action, or the session tags never come through:
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "cognito-identity.amazonaws.com" }, "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"], "Condition": { "StringEquals": { "cognito-identity.amazonaws.com:aud": "us-east-1:YOUR-IDENTITY-POOL-ID" }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" } } }] }
And one permissions policy covers everyone, because the prefix comes from the user's own tag:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::my-data-bucket", "Condition": { "StringLike": { "s3:prefix": "${aws:PrincipalTag/s3_prefix}*" } } }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::my-data-bucket/${aws:PrincipalTag/s3_prefix}*" } ] }
A user whose s3_prefix is projects/legal/ only sees s3://my-data-bucket/projects/legal/*. When their access changes, you update the SQL row. No IAM edits, no redeploy.
Wiring the claims to the tags. In the Identity Pool console, under Attributes for access control, map dept to a department tag, s3_prefix to an s3_prefix tag, and so on. When credentials are issued, the pool calls STS with TagSession and ${aws:PrincipalTag/s3_prefix} resolves to that user's value.
RBAC or ABAC? If you have a handful of fixed profiles, use RBAC (one role each, picked via preferredRole). If you need real per-user prefix scoping, use ABAC (session tags plus ${aws:PrincipalTag} in the policy).
Two things to watch out for:
- Do not map any attribute the user can edit to a principal tag. If they can change it through
UpdateUserAttributes, they can walk past your SQL gating. Keep those values coming from the Lambda and mark the custom attributes as not mutable. - Sanitize the SQL values before putting them in a claim. A stray
*or?acts as a wildcard in thoseStringLikeconditions, and a "prefix" could quietly become "everything."
Useful docs:
- Pre Token Generation Lambda trigger, which lists changing the IAM role at runtime as a supported use case
- Identity Pools authentication flow
- Using attributes for access control (ABAC)
answered a day ago

In our use case, a user may be a member of an arbitrary number of “departments,” and the set of departments itself is dynamic. A user could therefore have access to many department-specific S3 prefixes rather than being associated with a single
deptvalue.Is this still possible to implement using ABAC with Cognito identity pools and principal/session tags? If so, what would the recommended approach be for representing multiple department memberships in the IAM policy?
It is also worth noting that the application is designed to manage one department at a time. It would be perfectly acceptable for the user to reauthenticate when switching to a different department. This means we do not necessarily need a single set of AWS credentials to provide simultaneous access to every department the user belongs to (though that would be preferable).