Skip to content

feat(cognito): implement user MFA preferences - #2896

Merged
pgermosen merged 2 commits into
floci-io:mainfrom
AbarnaaSree:feat/cognito-mfa-user-preferences
Sep 2, 2026
Merged

pgermosen merged 2 commits into
floci-io:mainfrom
AbarnaaSree:feat/cognito-mfa-user-preferences

Conversation

@AbarnaaSree

Copy link
Copy Markdown
Contributor

Summary

Implements the per-user MFA preference portion of #2433.

Changes

  • Added AdminSetUserMFAPreference

  • Added SetUserMFAPreference

  • Added persisted EmailMfaSettings to CognitoUser

    • Enabled
    • PreferredMfa
  • Added validation to prevent PreferredMfa from being enabled when email MFA is disabled

  • Added access-token validation for SetUserMFAPreference

  • Updated the existing SetUserPoolMfaConfig regression test to expect 404 ResourceNotFoundException for a non-existent user pool

Testing

  • Integration tests pass: 13 tests, 0 failures, 0 errors, 0 skipped
  • git diff --cached --check passes
  • Working tree is clean

Scope

This PR focuses on the per-user MFA preference portion of #2433.

SetUserPoolMfaConfig / GetUserPoolMfaConfig are being handled separately in Part 1.

The EMAIL_OTP authentication challenge remains out of scope as described in #2433.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🎉 Thanks for your first pull request to Floci!

Your CI checks need a maintainer to approve them before they run. That is GitHub's standard gate on first-time contributors, not a problem with your PR — so if the checks look like they are doing nothing, that is why. Once a maintainer approves, CI and the compatibility suite start automatically. Nothing is needed from you in the meantime.

While you wait, a couple of things that make review faster:

  • Link the issue this fixes with Closes #N in the description
  • Commits follow Conventional Commits (feat(s3): ..., fix(dynamodb): ...)
  • Behaviour changes come with a test — see CONTRIBUTING.md

Come join us in Slack — it is the fastest way to reach maintainers if you get stuck, or want feedback on an approach before investing more time in it.

@greptile-apps

greptile-apps Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds administrative and self-service Cognito operations for persisting per-user email MFA preferences.

  • Routes AdminSetUserMFAPreference and SetUserMFAPreference through the Cognito JSON handler.
  • Stores email MFA enabled/preferred state on CognitoUser.
  • Validates preference combinations before mutating persisted state.
  • Registers the new persisted model for reflection and adds administrative-path regression tests.

Confidence Score: 3/5

The PR is not safe to merge until SetUserMFAPreference verifies token authenticity, expiry, and token type before changing a user's MFA settings.

The previously reported token-validation defect remains: claim extraction and revocation checks lead directly to persisted MFA mutation without signature, expiration, or token_use verification.

Files Needing Attention: src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java; src/main/java/io/github/hectorvent/floci/services/cognito/CognitoJsonHandler.java

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoService.java Implements both MFA-preference update paths and fixes rejected-update mutation ordering, but the self-service path still trusts unverified JWT claims.
src/main/java/io/github/hectorvent/floci/services/cognito/CognitoJsonHandler.java Adds JSON dispatch and request parsing for both MFA-preference operations, passing the self-service token directly into the service.
src/main/java/io/github/hectorvent/floci/services/cognito/model/CognitoUser.java Adds persisted email MFA settings to the Cognito user model.
src/main/java/io/github/hectorvent/floci/services/cognito/model/EmailMfaSettings.java Defines the reflection-registered persisted model for enabled and preferred email MFA state.
src/test/java/io/github/hectorvent/floci/services/cognito/CognitoServiceTest.java Covers administrative updates, invalid-update atomicity, and clearing preferred MFA when email MFA is disabled.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Request[Admin or self-service MFA request] --> Route[Cognito JSON handler]
    Route --> Mode{Operation type}
    Mode -->|Admin| Lookup[Look up requested pool and user]
    Mode -->|Self-service| Claims[Extract pool and username from token]
    Claims --> Revocation[Check revocation and global sign-out]
    Revocation --> Lookup
    Lookup --> Validate[Compute and validate MFA preference]
    Validate --> Persist[Persist EmailMfaSettings]
    Persist --> Response[Return success]
Loading

Reviews (3): Last reviewed commit: "fix(cognito): register email MFA setting..." | Re-trigger Greptile

Comment on lines +2965 to +2969
String username = extractUsernameFromToken(accessToken);
String poolId = extractPoolIdFromToken(accessToken);
String jti = extractJtiFromToken(accessToken);

