π 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¶
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: