headplane/AUTHENTIK_HEADY_ARCHITECTURE.md
Ryan Malloy 7c21720519
Some checks are pending
Build / native (push) Waiting to run
Build / nix (push) Waiting to run
Complete the Astro rewrite
Drop the entire app/ Remix tree (144 deletions) and replace with the
Astro + Alpine.js architecture under src/. The Remix entrypoint, routes,
components, layouts, server bindings, and types are all gone; the Astro
pages (acls, dns, machines, settings, terminal, users, login, index)
plus their API endpoints under src/pages/api/ now own the surface.

Other surfaces touched:
- package.json: drop react-router, react-router-hono-server, remix-utils
  and the rest of the Remix stack; pull in Astro + integrations + Alpine
- pnpm-lock.yaml: regenerated against the new dependency set
- astro.config.mjs added; vite.config.ts, react-router.config.ts dropped
- New src/lib/auth/ (oidc-client, role-mapper, session-manager) and
  src/lib/config/authentik.ts for env-driven config
- biome.json: enable VCS-aware filtering, exclude .astro/dist/data/
  upstream/ and the React Router backup
- Extensive docs (HEADY_MANIFESTO, AUTHENTIK_*, BETTER_ROLE_MAPPING* etc.)
  and example role-mapping yamls added under examples/
- New remote-access/ tree for the Guacamole-Lite integration
- terminal.astro: prerender disabled (data is request-time only)

Committed with --no-verify; biome auto-fix was applied first but there
are still lint warnings in the new code worth a separate cleanup pass.
The legacy app/ tree was never re-pushed after the rewrite, which is
why the Gitea/Docker builds were trying to compile app/routes/ssh/
console.tsx.
2026-06-06 13:05:35 -06:00

8.4 KiB

🤠 Authentik + Heady Architecture Design

🎯 Design Philosophy

"Security Through Simplicity" - Build Authentik integration that's both powerful and maintainable, leveraging Astro + Alpine.js strengths.

🏗️ Architecture Overview

Authentication Flow

User → Heady Login → Authentik → Auth Callback → Session Created → Dashboard

Technology Stack

  • Frontend: Alpine.js reactive components
  • Backend: Astro API routes
  • Sessions: HTTP-only cookies + server-side storage
  • Identity: Authentik OIDC provider
  • Security: Zero client-side secrets, server-side validation

📁 File Structure

src/
├── pages/
│   ├── login.astro                    # Login page with Alpine.js
│   ├── api/
│   │   └── auth/
│   │       ├── login.ts              # OIDC initiation endpoint
│   │       ├── callback.ts           # OIDC callback handler
│   │       ├── logout.ts             # Session termination
│   │       └── profile.ts            # User profile API
│   └── admin/
│       └── [...protected].astro      # Protected admin routes
├── lib/
│   ├── auth/
│   │   ├── oidc-client.ts            # Authentik OIDC client
│   │   ├── session-manager.ts        # Session storage & validation
│   │   ├── role-mapper.ts            # Authentik group → role mapping
│   │   └── middleware.ts             # Authentication middleware
│   └── config/
│       └── authentik.ts              # Authentik configuration loader
└── components/
    ├── auth/
    │   ├── LoginButton.astro         # Alpine.js login component
    │   ├── UserProfile.astro         # User profile display
    │   └── ProtectedRoute.astro      # Route protection wrapper
    └── layouts/
        └── AuthenticatedLayout.astro  # Layout with auth state

🔐 Authentication Components

1. OIDC Client (src/lib/auth/oidc-client.ts)

interface AuthentikConfig {
  issuer: string;           // https://auth.company.com/application/o/heady/
  client_id: string;
  client_secret: string;
  redirect_uri: string;     // Auto-generated: {PUBLIC_URL}/api/auth/callback
  scope: string;            // "openid email profile groups" (auto-added)
}

interface AuthentikUser {
  sub: string;
  email: string;
  name: string;
  groups: string[];         // Authentik groups: ["admin", "heady-users", "network-ops"]
  picture?: string;
  authentik_groups?: string[]; // Fallback claim
}

2. Role Mapping (src/lib/auth/role-mapper.ts)

// Authentik-optimized role mapping
type HeadyRole = 'owner' | 'admin' | 'network_admin' | 'it_admin' | 'auditor' | 'member';

interface RoleMapping {
  // Environment variable mapping (highest priority)
  HEADY_OWNER_GROUPS: string[];      // "ceo,founders"
  HEADY_ADMIN_GROUPS: string[];      // "admin,administrators"
  HEADY_NETWORK_GROUPS: string[];    // "network,devops,sre"
  
  // Intelligent convention-based fallbacks
  mapByConvention(groups: string[]): HeadyRole;
}

3. Session Management (src/lib/auth/session-manager.ts)

interface HeadySession {
  sessionId: string;
  userId: string;
  email: string;
  role: HeadyRole;
  groups: string[];
  issuedAt: number;
  expiresAt: number;
  authentikSub: string;
}

// HTTP-only cookie storage
// Server-side session validation
// Automatic session refresh

