Guide · AWS Cognito · Advanced Security

AWS Cognito Advanced Security in MCP Servers — Adaptive Authentication, Risk Scoring, Compromised Credentials

Cognito Advanced Security Features (ASF) adds risk-based authentication on top of standard sign-in without requiring any changes to your MCP server's authentication code. For each authentication event, Cognito evaluates device fingerprint, location history, IP reputation, and velocity signals to assign a risk level: LOW, MEDIUM, or HIGH. Your configured response per risk level can be: allow (no friction), optional MFA challenge, or block. Separately, compromised credentials protection checks submitted passwords against a continuously updated breach database — if a password appears in known data dumps, Cognito can block or notify. This guide covers enabling ASF, configuring risk responses, reading security events from CloudWatch Logs Insights, and using the event feedback API to teach Cognito which sign-ins were legitimate even when they scored HIGH risk.

TL;DR

Enable ASF by calling UpdateUserPoolCommand with UserPoolAddOns: { AdvancedSecurityMode: 'ENFORCED' } (or 'AUDIT' for logging without enforcement). Configure RiskConfiguration via SetRiskConfigurationCommand to define per-risk-level responses. Compromised credentials protection uses CompromisedCredentialsRiskConfiguration with Actions: { EventAction: 'BLOCK' }. Call UpdateAuthEventFeedbackCommand with FeedbackValue: 'Valid' or 'Invalid' to correct misclassified risk scores. ASF costs $0.050 per MAU beyond the free tier — budget for it when planning billing.

Enabling Advanced Security Features

import {
  CognitoIdentityProviderClient,
  UpdateUserPoolCommand,
  AdvancedSecurityModeType,
} from '@aws-sdk/client-cognito-identity-provider';

const cognito = new CognitoIdentityProviderClient({ region: 'us-east-1' });

// AUDIT mode: evaluate risk and log events, but don't enforce responses
// ENFORCED mode: evaluate risk and apply configured responses (block, MFA challenge, etc.)
// Start with AUDIT to observe risk distribution before switching to ENFORCED
await cognito.send(new UpdateUserPoolCommand({
  UserPoolId: 'us-east-1_XXXXXXXXX',
  UserPoolAddOns: {
    AdvancedSecurityMode: AdvancedSecurityModeType.AUDIT,  // switch to ENFORCED after baselining
  },
}));

Start with AUDIT mode for 1–2 weeks before switching to ENFORCED. In AUDIT mode, Cognito evaluates and logs risk levels for every sign-in event but takes no enforcement action — this lets you observe what percentage of your users' sign-ins score HIGH risk before you block any. Common reason for unexpectedly high scores in AUDIT: users accessing from VPNs or corporate proxies whose IP ranges Cognito has not seen before.

ModeRisk evaluatedResponse enforcedEvents logged
OFFNoNoNo
AUDITYesNoYes
ENFORCEDYesYesYes

Configuring risk response actions

import {
  SetRiskConfigurationCommand,
  RiskDecisionType,
  CompromisedCredentialsEventActionType,
} from '@aws-sdk/client-cognito-identity-provider';

await cognito.send(new SetRiskConfigurationCommand({
  UserPoolId: 'us-east-1_XXXXXXXXX',
  // Leave ClientId blank to set pool-level defaults; provide ClientId for per-client overrides

  // Adaptive authentication: response per risk level
  AccountTakeoverRiskConfiguration: {
    Actions: {
      LowRiskResponse: {
        Notify: false,
        EventAction: RiskDecisionType.NO_ACTION,  // low risk — allow silently
      },
      MediumRiskResponse: {
        Notify: true,                              // send notification email to user
        EventAction: RiskDecisionType.MFA_IF_CONFIGURED,  // prompt MFA if enabled
      },
      HighRiskResponse: {
        Notify: true,
        EventAction: RiskDecisionType.BLOCK,       // block sign-in, require support contact
      },
    },
    NotifyConfiguration: {
      From: 'security@myapp.com',
      ReplyTo: 'support@myapp.com',
      SourceArn: 'arn:aws:ses:us-east-1:123456789012:identity/myapp.com',
      BlockEmail: {
        Subject: 'Sign-in to your account was blocked',
        HtmlBody: '<p>A sign-in to your account was blocked due to suspicious activity. If this was you, contact support.</p>',
        TextBody: 'A sign-in to your account was blocked. Contact support if this was you.',
      },
      MfaEmail: {
        Subject: 'Additional verification required',
        HtmlBody: '<p>Unusual sign-in detected. Please complete MFA to continue.</p>',
        TextBody: 'Unusual sign-in detected. Please complete MFA.',
      },
      NoActionEmail: {
        Subject: 'New sign-in to your account',
        HtmlBody: '<p>A new sign-in was detected from an unrecognized device.</p>',
        TextBody: 'A new sign-in was detected from an unrecognized device.',
      },
    },
  },

  // Compromised credentials: check against breach databases
  CompromisedCredentialsRiskConfiguration: {
    EventFilter: ['SIGN_IN', 'SIGN_UP', 'PASSWORD_CHANGE'],
    Actions: {
      EventAction: CompromisedCredentialsEventActionType.BLOCK,
    },
  },
}));

