Two-Factor Authentication Plugin

TOTP-based two-factor authentication plugin for Svelar/SvelteKit with QR code generation, recovery codes, and pre-built UI components for setup, verification, and recovery. Zero external dependencies for QR code and TOTP generation.

Package: @beeblock/svelar-two-factor

Install:

npx svelar plugin:install @beeblock/svelar-two-factor

Imports:

// Plugin registration
import { SvelarTwoFactorPlugin } from '@beeblock/svelar-two-factor/server';

// Core API
import { TwoFactor, TwoFactorService, TOTP, QRCode, RecoveryCodes, HasTwoFactor } from '@beeblock/svelar-two-factor';

// Server-side (controller, middleware)
import { TwoFactorController, TwoFactorMiddleware } from '@beeblock/svelar-two-factor/server';

// UI components
import { TwoFactorSetup, TwoFactorChallenge, RecoveryCodes as RecoveryCodesUI } from '@beeblock/svelar-two-factor/ui';

// Types
import type { TwoFactorConfig, TwoFactorPluginConfig } from '@beeblock/svelar-two-factor';

Quick Start

1. Register the Plugin

// src/lib/plugins.ts
import { SvelarTwoFactorPlugin } from '@beeblock/svelar-two-factor/server';

export const twoFactorPlugin = new SvelarTwoFactorPlugin({
  issuer: 'MyApp',
  digits: 6,
  period: 30,
  algorithm: 'SHA1',
  recoveryCodesCount: 8,
  window: 1,
});

2. Add to Your User Model

import { Model } from '@beeblock/svelar/database';
import { HasTwoFactor } from '@beeblock/svelar-two-factor';

class User extends HasTwoFactor(Model) {
  static table = 'users';

  hasTwoFactorEnabled(): boolean {
    return Boolean(this.getAttribute('two_factor_secret') && this.getAttribute('two_factor_confirmed_at'));
  }
}

3. Set Up API Routes

// src/routes/api/two-factor/+server.ts
import { TwoFactorController } from '@beeblock/svelar-two-factor/server';

export const POST = async (event) => TwoFactorController.enable(event);
export const DELETE = async (event) => TwoFactorController.disable(event);
// src/routes/api/two-factor/confirm/+server.ts
import { TwoFactorController } from '@beeblock/svelar-two-factor/server';

export const POST = async (event) => TwoFactorController.confirm(event);
// src/routes/api/two-factor/verify/+server.ts
import { TwoFactorController } from '@beeblock/svelar-two-factor/server';

export const POST = async (event) => TwoFactorController.verify(event);

Configuration

The SvelarTwoFactorPlugin constructor accepts:

Option Type Default Description
issuer string 'Svelar' Issuer name shown in authenticator apps
digits number 6 Number of digits in the TOTP code
period number 30 TOTP code validity period in seconds
algorithm string 'SHA1' HMAC algorithm (SHA1, SHA256, SHA512)
recoveryCodesCount number 8 Number of recovery codes to generate
window number 1 Time step window for TOTP validation (allows +/- N steps)
prefix string '/api' API route prefix

Core API

TwoFactor Facade

import { TwoFactor } from '@beeblock/svelar-two-factor';

// Configure (done automatically by plugin)
TwoFactor.configure({
  issuer: 'MyApp',
  digits: 6,
  period: 30,
});

// Enable 2FA for a user — returns secret and QR code
const setup = await TwoFactor.enable(user);
// setup.secret   — base32-encoded secret
// setup.qrCode   — SVG string of the QR code
// setup.otpauthUrl — otpauth:// URI
// setup.recoveryCodes — string[] of recovery codes

// Confirm 2FA setup with a TOTP code from the user
await TwoFactor.confirm(user, '123456');

// Verify a TOTP code during login
const valid = await TwoFactor.verify(user, '123456');

// Verify using a recovery code
const valid = await TwoFactor.verifyRecoveryCode(user, 'ABCD-EFGH');

// Disable 2FA for a user
await TwoFactor.disable(user);

// Regenerate recovery codes
const newCodes = await TwoFactor.regenerateRecoveryCodes(user);

TwoFactorService

Lower-level service with direct access:

import { TwoFactorService } from '@beeblock/svelar-two-factor';

const service = new TwoFactorService(config);

const secret = service.generateSecret();
const otpauthUrl = service.buildOtpauthUrl(secret, 'user@example.com');
const qrSvg = service.generateQrCode(otpauthUrl);
const isValid = service.verifyCode(secret, '123456');

TOTP

Low-level TOTP implementation (RFC 6238):

import { TOTP } from '@beeblock/svelar-two-factor';

// Generate a TOTP code for the current time
const code = TOTP.generate(secret, { digits: 6, period: 30, algorithm: 'SHA1' });

// Verify a TOTP code with time window
const valid = TOTP.verify(secret, '123456', {
  digits: 6,
  period: 30,
  algorithm: 'SHA1',
  window: 1,
});

QRCode

Built-in QR code SVG generator (no external dependencies):

import { QRCode } from '@beeblock/svelar-two-factor';

const svg = QRCode.toSvg('otpauth://totp/MyApp:user@example.com?secret=BASE32SECRET&issuer=MyApp', {
  width: 200,
  margin: 4,
});

RecoveryCodes

Generate and manage recovery codes:

import { RecoveryCodes } from '@beeblock/svelar-two-factor';

// Generate N recovery codes
const codes: string[] = RecoveryCodes.generate(8);
// => ['ABCD-EFGH', 'IJKL-MNOP', ...]

// Hash codes for storage
const hashed: string[] = await RecoveryCodes.hash(codes);

// Verify a recovery code against hashed codes
const { valid, remaining } = await RecoveryCodes.verify('ABCD-EFGH', hashedCodes);

HasTwoFactor Mixin

Adds 2FA fields and helpers to a Model:

class User extends HasTwoFactor(Model) {
  static table = 'users';
}

const user = await User.find(1);
user.two_factor_secret;        // string | null
user.two_factor_recovery_codes; // string | null (JSON-encoded)
user.two_factor_confirmed_at;  // string | null
user.hasTwoFactorEnabled();    // boolean

Server-Side

TwoFactorController

Handles all 2FA API endpoints:

Method Route Description
TwoFactorController.enable(event) POST /api/two-factor Start 2FA setup, returns secret + QR code
TwoFactorController.confirm(event) POST /api/two-factor/confirm Confirm setup with a TOTP code
TwoFactorController.verify(event) POST /api/two-factor/verify Verify a TOTP code during login
TwoFactorController.disable(event) DELETE /api/two-factor Disable 2FA
TwoFactorController.recoveryCodes(event) GET /api/two-factor/recovery-codes Get recovery codes
TwoFactorController.regenerateRecoveryCodes(event) POST /api/two-factor/recovery-codes Regenerate recovery codes

TwoFactorMiddleware

Middleware that enforces 2FA verification:

// hooks.server.ts
import { TwoFactorMiddleware } from '@beeblock/svelar-two-factor/server';

// Add to your middleware chain
// This middleware checks if the user has 2FA enabled but hasn't verified
// in the current session, and redirects to the challenge page.
const twoFactorMiddleware = new TwoFactorMiddleware({
  challengeUrl: '/two-factor/challenge',
  excludePaths: ['/two-factor', '/logout', '/api'],
});

UI Components

TwoFactorSetup

Full setup flow with QR code display and code confirmation:

<script lang="ts">
  import { TwoFactorSetup } from '@beeblock/svelar-two-factor/ui';
</script>

<TwoFactorSetup
  enableUrl="/api/two-factor"
  confirmUrl="/api/two-factor/confirm"
  onComplete={() => console.log('2FA enabled!')}
/>

TwoFactorChallenge

Verification form for login challenge:

<script lang="ts">
  import { TwoFactorChallenge } from '@beeblock/svelar-two-factor/ui';
</script>

<TwoFactorChallenge
  verifyUrl="/api/two-factor/verify"
  onSuccess={() => window.location.href = '/dashboard'}
  onError={(msg) => console.error(msg)}
  showRecoveryOption={true}
/>

RecoveryCodes

Display and copy recovery codes:

<script lang="ts">
  import { RecoveryCodes } from '@beeblock/svelar-two-factor/ui';
</script>

<RecoveryCodes
  codes={['ABCD-EFGH', 'IJKL-MNOP', 'QRST-UVWX']}
  regenerateUrl="/api/two-factor/recovery-codes"
/>

Migration SQL

Add 2FA columns to your users table:

ALTER TABLE users ADD COLUMN two_factor_secret TEXT;
ALTER TABLE users ADD COLUMN two_factor_recovery_codes TEXT;
ALTER TABLE users ADD COLUMN two_factor_confirmed_at TEXT;

Full Working Example

// src/lib/plugins.ts
import { SvelarTwoFactorPlugin } from '@beeblock/svelar-two-factor/server';

export const twoFactorPlugin = new SvelarTwoFactorPlugin({
  issuer: 'MyApp',
  digits: 6,
  period: 30,
  recoveryCodesCount: 8,
});
// src/routes/api/two-factor/+server.ts
import { TwoFactorController } from '@beeblock/svelar-two-factor/server';

export const POST = async (event) => TwoFactorController.enable(event);
export const DELETE = async (event) => TwoFactorController.disable(event);
// src/routes/api/two-factor/confirm/+server.ts
import { TwoFactorController } from '@beeblock/svelar-two-factor/server';

export const POST = async (event) => TwoFactorController.confirm(event);
<!-- src/routes/settings/security/+page.svelte -->
<script lang="ts">
  import { TwoFactorSetup } from '@beeblock/svelar-two-factor/ui';
  import { RecoveryCodes } from '@beeblock/svelar-two-factor/ui';

  interface Props {
    data: { user: any };
  }
  let { data }: Props = $props();
  let enabled = $state(data.user.two_factor_confirmed_at !== null);
  let recoveryCodes = $state<string[]>([]);

  function handleComplete() {
    enabled = true;
  }
</script>

<h2>Two-Factor Authentication</h2>

{#if enabled}
  <p>Two-factor authentication is enabled.</p>
  <RecoveryCodes
    codes={recoveryCodes}
    regenerateUrl="/api/two-factor/recovery-codes"
  />
{:else}
  <TwoFactorSetup
    enableUrl="/api/two-factor"
    confirmUrl="/api/two-factor/confirm"
    onComplete={handleComplete}
  />
{/if}
<!-- src/routes/two-factor/challenge/+page.svelte -->
<script lang="ts">
  import { TwoFactorChallenge } from '@beeblock/svelar-two-factor/ui';
</script>

<h1>Two-Factor Verification</h1>
<p>Enter the code from your authenticator app.</p>

<TwoFactorChallenge
  verifyUrl="/api/two-factor/verify"
  onSuccess={() => window.location.href = '/dashboard'}
  showRecoveryOption={true}
/>
Svelar © 2026 · MIT License