Plugins

Two-factor

Official plugin — method-locked TOTP or email OTP after first-factor sign-in, with step-up enrollment and method change.

twoFactor() adds a second factor after password (or username) sign-in: RFC 6238 TOTP or email OTP. When 2FA is enabled, first-factor success withholds session tokens and returns a method-locked challenge.

The one rule

Enable gate.auth, then .plug(twoFactor()). Login challenges lock the configured method (totp or email_otp). Mid-challenge QR enrollment returns Forbidden. Method change needs step-up verify → provision → confirm.

Quick start

Plug it

src/app.ts
import { oke } from "okengine";
import { twoFactor } from "okengine/plugins";

export const app = oke({
  name: "shop",
  env: "dev",
  gate: { auth: {} },
}).plug(twoFactor());

First-time enable (session required)

Wire Bearer on createClient (auth.getToken / memorySession) — calls take input only.

const { data } = await api.auth.twoFactorEnable({});
// data.secret, data.otpauthUrl, data.recoveryCodes, data.method === "totp"

POST /auth/two-factor/enable — writes oke_two_factor. Store recovery codes once. Re-enrollment when 2FA is already enabled requires step-up first (twoFactorStepUp). An active login challenge → Forbidden / active_2fa_challenge.

Sign-in then verify

const signIn = await api.auth.signInUsername({ username, password });
if ("twoFactorRequired" in signIn.data && signIn.data.twoFactorRequired) {
  const { data } = await api.auth.twoFactorVerify({
    challengeId: signIn.data.challengeId,
    code: "123456",
  });
  // hybrid session tokens
}

POST /auth/two-factor/verify takes { challengeId, code } — not a bare userId. Missing challenge, method mismatch, or bad code → AuthFailed / invalid_credentials.

Method lock

When a login challenge is issued, the active method is recorded on the pending challenge. Until that challenge is consumed or expires:

  • Only codes for the locked method are accepted.
  • twoFactorEnable, twoFactorChangeMethod, twoFactorConfirmChange, and twoFactorDisable return Forbidden with active_2fa_challenge if called for that user (including with an older Bearer session).

This closes the July–August 2026 method-switching bypass pattern (switch to TOTP enrollment mid email-OTP challenge without proving the current factor).

Step-up and method change

  1. POST /auth/two-factor/step-up{ code, purpose } verifies the current method and grants a short-lived step-up (enroll | change | disable).
  2. POST /auth/two-factor/change-method{ method, email? } provisions the new method in a pending state (TOTP QR or email OTP). Requires step-up.
  3. POST /auth/two-factor/confirm-change{ code } activates the new method and immediately invalidates the old secret / recovery codes.

Disable also requires step-up when 2FA is already enabled.

Options

OptionTypeDefaultMeaning
secretstringactive*HMAC secret (*from gate.auth)
sessionsSessionStoreactive*Session store
now() => numberDate.nowInjectable clock
factorsTwoFactorStorenewPer-user method + TOTP / recovery
issuerstring"oke"Label in otpauth:// URLs
pendingPendingTwoFactorStoreactive*Login challenges
stepUpStepUpStoreactive*Privileged-op grants
verificationsVerificationStoreactive*Email OTP as second factor (2fa:{userId})
exposeDevOtpbooleanfalseReturn devOtp on email_otp challenge paths

Surfaces

FlowPathGate
auth.twoFactorEnablePOST /auth/two-factor/enablesession + bearer
auth.twoFactorVerifyPOST /auth/two-factor/verifygate.public + otp rate
auth.twoFactorStepUpPOST /auth/two-factor/step-upsession + bearer
auth.twoFactorChangeMethodPOST /auth/two-factor/change-methodsession + bearer
auth.twoFactorConfirmChangePOST /auth/two-factor/confirm-changesession + bearer
auth.twoFactorRequestEmailOtpPOST /auth/two-factor/request-email-otppublic + otp rate
auth.twoFactorDisablePOST /auth/two-factor/disablesession + bearer

Consequence: email/password and username sign-in return { twoFactorRequired, challengeId, method, userId } (no tokens) when the account has 2FA enabled. Complete login only via twoFactorVerify.

Troubleshooting

Learn more

  • Passkey — WebAuthn register / authenticate
  • Gate — session + policies
  • Username — first factor to enroll against
  • Primary OTP sign-in (not a second factor)? See OTP / Magic link

Next

On this page