MFA_IF_CONFIGURED only challenges the user if they have MFA set up on their account — it does not force MFA enrollment. For higher-security deployments where you require MFA for medium/high-risk sign-ins, set MFA_REQUIRED to force MFA enrollment before allowing the sign-in. Pair this with user pool MFA settings (SmsConfiguration or EnabledMfas: ['SOFTWARE_TOKEN_MFA']).

Reading security events from CloudWatch

// CloudWatch Logs Insights query — run against the Cognito user pool event log group
// Log group name: /aws/cognito/userpools/us-east-1_XXXXXXXXX/<pool-name>

/*
fields @timestamp, eventType, riskDecision, riskLevel, userId, ipAddress
| filter eventType = "Authentication"
| filter riskLevel in ["HIGH", "MEDIUM"]
| sort @timestamp desc
| limit 100
*/

// Using the AWS SDK to run the query programmatically:
import { CloudWatchLogsClient, StartQueryCommand, GetQueryResultsCommand } from '@aws-sdk/client-cloudwatch-logs';

const cwl = new CloudWatchLogsClient({ region: 'us-east-1' });

const { queryId } = await cwl.send(new StartQueryCommand({
  logGroupName: '/aws/cognito/userpools/us-east-1_XXXXXXXXX/my-pool',
  startTime: Math.floor(Date.now() / 1000) - 86400,  // last 24h
  endTime: Math.floor(Date.now() / 1000),
  queryString: `
    fields @timestamp, eventType, riskDecision, riskLevel, userId, ipAddress
    | filter eventType = "Authentication"
    | filter riskLevel in ["HIGH", "MEDIUM"]
    | sort @timestamp desc
    | limit 200
  `,
}));

// Poll until complete
let results;
while (true) {
  const { status, results: r } = await cwl.send(new GetQueryResultsCommand({ queryId }));
  if (status === 'Complete') { results = r; break; }
  await new Promise(resolve => setTimeout(resolve, 1000));
}

Cognito logs security events to a user-pool-specific CloudWatch Logs group. The log group is not created until you enable ASF — it will not exist if the pool is in OFF mode. Security event logs include the risk level, risk decision, contextual data used for scoring (device fingerprint hash, IP geolocation), and the final authentication outcome. Retention is 90 days by default; set a shorter retention policy to control costs if you have high sign-in volume.

Event feedback API: correcting misclassified risk scores

import {
  AdminGetUserCommand,
  ListUserAuthEventsCommand,
  UpdateAuthEventFeedbackCommand,
} from '@aws-sdk/client-cognito-identity-provider';

// Fetch recent auth events for a user
const { AuthEvents } = await cognito.send(new ListUserAuthEventsCommand({
  UserPoolId: 'us-east-1_XXXXXXXXX',
  Username: 'user-sub-uuid',
  MaxResults: 10,
}));

// Find the blocked event the user is disputing
const blockedEvent = AuthEvents?.find(e =>
  e.EventType === 'SignIn' &&
  e.EventRisk?.RiskDecision === 'Block'
);

if (blockedEvent?.EventId) {
  // Support agent confirms this was a legitimate sign-in (corrects the model)
  await cognito.send(new UpdateAuthEventFeedbackCommand({
    UserPoolId: 'us-east-1_XXXXXXXXX',
    Username: 'user-sub-uuid',
    EventId: blockedEvent.EventId,
    FeedbackToken: blockedEvent.EventFeedback?.FeedbackToken ?? '',
    FeedbackValue: 'Valid',  // 'Valid' = legitimate; 'Invalid' = was actually an attack
  }));
}

The event feedback API feeds signal back into Cognito's per-user risk model. When support confirms a blocked sign-in was legitimate, sending FeedbackValue: 'Valid' adjusts the model for that user — subsequent sign-ins from the same device/location will score lower. This is critical for users accessing from corporate VPNs or who frequently travel: the first sign-in from a new IP scores high, support unblocks and marks valid, subsequent sign-ins from that IP score lower.

After unblocking a user via feedback, you may also need to call AdminResetUserPasswordCommand if the account was locked, or AdminEnableUserCommand if the account was disabled by the BLOCK response. The BLOCK action does not automatically disable the Cognito user account — it rejects the authentication session but the account remains active.