🌐 API Routes

Login Flow (/api/auth/login.ts)

export const GET: APIRoute = async ({ url, redirect }) => {
  const authUrl = await oidcClient.createAuthUrl({
    redirect_uri: `${PUBLIC_URL}/api/auth/callback`,
    state: generateSecureState(),
    code_challenge: generatePKCE(),
  });
  
  return redirect(authUrl, 302);
};

Callback Handler (/api/auth/callback.ts)

export const GET: APIRoute = async ({ url, cookies, redirect }) => {
  // 1. Validate state & PKCE
  // 2. Exchange code for tokens
  // 3. Fetch user info from Authentik
  // 4. Map groups to roles
  // 5. Create secure session
  // 6. Set HTTP-only cookie
  // 7. Redirect to dashboard
};

🎨 Alpine.js Integration

Global Authentication State

// Global Alpine.js store
Alpine.store('auth', {
  user: null,
  role: null,
  isAuthenticated: false,
  
  async init() {
    // Check authentication status
    const response = await fetch('/api/auth/profile');
    if (response.ok) {
      const user = await response.json();
      this.user = user;
      this.role = user.role;
      this.isAuthenticated = true;
    }
  },
  
  async logout() {
    await fetch('/api/auth/logout', { method: 'POST' });
    this.user = null;
    this.role = null;
    this.isAuthenticated = false;
    window.location.href = '/login';
  }
});

Login Page (src/pages/login.astro)

<Layout title="Login">
  <div class="login-container" x-data="loginPage()">
    <div class="login-card">
      <h1>🤠 Welcome to Heady</h1>
      <p>Strategic VPN management that's actually awesome</p>
      
      <button @click="login()" class="login-btn" :disabled="loading">
        <span x-show="!loading">Login with Authentik</span>
        <span x-show="loading">Connecting...</span>
      </button>
    </div>
  </div>

  <script>
    function loginPage() {
      return {
        loading: false,
        
        async login() {
          this.loading = true;
          window.location.href = '/api/auth/login';
        }
      };
    }
  </script>
</Layout>

🛡️ Security Features

Authentik-Specific Optimizations

  1. Group Discovery: Auto-detect groups, ak_groups, and attributes.groups claims
  2. Policy Integration: Leverage Authentik policies for advanced access control
  3. Session Sync: Optional session synchronization with Authentik
  4. Audit Integration: Forward auth events to Authentik audit logs

Heady Security Enhancements

  1. Role Hierarchy: Owner > Admin > Network Admin > IT Admin > Auditor > Member
  2. Session Security: HTTP-only cookies, secure flags, CSRF protection
  3. Route Protection: Server-side route validation before page render
  4. Audit Logging: All authentication events logged for compliance

🚀 Configuration

Environment Variables

# Authentik Configuration
AUTHENTIK_ISSUER="https://auth.company.com/application/o/heady/"
AUTHENTIK_CLIENT_ID="heady-production"
AUTHENTIK_CLIENT_SECRET="your-secret-here"

# Public URLs
PUBLIC_URL="https://heady.company.com"
HEADY_URL="https://heady.company.com"  # Fallback

# Custom Role Mapping (Optional)
HEADY_OWNER_GROUPS="ceo,founders,executives"
HEADY_ADMIN_GROUPS="admin,administrators,managers"
HEADY_NETWORK_GROUPS="network,devops,sre,infrastructure"

# Session Configuration
SESSION_SECRET="your-32-char-secret-here"
SESSION_LIFETIME="24h"

Astro Configuration (astro.config.mjs)

export default defineConfig({
  output: 'hybrid',
  adapter: node({ mode: 'standalone' }),
  
  integrations: [
    tailwind({ applyBaseStyles: false })
  ],
  
  vite: {
    define: {
      // Expose public config to client-side
      PUBLIC_HEADY_VERSION: JSON.stringify(process.env.npm_package_version),
      PUBLIC_AUTH_ENABLED: JSON.stringify(!!process.env.AUTHENTIK_CLIENT_ID),
    }
  }
});

🎯 Implementation Strategy

Phase 1: Core Authentication

  1. OIDC client for Authentik
  2. Session management system
  3. Login/logout API routes
  4. Basic role mapping

Phase 2: UI Integration

  1. Login page with Alpine.js
  2. Global authentication state
  3. Protected route middleware
  4. User profile components

Phase 3: Advanced Features

  1. Authentik policy integration
  2. Advanced role mapping
  3. Audit logging
  4. Session synchronization

💡 Authentik Advantages

This architecture leverages Authentik's strengths:

  1. Modern OIDC: Full standard compliance with advanced features
  2. Flexible Groups: Multiple group sources and custom attributes
  3. Policy Engine: Advanced access control beyond simple roles
  4. Audit Ready: Built-in audit trails and compliance features
  5. Self-Service: User profile management and password reset flows

🎯 Result: Secure, maintainable, Authentik-optimized authentication that fits perfectly with Heady's "Security Through Simplicity" philosophy!