Authsignal secures millions of passkey transactions out of our hosted Sydney region.

Authsignal secures millions of passkey transactions out of our hosted Sydney region.

Join us today!
Blog
/
Current article

How to implement adaptive MFA with passkeys for smarter, risk-based authentication

Last Updated:
July 9, 2025
Ashutosh Bhadauriya
How to implement adaptive MFA with passkeys for smarter, risk-based authentication
AWS Partner
Authsignal is an AWS-certified partner and has passed the Well-Architected Review Framework (WAFR) for its Cognito integration.
AWS Marketplace

In our previous blog, we explored how you can use passkeys for step-up authentication, providing biometric verification for high-value transactions. Today, we're taking that concept further by diving into adaptive MFA, where security decisions are made intelligently based on real-time risk assessment rather than fixed rules.

Imagine you're using your banking app at home, transferring $500 to pay rent. The transaction goes through seamlessly. Later, you're on vacation connecting through a public WiFi to send $100 to a friend, and suddenly you're prompted for additional verification. That's adaptive MFA in action.

Unlike traditional step-up authentication that only looks at transaction amounts, adaptive MFA considers multiple risk factors: Are you connecting from an anonymous IP? Are you in an unusual location? Is this transaction pattern normal for you? A $10,000 transfer from your home IP might be perfectly safe, while a $50 transfer from an anonymous VPN could signal account compromise.

What we're building

We’ll build a smart money transfer app that adapts its security based on real-time risk assessment. Here's how our adaptive MFA in the app works:

Anonymous/risky IPs: Always require passkey verification for connections from VPNs, Tor, or suspicious IP rangesTrusted IPs + high value: Require verification for transactions over $2,000 from known good IPsTrusted IPs + low value: Allow transactions to proceed seamlessly

This way, your regular IP addresses will become trusted over time, while suspicious network activity gets flagged immediately.

Architecture overview

The system has four key components:

IP Risk Assessment: Authsignal automatically identifies anonymous IPs, VPNs, Tor networks, and suspicious IP ranges.

Risk Assessment: Every transaction is evaluated against configurable rules that consider IP reputation, transaction amount, and other risk factors.

Adaptive Challenge: When risk is detected, users get a passkey prompt. Low-risk transactions flow through without friction.

Continuous Learning: The system learns IP patterns over time, reducing false positives while maintaining security.

Set up rules in Authsignal

Before we dive into the code, we need to configure our adaptive MFA rules in the Authsignal Portal.

Create the action: Navigate to the Actions section in your Authsignal dashboard and create a new action called adaptive-mfa-transfer.

Create and configure rules: Head to the Rules tab and set up your risk-based conditions. Set the outcome to "Challenge" and add these conditions. Don’t forget to set the rule outcome to ‘Challenge’.

  • IP is anonymous = True → Challenge
  • transactionValue > 2000 → Challenge

This means users will be prompted for verification when connecting from anonymous IPs or when making high-value transfers from trusted networks.

Implementation

Now, let's build the app:

Install packages:

npm install @authsignal/node @authsignal/browser

Grab your Authsignal credentials:

// lib/authsignal.ts
import { Authsignal } from '@authsignal/node'

export const authsignal = new Authsignal({
  apiSecretKey: process.env.AUTHSIGNAL_API_SECRET_KEY,
  apiUrl: 'https://api.authsignal.com/v1', // your region api url
})

export const authsignalConfig = {
  tenantId: process.env.NEXT_PUBLIC_AUTHSIGNAL_TENANT_ID,
  baseUrl: 'https://api.authsignal.com/v1', // your region api url
}

Passkey enrollment

Before adaptive MFA can work, users need passkeys enrolled.

Backend enrollment endpoint:

// api/passkey/enroll/route.ts
export async function POST(request: NextRequest) {
  const userId = await getUserId()

  // Tell Authsignal we want to start passkey enrollment
  const response = await authsignal.track({
    userId,
    action: 'enroll-passkey',
    attributes: { scope: 'add:authenticators' },
  })

  return NextResponse.json({ token: response.token })
}

Frontend enrollment flow:

// components/passkey-manager.tsx
const enrollPasskey = async () => {
  try {
    // Step 1: Get enrollment token
    const challengeResponse = await fetch('/api/passkey/enroll', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${authToken}` }
    })
    const { token } = await challengeResponse.json()

    // Step 2: Create passkey
    const passkeyResponse = await authsignalClient.passkey.signUp({
      token,
      username: userEmail,
    })

    // Step 3: Validate enrollment
    if (passkeyResponse.data?.token) {
      const validationResponse = await fetch('/api/passkey/validate', {
        method: 'POST',
        body: JSON.stringify({ token: passkeyResponse.data.token }),
      })

      if (validationResponse.ok) {
        setIsEnrolled(true)
        setMessage('Passkey successfully enrolled!')
      }
    }
  } catch (error) {
    console.error('Enrollment failed:', error)
  }
}

Adaptive MFA Implementation

Transaction verification:

// api/passkey/verify-transaction/route.ts
export async function POST(request: NextRequest) {
  const userId = await getUserId()
  const { amount, recipient } = await request.json()
  
  // Get client context
  const userAgent = request.headers.get('user-agent') || ''
  const ipAddress = request.headers.get('x-forwarded-for') || 
                   request.headers.get('x-real-ip')

  // Let Authsignal evaluate risk and decide
  const response = await authsignal.track({
    userId,
    action: 'adaptive-mfa-transfer',
    attributes: {
      userAgent,
      ipAddress,
      custom: {
        transactionValue: amount
      }
    }
  })

  if (response.state === 'CHALLENGE_REQUIRED') {
    return NextResponse.json({
      requiresVerification: true,
      token: response.token,
      message: `Security verification required for ${amount.toFixed(2)} transfer`
    })
  } else if (response.state === 'ALLOW') {
    return NextResponse.json({
      requiresVerification: false,
      message: 'Transaction approved'
    })
  }
}

Challenge the user

// components/passkey-challenge.tsx
const performPasskeyVerification = async () => {
  try {
    // Perform passkey authentication
    const passkeyResponse = await authsignalClient.passkey.signIn({
      action: 'adaptive-mfa-transfer'
    })

    if (passkeyResponse.data?.token) {
      // Validate the verification
      const validationResponse = await fetch('/api/passkey/validate', {
        method: 'POST',
        body: JSON.stringify({ token: passkeyResponse.data.token }),
      })

      const validationData = await validationResponse.json()

      if (validationData.isValid) {
        onVerificationComplete(true, 'Verification successful! Transaction approved.')
      }
    }
  } catch (error) {
    // Handle error
  
  }
}

Different adaptive MFA scenarios

Scenario 1: Anonymous IP, any amount ($100)

  1. User connects via VPN and enters transfer details
  2. Authsignal detects anonymous/VPN IP address
  3. IP is anonymous = True rule triggers
  4. User prompted for passkey verification
  5. After successful verification, transaction proceeds

Scenario 2: Trusted IP, high amount ($3,000)

  1. User enters large transfer from home IP
  2. Authsignal recognizes trusted IP but sees high amount
  3. transactionValue > 2000 rule triggers
  4. User prompted for passkey verification
  5. Transfer proceeds after verification

Scenario 3: Trusted IP, normal amount ($500)

  1. User enters regular transfer from home/office IP
  2. Authsignal recognizes trusted IP and sees normal amount
  3. No rules trigger
  4. Transfer proceeds immediately

Scenario 4: Tor network, any amount

  1. User connects through Tor browser
  2. Authsignal detects Tor exit node IP
  3. IP is anonymous = True rule triggers
  4. Verification required regardless of amount

Extensions and customization

You can enhance adaptive MFA with additional risk signals:

Geographic risk: New countries or impossible travel detection

Network analysis: VPN detection and IP reputation scoring

Velocity checks: Rapid successive transactions from different devices

Business context: Different thresholds for different user types or account balances

Wrapping up

That's it! We've built a smart money transfer app with adaptive MFA that responds intelligently to risk. A $10,000 transfer from your home IP is fundamentally different from a $100 transfer from an anonymous VPN. Adaptive MFA recognizes these differences and responds appropriately.

Check out the Authsignal adaptive MFA docs to start implementing it in your app!

Try out our passkey demo
Passkey Demo
Have a question?
Talk to an expert
You might also like
June 2025 SMS OTP regulatory updates: Banking's global shift to secure authentication
Governments worldwide are banning SMS OTPs due to rising fraud risks. Learn why regulators in the Philippines, UAE, and EU are cracking down, and what secure alternatives banks must adopt to stay compliant and protect customers.
Apple's WWDC25 Passkey Updates: Fast Forwarding The Journey To Passwordless
At WWDC 2025, Apple announced powerful passkey updates that streamline account creation, sync credentials, and automate upgrades, making passwordless login easier than ever. Discover the five key features and what they mean for developers, users, and the future of secure authentication.
June Update - AI Authentication Logic, Fast Backtesting, Advanced Banking Demo
Discover how Authsignal’s new AI assistant helps you understand complex rule logic in plain English, debug faster with rule backtesting, and fine-tune OTP rate limits—all from your dashboard.

Secure your customers’ accounts today with Authsignal.