if (username == null || poolId == null || jti == null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Unverified JWT authorizes MFA mutation

When a caller supplies a forged, expired, or wrong-type JWT containing the required claims, setUserMFAPreference trusts those decoded claims and updates the selected user's MFA preference without verifying the signature, expiry, or token_use. This allows unauthorized changes to another user's MFA settings. How this was verified: The request token flows directly through claim-only extraction and revocation checks to userStore.put, with no authenticity, expiry, or token-type validation.

Context Used: AGENTS.md (source)

Comment on lines +3014 to +3027
if (enabled != null) {
settings.setEnabled(enabled);
}

if (preferredMfa != null) {
settings.setPreferredMfa(preferredMfa);
}

if (settings.isPreferredMfa() && !settings.isEnabled()) {
throw new AwsException(
"InvalidParameterException",
"Preferred MFA setting cannot be enabled when the MFA method is disabled.",
400
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rejected updates mutate stored state

When a request sets PreferredMfa=true while email MFA is disabled, this method mutates the live settings object before throwing InvalidParameterException. The rejected preference therefore remains in memory and can be retained by a later partial update, so a failed request still changes the user's effective MFA state.

Context Used: AGENTS.md (source)

@pgermosen pgermosen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AbarnaaSree thanks for taking this on, and welcome, this is your first PR here. A few things to work through before this is ready.

The conflict and the size of the diff both trace back to the same thing: your description says SetUserPoolMfaConfig/GetUserPoolMfaConfig are handled separately in "Part 1," but the diff and commit list here include a full setUserPoolMfaConfig implementation anyway. That's PR #2831, a different contributor's still-open, unmerged PR. It looks like this branch was built on top of #2831 rather than main, so it's carrying those commits as its own, which is both why it conflicts and why the diff is bigger and more confusing than your own description promises. Worth rebasing onto main (once #2831 lands, or cleanly against it in the meantime) so this PR's diff only contains the user-preference work it's actually about.

Once that's sorted, there's a real bug in updateEmailMfaPreference worth fixing: it mutates the live EmailMfaSettings object before the validate-and-throw check runs. I traced this through the actual storage layer rather than taking it on faith, InMemoryStorage.get() returns the literal object reference from the backing map, not a copy, so when a request combines something like Enabled=false with a previously-set PreferredMfa=true and gets rejected, the mutation already happened in place before the exception fires. A later GetUser sees the partially-applied state from a request that was supposed to fail atomically, not the state from before it. Validating before mutating, or mutating a copy and only writing back after validation passes, would fix it.

On the JWT point greptile raised: the code claim is accurate, setUserMFAPreference decodes the token's identity claims with no signature verification, but that's not something this PR introduces. Every existing self-service access-token endpoint in this file, getUser, updateUserAttributes, deleteUserAttributes, does the identical unverified decode already. So this is extending an existing pattern rather than opening a new hole, worth being aware of rather than something to fix in this PR alone. One thing worth knowing, though: AWS's own docs for this specific operation say Cognito doesn't evaluate IAM policies for it at all, authorization is purely the access token's validity, which makes a forgeable token a more direct bypass here than for operations IAM could also gate.

Last thing, not confirmed against a live measurement either way, just an internal inconsistency worth checking: your own setUserPoolMfaConfig method explicitly clears the factor configuration when MFA is turned off, but updateEmailMfaPreference doesn't do the equivalent. Disabling email MFA while it was previously the preferred method throws instead of succeeding, since PreferredMfa isn't cleared along with it. Might be worth applying the same clearing logic there.

@pgermosen pgermosen added the waiting-contributor Deprecated: use waiting-author label Sep 1, 2026
@hectorvent hectorvent added waiting-author Review posted; waiting on the PR author to respond and removed waiting-contributor Deprecated: use waiting-author labels Sep 2, 2026
@AbarnaaSree
AbarnaaSree force-pushed the feat/cognito-mfa-user-preferences branch from 2e8ef99 to e17fb3d Compare September 2, 2026 14:25
@AbarnaaSree

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I’ve rebased the work onto main and cleaned up the PR so it only contains the user MFA preference changes. I also fixed the validation/mutation issue, ensured disabling email MFA clears PreferredMfa, and added regression tests. CognitoServiceTest passes with 172/172 tests.

@pgermosen pgermosen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good progress, both things from last round are genuinely fixed. updateEmailMfaPreference now validates before it mutates anything, and there's a regression test proving the prior state survives a rejected update. Disabling email MFA now clears PreferredMfa instead of throwing too, traced both branches and that's correct, including the case where a request tries to enable PreferredMfa and disable the method in the same call, which still correctly throws.

The thing actually blocking the build right now is PersistedModelReflectionTest failing on EmailMfaSettings, it's missing @RegisterForReflection. It's a field on the persisted CognitoUser now, so it's reachable from storage, and every sibling model class in this service carries that annotation, CognitoUser itself does. Adding it should clear the build. While you're in there, @JsonIgnoreProperties(ignoreUnknown = true) would match the sibling classes too, though that one isn't what's failing.

One thing carried over from last round, not new: setUserMFAPreference still authorizes off an access token with no signature check, same as every other self-service endpoint in this file already does, so not something to fix here, just flagging again for completeness.

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@AbarnaaSree

Copy link
Copy Markdown
Contributor Author

The reflection fix is pushed in 970c33e. All 19 CI checks are now passing, including PersistedModelReflectionTest and CognitoServiceTest (172/172).

@pgermosen pgermosen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the fix, confirmed. Both annotations added, matching the sibling classes, and CI is green now. Nothing else outstanding.

@pgermosen pgermosen removed the waiting-author Review posted; waiting on the PR author to respond label Sep 2, 2026
@pgermosen
pgermosen merged commit 82ee360 into floci-io:main Sep 2, 2026
19 checks passed
@hectorvent

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cognito Amazon Cognito enhancement New feature or request released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants