Skip to content

πŸ” Kinde Auth Setup

Scope: MVP - Authentication Provider Status: Primary auth solution SDK: Kinde SvelteKit SDK

Overview

Kinde provides enterprise-grade authentication without the complexity, perfect for GetCimple's need to support board directors and multi-tenant architecture.

Why Kinde for MVP

  • Fast Setup: Authentication in hours, not weeks
  • Multi-Tenant Ready: Built-in organization support
  • Enterprise Features: SSO, MFA, audit logs included
  • Australian Company: Data sovereignty friendly
  • Generous Free Tier: 7,500 MAU free

Initial Setup

1. Kinde Application Configuration

// Environment variables
PUBLIC_KINDE_DOMAIN = 'https://getcimple.kinde.com'
PUBLIC_KINDE_CLIENT_ID = 'your_client_id'
KINDE_CLIENT_SECRET = 'your_client_secret'
PUBLIC_KINDE_REDIRECT_URI = 'https://app.getcimple.com/api/auth/callback'
PUBLIC_KINDE_POST_LOGOUT_URI = 'https://app.getcimple.com'

2. Organization (Tenant) Setup

// Organization = Tenant in GetCimple
interface KindeOrganization {
  org_code: string // 'acme_corp'
  org_name: string // 'ACME Corporation'
  org_logo?: string // Custom branding
  org_settings: {
    enforce_mfa: boolean // For Essential Eight compliance
    session_duration: number
    allowed_domains: string[] // Email domain restrictions
  }
}

3. User Roles & Permissions

Roles:
  board_director:
    permissions:
      - view:reports
      - view:compliance
      - approve:critical

  executive:
    permissions:
      - view:all
      - manage:tasks
      - generate:reports
      - approve:standard

  it_manager:
    permissions:
      - view:all
      - manage:all
      - upload:evidence
      - update:compliance

  admin:
    permissions:
      - manage:users
      - manage:settings
      - view:audit_logs

SvelteKit Integration

1. Install Kinde SDK

npm install @kinde-oss/kinde-auth-sveltekit

2. Auth Configuration

// src/lib/auth/kinde.ts
import { KindeAuthClient } from '@kinde-oss/kinde-auth-sveltekit'
import type { HandleClientAuth } from '@kinde-oss/kinde-auth-sveltekit'

export const kindeAuthClient: HandleClientAuth = new KindeAuthClient({
  domain: import.meta.env.PUBLIC_KINDE_DOMAIN,
  clientId: import.meta.env.PUBLIC_KINDE_CLIENT_ID,
  clientSecret: import.meta.env.KINDE_CLIENT_SECRET,
  redirectUri: import.meta.env.PUBLIC_KINDE_REDIRECT_URI,
  logoutUri: import.meta.env.PUBLIC_KINDE_POST_LOGOUT_URI,
  scope: 'openid profile email offline',
  // Multi-tenant configuration
  audience: 'https://api.getcimple.com',
  orgCode: 'dynamic', // Set per login
})

3. Hooks Configuration

// src/hooks.server.ts
import { kindeAuthClient } from '$lib/auth/kinde'
import type { Handle } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  // Handle Kinde auth
  const authResult = await kindeAuthClient.handleAuth(event)
  if (authResult) return authResult

  // Get session
  const session = await kindeAuthClient.getSession(event)

  // Add to locals
  event.locals.user = session?.user || null
  event.locals.organization = session?.organization || null
  event.locals.permissions = session?.permissions || []

  // Add tenant context for Supabase
  if (session?.organization) {
    event.locals.tenantId = session.organization.org_code
  }

  return resolve(event)
}

4. Protected Routes

// src/routes/app/+layout.server.ts
import { redirect } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'

export const load: LayoutServerLoad = async ({ locals, url }) => {
  // Require authentication
  if (!locals.user) {
    throw redirect(302, `/api/auth/login?redirect=${url.pathname}`)
  }

  // Require organization
  if (!locals.organization) {
    throw redirect(302, '/organization-required')
  }

  return {
    user: locals.user,
    organization: locals.organization,
    permissions: locals.permissions,
  }
}

Multi-Tenant Implementation

1. Organization-Based Login

// Force organization selection
export async function GET({ url }) {
  const orgCode = url.searchParams.get('org')

  if (orgCode) {
    // Login to specific organization
    return kindeAuthClient.login({
      org_code: orgCode,
      redirect_to: url.searchParams.get('redirect') || '/app',
    })
  } else {
    // Show organization picker
    return new Response(null, {
      status: 302,
      headers: {
        Location: '/select-organization',
      },
    })
  }
}

2. Organization Switcher

<!-- src/lib/components/OrgSwitcher.svelte -->
<script lang="ts">
  import { page } from '$app/stores';

  export let currentOrg: Organization;
  export let userOrgs: Organization[];

  async function switchOrg(orgCode: string) {
    // Logout and re-login to new org
    window.location.href =
      `/api/auth/switch-org?org=${orgCode}&redirect=${$page.url.pathname}`;
  }
</script>

<select on:change={(e) => switchOrg(e.target.value)}>
  {#each userOrgs as org}
    <option value={org.org_code}
            selected={org.org_code === currentOrg.org_code}>
      {org.org_name}
    </option>
  {/each}
</select>

3. Tenant Context Provider

// src/lib/auth/tenant-context.ts
import { getContext, setContext } from 'svelte'
import type { Organization } from '@kinde-oss/kinde-auth-sveltekit'

const TENANT_KEY = Symbol('tenant')

export interface TenantContext {
  organization: Organization
  tenantId: string
  features: string[]
  limits: {
    users: number
    storage: number
  }
}

export function setTenantContext(context: TenantContext) {
  setContext(TENANT_KEY, context)
}

export function getTenantContext(): TenantContext {
  return getContext(TENANT_KEY)
}

Supabase Integration

1. JWT Customization

// Kinde webhook to add claims
export async function POST({ request }) {
  const event = await request.json()

  if (event.type === 'token.generated') {
    // Add tenant_id to JWT
    const customClaims = {
      tenant_id: event.organization.org_code,
      role: event.user.roles[0]?.key || 'viewer',
    }

    return json({ custom_claims: customClaims })
  }
}

2. Supabase Client Setup

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import type { Session } from '@kinde-oss/kinde-auth-sveltekit'

export function createSupabaseClient(session: Session) {
  return createClient(
    import.meta.env.PUBLIC_SUPABASE_URL,
    import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
    {
      global: {
        headers: {
          Authorization: `Bearer ${session.access_token}`,
          'X-Tenant-ID': session.organization.org_code,
        },
      },
    }
  )
}

User Management

1. Invite Flow

// Invite user to organization
export async function inviteUser(email: string, role: string) {
  const client = await getKindeManagementClient()

  return client.inviteUser({
    email,
    organization_code: getCurrentOrg(),
    roles: [role],
    first_name: email.split('@')[0],
    // Custom properties
    properties: {
      invited_by: getCurrentUser().id,
      invited_at: new Date().toISOString(),
    },
  })
}

2. User Provisioning

// Webhook: user.created
export async function handleUserCreated(event: KindeWebhookEvent) {
  const { user, organization } = event

  // Create user in our database
  await supabase.from('users').insert({
    id: user.id,
    email: user.email,
    name: `${user.first_name} ${user.last_name}`,
    role: user.roles[0]?.key || 'viewer',
    tenant_id: organization.org_code,
    kinde_data: user,
  })

  // Send welcome email
  await sendWelcomeEmail(user, organization)
}

Security Configuration

1. MFA Enforcement

// Force MFA for directors
export async function enforceMFA({ locals }) {
  if (locals.user?.roles?.includes('board_director')) {
    const mfaStatus = await kindeAuthClient.getMFAStatus()

    if (!mfaStatus.enabled) {
      throw redirect(302, '/security/enable-mfa')
    }
  }
}

2. Session Management

// Custom session duration by role
const SESSION_DURATION = {
  board_director: 30 * 60, // 30 minutes
  executive: 8 * 60 * 60, // 8 hours
  it_manager: 24 * 60 * 60, // 24 hours
  admin: 12 * 60 * 60, // 12 hours
}

// Refresh token rotation
export async function refreshSession(event) {
  const session = await kindeAuthClient.refreshToken(event)

  if (session) {
    // Update activity timestamp
    await updateUserActivity(session.user.id)
  }

  return session
}

3. Audit Logging

// Log all auth events
export async function logAuthEvent(event: AuthEvent) {
  await supabase.from('auth_audit_log').insert({
    user_id: event.user?.id,
    event_type: event.type,
    ip_address: event.ip,
    user_agent: event.userAgent,
    organization_id: event.organization?.org_code,
    metadata: event.metadata,
    timestamp: new Date().toISOString(),
  })
}

Production Configuration

1. Environment Setup

# Production environment
PUBLIC_KINDE_DOMAIN="https://getcimple.kinde.com"
PUBLIC_KINDE_REDIRECT_URI="https://app.getcimple.com/api/auth/callback"
KINDE_WEBHOOK_SECRET="webhook_secret_key"
KINDE_MANAGEMENT_CLIENT_ID="management_client_id"
KINDE_MANAGEMENT_CLIENT_SECRET="management_client_secret"

2. CORS Configuration

// Allowed origins
const ALLOWED_ORIGINS = [
  'https://app.getcimple.com',
  'https://getcimple.com',
  'https://getcimple.kinde.com',
]

3. Rate Limiting

// Auth endpoint rate limiting
const authRateLimiter = new RateLimiter({
  login: '5 per minute per IP',
  register: '3 per hour per IP',
  password_reset: '3 per hour per email',
})

Monitoring & Analytics

Key Metrics

-- Auth metrics
SELECT
  DATE(timestamp) as date,
  COUNT(DISTINCT user_id) as unique_logins,
  COUNT(*) as total_logins,
  COUNT(CASE WHEN event_type = 'login_failed' THEN 1 END) as failed_logins,
  AVG(CASE WHEN event_type = 'login_success'
    THEN EXTRACT(EPOCH FROM (completed_at - started_at))
  END) as avg_login_time
FROM auth_audit_log
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY DATE(timestamp);

Implementation Checklist

  • Create Kinde account and application
  • Configure organizations (tenants)
  • Set up roles and permissions
  • Install SvelteKit SDK
  • Configure authentication hooks
  • Implement protected routes
  • Add organization switcher
  • Integrate with Supabase
  • Set up user provisioning
  • Configure MFA requirements
  • Implement audit logging
  • Test multi-tenant isolation
  • Configure production environment
  • Set up monitoring

Related Documents: