Per-App MFA Policies
ZewstID lets you tune multi-factor authentication on a per-application basis. Some apps need MFA on every sign-in (banking, healthcare). Some only need it when something looks risky (most consumer apps). And some don't need it at all.
The MFA policy engine combines three signals to make a decision for each login:
- Portal detection — first-party ZewstID portals always require MFA.
- Per-app policy — the policy you set in the Developer Portal: ,
always, orsmart.never - Risk assessment — device fingerprint, IP reputation, location, and travel velocity.
This guide walks through each policy, how Smart MFA decides when to challenge, how to configure it, and how to use step-up authentication for sensitive actions.
The Three Policies
Every application has an
mfaPolicysmartalways — MFA on Every Sign-In
alwaysEvery sign-in triggers MFA. The user enters their password (or completes magic link / OTP / OAuth), and immediately gets prompted for a second factor — TOTP code or push approval.
Use this for:
- Banking, payments, financial apps
- Healthcare and patient data
- Internal admin tools that touch production
- Anything where the cost of a single account compromise is catastrophic
Trade-off: higher friction. Users complete MFA on every device, every session, every day. Expect some support tickets about lost authenticators.
smart — Risk-Based MFA (Default)
smartMFA only triggers when something looks off. If the user is on a known device, on a known IP, in their usual region, and recent risk signals are clean — MFA is skipped. Otherwise, they're prompted.
Use this for:
- Most consumer-facing apps
- SaaS dashboards
- Anything where the goal is "good enough security with minimal friction"
Trade-off: users on new devices or traveling will get challenged. That's the point — the system trades occasional prompts for low day-to-day friction.
never — MFA is Never Triggered During Sign-In
neverUsers can still enroll TOTP and push devices in their account settings, but those methods are never required to sign in.
Use this for:
- Low-sensitivity apps where you've explicitly decided MFA isn't needed
- Read-only public-data apps
- Apps where you've moved authentication to another layer (e.g. you're behind a VPN)
Warning: even with
neversmartZewstID Portals Always Require MFA
The four first-party portals always enforce MFA, regardless of any per-app policy:
- (account.zewstid.com)
user-portal - (developers.zewstid.com)
developer-portal - (admin.zewstid.com)
admin-dashboard - (orgs.zewstid.com)
org-portal
This is a hard security boundary. These portals control:
- The user's primary identity, password, and enrolled MFA methods
- OAuth client credentials, API keys, and webhook secrets
- Tenant-wide settings: SSO, SCIM, branding, domain verification
- Platform admin operations
A compromise at the portal layer is a compromise of every downstream app. Allowing developers to disable MFA for these portals would let one weak account undo the security posture of every app and tenant they own. So it's not configurable.
If your users complain about prompts on the portal, the answer is enroll a passkey or push device for low-friction MFA — not weaken the policy.
How Smart MFA Decides
When
smart| Signal | Behavior |
|---|---|
| New device or browser fingerprint | MFA required |
| New IP address | MFA required |
| New geographic location | MFA required |
| Trusted device expired (30 days since last MFA on that device) | MFA required |
| Impossible travel (e.g. NYC then Tokyo 30 min later) | MFA required |
| Risk score ≥ 30 | MFA required |
| Risk score ≥ 80 | Login blocked entirely |
| Known device + known IP + low risk score | MFA skipped |
The decision is reflected in the response from
POST /api/v1/auth/mfa/status/check{ "mfaRequired": true, "mfaSessionId": "a1b2c3d4...", "methods": { "totp": true, "push": true } }
If the risk score is critical (≥ 80), the engine returns a 403 instead and writes an audit log entry:
{ "error": "access_denied", "error_description": "Login blocked due to suspicious activity. Please try again later." }
Trusted Devices
After a successful MFA verification, the device fingerprint is marked as trusted for that user for 30 days. Within that window, future sign-ins from the same device + IP combination skip MFA (assuming risk stays low).
If the device hasn't been seen in 30 days, it expires and the next sign-in re-challenges. This is intentional — a stolen laptop you forgot about shouldn't get a forever pass.
Fail-Safe Behavior
If the risk service is temporarily unavailable, Smart policy defaults to requiring MFA. We'd rather inconvenience a user than silently grant access during an outage.
Configuring the Policy
In the Developer Portal
- Sign in to developers.zewstid.com
- Open Applications → Your App → Settings
- Scroll to MFA Policy
- Pick a radio option: Smart (Recommended), Always, or Never
- Click Save Policy
The policy is cached in Redis for 5 minutes, so changes take effect within that window. To force an immediate change, restart the API gateway or wait 5 minutes.
Via the API
You can also update the policy programmatically:
curl -X PATCH https://api.zewstid.com/api/v1/applications/$APP_ID \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mfaPolicy": "always"}'
Valid values:
"smart""always""never"Step-Up Authentication
For sensitive actions mid-session — payments, data exports, password changes, deleting resources — you can require a fresh MFA verification even if the user is already signed in. This is called step-up authentication.
How It Works
recordMFA()user:{id}:mfa:last- If the user MFA'd within the last 5 minutes, step-up is auto-approved (no prompt).
- Otherwise, step-up returns a fresh MFA session ID, and the user must verify again.
Request a Step-Up
curl -X POST https://api.zewstid.com/api/v1/auth/mfa/step-up \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"action": "payment"}'
The
actionpaymentdata-exportchange-passworddelete-accountResponse — Recently MFA'd
{ "stepUpRequired": false, "verified": true, "reason": "MFA was recently completed" }
You can proceed with the sensitive action immediately.
Response — Step-Up Required
{ "stepUpRequired": true, "mfaSessionId": "f0e1d2c3...", "methods": { "totp": true, "push": true }, "expiresIn": 300 }
Your client should then prompt the user to complete MFA, just like it does on initial sign-in.
Verify the Step-Up
Use the same
/mfa/verifycurl -X POST https://api.zewstid.com/api/v1/auth/mfa/verify \ -H "Content-Type: application/json" \ -d '{ "mfaSessionId": "f0e1d2c3...", "method": "totp", "code": "123456" }'
For push verification, initiate the challenge first via
/mfa/challenge/pushchallengeIdOn success:
{ "verified": true, "userId": "user-uuid" }
The user is now considered freshly MFA'd for another 30 minutes.
When to Use Step-Up
Use step-up whenever the cost of an action is meaningfully higher than reading data:
- Payments: before charging a card, transferring money, or initiating a payout
- Data exports: before generating a full account export (GDPR-style)
- Settings changes: before changing email, password, or disabling MFA
- Destructive actions: before deleting an account, an organization, or production resources
- API key generation: before issuing new credentials
Don't use step-up for routine reads or low-risk writes — it just trains users to click through prompts.
Risk-Based Blocking
Smart MFA can do more than challenge — for high enough risk scores, it blocks the login entirely. This is for cases where MFA alone isn't enough signal:
- Brand-new device, brand-new IP, brand-new country
- Impossible travel from the last sign-in
- IP on known threat intel feeds
- Repeated failed attempts from the same fingerprint
When the risk score crosses 80, the engine returns:
{ "blocked": true, "reason": "Login blocked: new device, new country, impossible travel" }
The API responds with HTTP 403 and the user sees a generic "suspicious activity" message. An audit log entry is written so you can review blocks in the admin dashboard.
Blocked users can recover by:
- Signing in from a known device/IP (e.g. their phone on home Wi-Fi)
- Going through the standard account recovery flow (email verification + password reset)
Rolling Out a Policy Change
If you're moving an existing app from
smartalways- Users on trusted devices will suddenly see MFA prompts on next sign-in
- Support volume around lost authenticators may spike for 1–2 weeks
- Push device adoption (if enabled) typically increases
A reasonable rollout plan:
- Announce the change to users 2 weeks ahead
- Encourage passkey or push device enrollment in the user portal
- Flip the policy
- Monitor MFA failure rate for the first 48 hours
Going the other direction (
alwayssmartAudit and Observability
Every policy decision is logged at the
info- — first-party portal hit
MFA required: ZewstID portal - — Smart policy challenged
MFA required by risk assessment - — Smart policy allowed through
MFA skipped: low risk - — Smart policy blocked (warn level)
Login blocked by risk assessment - — session generated, user is now in MFA flow
MFA session created - — user completed MFA
MFA verification successful - — step-up was requested
Step-up MFA session created
Logs include
userIdclientIdpolicyreasonriskScoreCommon Questions
"Why does the policy take 5 minutes to update?"
The policy is cached in Redis with a 300-second TTL to avoid hitting the developer portal database on every login. If you need an immediate change, evict the cache key
mfa_policy:{clientId}"Can I have different policies for different user groups?"
Not with
mfaPolicy"What if a user has no MFA methods enrolled?"
The engine returns
requireMFA: falsemfaEnabled === trueGET /api/v1/auth/mfa/status"Does never disable enrollment too?"
neverNo. Users can still enroll TOTP, push, and backup codes. The
never"How is push fatigue handled?"
The
/mfa/challenge/pushNext Steps
- MFA Comparison — TOTP vs Push vs Backup Codes — pick the right enrollment options for your users
- M2M Authentication — service accounts and machine-to-machine auth (no MFA)
- RBAC & Roles — gate sensitive routes by role and combine with step-up
- Push Authentication — implement Okta Verify-style push for the lowest-friction MFA experience
Was this page helpful?
Let us know how we can improve our documentation