Migrating to Authentik Authentication¶
Step-by-step guide for migrating applications from Supabase Auth (or no auth) to the standardized Authentik authentication pattern.
Prerequisites¶
- Application already using Supabase for data storage
- Access to Authentik admin panel (
https://auth.internal/if/admin/) - Network access to
auth.internal(for integration testing)
Overview¶
The migration involves: 1. Setting up Authentik application 2. Installing auth dependencies 3. Adding auth file structure 4. Updating existing code 5. Testing all auth modes 6. Deploying with proper safeguards
Estimated effort: 2-4 hours depending on app complexity
Phase 1: Authentik Setup¶
1.1 Create OAuth2/OpenID Provider¶
- Navigate to Applications > Providers in Authentik admin
- Click Create and select OAuth2/OpenID Provider
- Configure:
Name: {app-name}-provider
Authorization flow: default-authorization-flow
Client type: Confidential
Client ID: (copy this - auto-generated)
Client Secret: (copy this - auto-generated)
Redirect URIs:
- http://localhost:3000/api/auth/callback/authentik
- https://{app}.internal/api/auth/callback/authentik
Signing Key: authentik Self-signed Certificate
Subject mode: Based on the User's hashed ID
Advanced:
Access token validity: minutes=5
Scopes: openid, profile, email, groups
1.2 Create Application¶
- Navigate to Applications > Applications
- Click Create
- Configure:
Name: {app-name}
Slug: {app-name}
Provider: {app-name}-provider
Launch URL: https://{app}.internal
UI Settings:
Icon: (optional - upload app icon)
1.3 (Optional) Configure Group Mappings¶
If your app uses RBAC with groups:
- Navigate to Customization > Property Mappings
- Verify
authentik default OAuth Mapping: OpenID 'groups'exists - Ensure the provider includes this mapping in its scope
1.4 Record Credentials¶
Save these values for .env.local:
AUTHENTIK_ISSUER=https://auth.internal/application/o/{app-slug}/
AUTHENTIK_CLIENT_ID={from step 1.1}
AUTHENTIK_CLIENT_SECRET={from step 1.1}
Phase 2: Install Dependencies¶
cd /root/projects/{app-name}
# Install auth dependencies
npm install next-auth@beta jsonwebtoken zod
# Type definitions
npm install -D @types/jsonwebtoken
Verify package.json:
Phase 3: Add Auth File Structure¶
3.1 Copy Template Files¶
Copy the auth files from the scaffold template:
# From scaffold template
cp -r /root/tower-fleet/scaffolds/nextjs/src/lib/auth /root/projects/{app-name}/src/lib/
cp -r /root/tower-fleet/scaffolds/nextjs/src/components/auth /root/projects/{app-name}/src/components/
cp -r /root/tower-fleet/scaffolds/nextjs/src/components/layout /root/projects/{app-name}/src/components/
cp /root/tower-fleet/scaffolds/nextjs/src/types/next-auth.d.ts /root/projects/{app-name}/src/types/
cp -r /root/tower-fleet/scaffolds/nextjs/src/app/api/auth /root/projects/{app-name}/src/app/api/
cp -r /root/tower-fleet/scaffolds/nextjs/src/app/login /root/projects/{app-name}/src/app/
cp /root/tower-fleet/scaffolds/nextjs/src/middleware.ts /root/projects/{app-name}/src/
# Replace template variables
SCHEMA_NAME="your_schema"
APP_NAME="your-app"
find /root/projects/{app-name}/src -type f \( -name "*.ts" -o -name "*.tsx" \) -exec sed -i \
-e "s/{{SCHEMA_NAME}}/$SCHEMA_NAME/g" \
-e "s/{{APP_NAME}}/$APP_NAME/g" {} \;
Note: For new apps, use /intents:scaffold-nextjs which includes auth automatically.
3.2 Required File Structure¶
Create these files (see Authentik Auth Pattern for full implementations):
src/lib/auth/
├── index.ts # Main exports
├── config.ts # AUTH_MODE validation
├── types.ts # User/Session Zod schemas
├── middleware.ts # Middleware factory
├── supabase-bridge.ts # JWT bridge for RLS
├── rbac.ts # Group/tier helpers
└── providers/
├── authentik.ts # NextAuth config
├── mock.ts # Mock provider
└── none.ts # Passthrough
3.3 Add Components¶
src/components/auth/
├── session-provider.tsx # SessionProvider wrapper
├── login-button.tsx # Login button component
├── user-menu.tsx # User dropdown
└── dev-banner.tsx # Auth mode warning
3.4 Add Types¶
Create src/types/next-auth.d.ts:
import 'next-auth'
declare module 'next-auth' {
interface Session {
accessToken?: string
idToken?: string
user: {
id: string
email: string
name?: string
image?: string
groups: string[]
}
}
interface User {
groups?: string[]
}
}
declare module 'next-auth/jwt' {
interface JWT {
accessToken?: string
idToken?: string
authentikId?: string
groups?: string[]
}
}
3.5 Add API Route¶
Create src/app/api/auth/[...nextauth]/route.ts:
import { getAuthMode } from '@/lib/auth/config'
export async function GET(request: Request) {
const mode = getAuthMode()
if (mode !== 'authentik') {
return new Response('Auth disabled', { status: 404 })
}
const { handlers } = await import('@/lib/auth/providers/authentik')
return handlers.GET(request)
}
export async function POST(request: Request) {
const mode = getAuthMode()
if (mode !== 'authentik') {
return new Response('Auth disabled', { status: 404 })
}
const { handlers } = await import('@/lib/auth/providers/authentik')
return handlers.POST(request)
}
3.6 Add Auth Mode API (for dev banner)¶
Create src/app/api/auth/mode/route.ts:
import { NextResponse } from 'next/server'
import { getAuthMode } from '@/lib/auth/config'
export async function GET() {
return NextResponse.json({ mode: getAuthMode() })
}
3.7 Add Login Page¶
Create src/app/login/page.tsx:
import { auth, signIn } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { getAuthMode } from '@/lib/auth/config'
export default async function LoginPage({
searchParams,
}: {
searchParams: { callbackUrl?: string }
}) {
const mode = getAuthMode()
// In non-authentik modes, redirect to home
if (mode !== 'authentik') {
redirect(searchParams.callbackUrl || '/')
}
// Already logged in? Redirect
const session = await auth()
if (session) {
redirect(searchParams.callbackUrl || '/')
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="p-8 rounded-lg border bg-card">
<h1 className="text-2xl font-bold mb-6">Sign In</h1>
<form
action={async () => {
'use server'
await signIn('authentik', {
redirectTo: searchParams.callbackUrl || '/',
})
}}
>
<button
type="submit"
className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Sign in with Authentik
</button>
</form>
</div>
</div>
)
}
Phase 4: Update Existing Code¶
4.1 Update Root Middleware¶
Replace or update src/middleware.ts:
import { createAuthMiddleware } from '@/lib/auth/middleware'
export default createAuthMiddleware({
publicRoutes: ['/login', '/api/auth', '/api/health'],
loginPath: '/login',
})
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
4.2 Update Root Layout¶
Update src/app/layout.tsx:
import { SessionProvider } from '@/components/auth/session-provider'
import { DevAuthBanner } from '@/components/auth/dev-banner'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SessionProvider>
<DevAuthBanner />
{/* rest of your layout */}
{children}
</SessionProvider>
</body>
</html>
)
}
4.3 Update next.config.ts¶
Add build-time auth validation:
import type { NextConfig } from 'next'
// Validate auth mode for production builds
if (process.env.NODE_ENV === 'production') {
if (process.env.AUTH_MODE !== 'authentik') {
throw new Error(
'Production builds require AUTH_MODE=authentik. ' +
`Current value: ${process.env.AUTH_MODE || 'undefined'}`
)
}
}
const nextConfig: NextConfig = {
// ... your config
}
export default nextConfig
4.4 Update API Routes¶
Replace Supabase auth calls with new pattern:
Before (Supabase Auth):
import { createClient } from '@/lib/supabase/server'
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data } = await supabase.from('items').select('*')
return NextResponse.json(data)
}
After (Authentik + RLS Bridge):
import { getAuthContextWithRLS } from '@/lib/auth/supabase-bridge'
export async function GET() {
const { user, supabase } = await getAuthContextWithRLS()
if (!user || !supabase) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data } = await supabase.from('items').select('*')
return NextResponse.json(data)
}
4.5 Update Client Components¶
Before (Supabase Auth):
import { createClient } from '@/lib/supabase/client'
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
After (NextAuth):
import { useSession } from 'next-auth/react'
const { data: session } = useSession()
const user = session?.user
4.6 Update Server Components¶
Before:
import { createClient } from '@/lib/supabase/server'
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
After:
Phase 5: Environment Configuration¶
5.1 Update .env.local¶
# Auth Mode: 'none' | 'mock' | 'authentik'
AUTH_MODE=mock
# NextAuth (required for all modes)
AUTH_SECRET=generate-with-openssl-rand-base64-32
AUTH_URL=http://localhost:3000
AUTH_TRUST_HOST=true
# Authentik (only needed when AUTH_MODE=authentik)
AUTHENTIK_ISSUER=https://auth.internal/application/o/{app-name}/
AUTHENTIK_CLIENT_ID=your-client-id
AUTHENTIK_CLIENT_SECRET=your-client-secret
# Mock user (only used when AUTH_MODE=mock)
MOCK_USER_ID=dev-user-123
MOCK_USER_EMAIL=dev@local.test
MOCK_USER_NAME=Dev User
MOCK_USER_GROUPS=tier-owner
# Supabase (unchanged)
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
SUPABASE_JWT_SECRET=...
5.2 Generate AUTH_SECRET¶
Phase 6: Database Migration (RLS)¶
If using Supabase RLS, update policies to use JWT claims from the auth bridge.
6.1 Create RLS Helper Functions¶
-- Create helper functions for extracting auth claims
CREATE OR REPLACE FUNCTION {schema}.auth_user_id()
RETURNS TEXT AS $$
SELECT COALESCE(
current_setting('request.jwt.claims', true)::json->>'sub',
(current_setting('request.jwt.claims', true)::json->'user_metadata'->>'sub')::text
)
$$ LANGUAGE sql STABLE;
CREATE OR REPLACE FUNCTION {schema}.user_groups()
RETURNS TEXT[] AS $$
SELECT COALESCE(
ARRAY(SELECT jsonb_array_elements_text(
current_setting('request.jwt.claims', true)::jsonb->'groups'
)),
ARRAY[]::TEXT[]
)
$$ LANGUAGE sql STABLE;
CREATE OR REPLACE FUNCTION {schema}.is_admin()
RETURNS BOOLEAN AS $$
SELECT 'tier-owner' = ANY({schema}.user_groups())
$$ LANGUAGE sql STABLE;
6.2 Update RLS Policies¶
-- Example: Update existing RLS policy
DROP POLICY IF EXISTS "Users can view own items" ON {schema}.items;
CREATE POLICY "Users can view own items" ON {schema}.items
FOR SELECT
USING (
{schema}.is_admin()
OR user_id = {schema}.auth_user_id()
);
Phase 7: Testing¶
7.1 Test AUTH_MODE=none¶
Verify: - [ ] All routes accessible without login - [ ] No auth errors in console - [ ] Can navigate entire app
7.2 Test AUTH_MODE=mock¶
Verify: - [ ] Dev banner shows "Authentication mocked" - [ ] Protected routes accessible - [ ] User info shows mock user - [ ] API routes return data - [ ] RBAC works with mock groups
7.3 Test AUTH_MODE=mock with Different Roles¶
Verify: - [ ] Reduced permissions applied - [ ] Admin features hidden/inaccessible
7.4 Test AUTH_MODE=authentik¶
Verify: - [ ] Redirect to Authentik login - [ ] OAuth flow completes - [ ] Session established - [ ] User info correct - [ ] Groups populated - [ ] Logout works - [ ] RLS filters data correctly
7.5 Test Production Build¶
Verify:
- [ ] Build succeeds with AUTH_MODE=authentik
- [ ] Build fails with AUTH_MODE=mock (safeguard works)
Phase 8: Deployment¶
8.1 Update Kubernetes Secrets¶
Add to deployment secrets:
AUTH_MODE: authentik
AUTH_SECRET: <base64-encoded-secret>
AUTH_URL: https://{app}.internal
AUTHENTIK_ISSUER: https://auth.internal/application/o/{app-name}/
AUTHENTIK_CLIENT_ID: <your-client-id>
AUTHENTIK_CLIENT_SECRET: <your-client-secret>
8.2 Update CI/CD¶
Add auth validation to GitHub Actions:
- name: Validate auth configuration
run: |
if [[ "$AUTH_MODE" != "authentik" ]]; then
echo "ERROR: Deployment requires AUTH_MODE=authentik"
exit 1
fi
env:
AUTH_MODE: ${{ secrets.AUTH_MODE }}
8.3 Deploy¶
Migration Checklist¶
Authentik Setup¶
- [ ] Created OAuth2/OpenID Provider
- [ ] Created Application
- [ ] Configured redirect URIs (localhost + production)
- [ ] Recorded client ID and secret
Dependencies¶
- [ ] Installed
next-auth@beta - [ ] Installed
jsonwebtoken - [ ] Installed
zod
File Structure¶
- [ ] Created
src/lib/auth/directory - [ ] Added
index.ts,config.ts,types.ts - [ ] Added
middleware.ts,supabase-bridge.ts,rbac.ts - [ ] Added
providers/authentik.ts,mock.ts,none.ts - [ ] Created
src/components/auth/components - [ ] Added
src/types/next-auth.d.ts - [ ] Created API routes
Code Updates¶
- [ ] Updated root middleware
- [ ] Updated root layout with SessionProvider
- [ ] Added DevAuthBanner
- [ ] Updated next.config.ts with production check
- [ ] Migrated API routes to new auth pattern
- [ ] Migrated client components to useSession
- [ ] Migrated server components to auth()
Environment¶
- [ ] Updated
.env.localwith all auth vars - [ ] Generated AUTH_SECRET
Database¶
- [ ] Created RLS helper functions (if applicable)
- [ ] Updated RLS policies (if applicable)
Testing¶
- [ ] Tested AUTH_MODE=none
- [ ] Tested AUTH_MODE=mock
- [ ] Tested AUTH_MODE=mock with different roles
- [ ] Tested AUTH_MODE=authentik
- [ ] Verified production build safeguard
Deployment¶
- [ ] Updated Kubernetes secrets
- [ ] Updated CI/CD pipeline
- [ ] Deployed successfully
Troubleshooting¶
"Invalid issuer" Error¶
- Verify
AUTHENTIK_ISSUERends with/ - Check Authentik provider is properly configured
- Ensure provider slug matches issuer URL
OAuth Callback Fails¶
- Verify redirect URI in Authentik matches exactly
- Check AUTH_URL matches actual URL
- Ensure
AUTH_TRUST_HOST=truefor proxied setups
RLS Not Working¶
- Verify
SUPABASE_JWT_SECRETmatches Supabase config - Check JWT claims include expected fields
- Test with
SELECT current_setting('request.jwt.claims', true)
Mock Mode Not Working¶
- Verify AUTH_MODE is set correctly
- Check SessionProvider wraps the app
- Verify mock user defaults in types.ts
Rollback Plan¶
If migration fails:
- Revert to previous auth implementation
- Remove new auth dependencies
- Restore original middleware
- Restore original API routes
- Keep Authentik application (can retry later)
Related Documentation¶
- Authentik Auth Pattern - Full pattern reference
- Supabase Integration - Database setup
- Production Deployment - Deployment guide