Olympus Docs
CookbookMFA & step-up

Force MFA enrollment for specific roles

Require certain users to enroll MFA before they can complete login

Admin accounts and high-privilege users should be required to use MFA. Out of the box, Olympus makes MFA optional. This recipe makes it mandatory for users with a specific role.

Approach

After login, check: does the user's role trait require MFA? Are they enrolled?

If required but not enrolled → redirect to enrollment.

Configuration

In kratos.yml

selfservice:
  flows:
    login:
      after:
        password:
          hooks:
            - hook: web_hook
              config:
                url: https://your-backend/internal/check-mfa-required
                method: POST
                body: file:///etc/config/kratos/hooks/check-mfa.jsonnet
                response:
                  ignore: false
                  parse: true

hooks/check-mfa.jsonnet:

local identity = std.extVar('identity');
local flow = std.extVar('flow');
{
  identity_id: identity.id,
  role: identity.traits.role,
  has_totp: 'totp' in (identity.credentials | default { }),
  has_webauthn: 'webauthn' in (identity.credentials | default { }),
}

Your backend

// POST /internal/check-mfa-required
export async function POST(request: Request) {
  const { identity_id, role, has_totp, has_webauthn } = await request.json();

  const mfaRequiredRoles = ["admin", "operator", "support"];
  if (!mfaRequiredRoles.includes(role)) {
    return Response.json({ allowed: true });
  }

  if (has_totp || has_webauthn) {
    return Response.json({ allowed: true });
  }

  // Force redirect to settings → enroll MFA
  return Response.json({
    allowed: false,
    redirect_to: `https://iam.your-domain/settings?force_mfa=true`,
  });
}

The Kratos hook with response.parse: true lets your webhook redirect the flow.

Alternative: enforce in your app

If you don't want to maintain a Kratos webhook, do it in your app:

// app/layout.tsx or middleware
export async function middleware(req: Request) {
  const session = await getSession(req);
  if (!session) return; // not logged in; let normal flow handle

  const requiresMfa = ["admin", "operator"].includes(session.role);
  if (requiresMfa && !session.has_mfa) {
    return Response.redirect(`/settings?force_mfa=true`);
  }
}

Simpler but means each app must check. The webhook approach centralizes in Kratos.

Grace period

Hard-forcing on first login frustrates users who didn't expect it. Add a grace period:

  • Allow N days from role assignment to enroll.
  • Send daily reminder emails during the grace period.
  • After N days, block login until enrolled.

Track in a settings vault entry: mfa.grace_period_days, mfa.enforcement_date.

On this page