Ghost Keycloak OIDC intégration (member & staff)

:locked_with_key: Ghost Keycloak Bridge - Native SSO/OIDC Integration for Self-Hosted Ghost

Hey everyone! :waving_hand:

I know SSO/OIDC integration has been a highly requested feature for years on this forum. After struggling to find a working solution for my self-hosted Ghost instance, I decided to build my own bridge. It’s now stable and production-ready (but need somes feedbacks), so I’m sharing it with the community.


The Problem

If you’re self-hosting Ghost and want to integrate it with an identity provider (Keycloak, Authentik, or any OIDC provider), you’ve probably noticed that:

  • Native SSO is only available on Ghost(Pro) hosted plans

  • Existing third-party solutions are either abandoned or half-working

  • There’s virtually no documentation on how Ghost handles authentication internally

This bridge solves that problem by creating native Ghost sessions through what I call “Cookie Forgery” : essentially reverse-engineering how Ghost authenticates users and replicating that process after successful OIDC authentication.


Features

:white_check_mark: Member SSO (Blog Subscribers)

  • Auto-provisioning: New users are automatically created in Ghost on first login

  • Magic link integration: Uses Ghost’s native token system for seamless session establishment

  • Single Logout (SLO): Logging out clears both Ghost and Keycloak sessions

  • Signup support: Can redirect to Keycloak registration page

:white_check_mark: Staff SSO (Admin Panel)

  • Direct session injection: Creates valid admin sessions in Ghost’s database

  • User validation: Only allows login for existing Ghost staff members

  • Native cookie signing: Uses Ghost’s internal admin_session_secret

  • Optional UI injection: Adds a “Login with OIDC” button directly on /ghost/ login page

:white_check_mark: Production-Ready (v1.1.0)

  • Health check endpoints: /health, /ready, /startup for Kubernetes/Podman

  • Structured logging: Winston-based with JSON output for log aggregators

  • Comprehensive test suite: 102 tests, 84% code coverage

  • Lightweight Docker image: ~50MB (Node 22 Alpine)


How It Works

The bridge acts as a middleware between your reverse proxy and Ghost:

User → Nginx → Ghost Keycloak Bridge → Keycloak
                      ↓
                Ghost (API + Database)

Member Authentication Flow:

  1. User clicks “Login” → Redirected to Keycloak

  2. User authenticates → Keycloak redirects to bridge callback

  3. Bridge queries Ghost Admin API to find/create member

  4. Bridge generates a magic token and inserts it into Ghost’s tokens table

  5. User is redirected to /members/?token=xxx → Ghost establishes native session

Staff Authentication Flow:

  1. Staff clicks “Login with OIDC” → Redirected to Keycloak

  2. Staff authenticates → Keycloak redirects to bridge callback

  3. Bridge verifies user exists in Ghost’s users table

  4. Bridge creates a session record in Ghost’s sessions table

  5. Bridge signs the session cookie using Ghost’s secret

  6. Staff is redirected to /ghost/ with a valid admin session


Requirements

  • Ghost: v6.0+ (self-hosted with MySQL/MariaDB)

  • Keycloak (or any OIDC-compatible provider with some adjustments)

  • Shared domain: Ghost and the bridge must be on the same root domain (for cookie sharing)

  • Reverse proxy: Nginx, Traefik, Caddy, etc.


Quick Start

Docker Compose:

services:
  ghost-bridge:
    image: ghcr.io/astocanthus/ghost-keycloak-bridge:1.1.0
    environment:
      - BLOG_PUBLIC_URL=https://blog.example.com
      - GHOST_INTERNAL_URL=http://ghost:2368
      - DB_HOST=ghost-db
      - DB_USER=ghost
      - DB_PASSWORD=${GHOST_DB_PASSWORD}
      - MEMBER_KEYCLOAK_ISSUER=https://auth.example.com/realms/members
      - MEMBER_CLIENT_ID=ghost-members
      - MEMBER_CLIENT_SECRET=${MEMBER_CLIENT_SECRET}
      - MEMBER_CALLBACK_URL=https://blog.example.com/auth/member/callback
      - STAFF_KEYCLOAK_ISSUER=https://auth.example.com/realms/staff
      - STAFF_CLIENT_ID=ghost-admin
      - STAFF_CLIENT_SECRET=${STAFF_CLIENT_SECRET}
      - STAFF_CALLBACK_URL=https://blog.example.com/auth/admin/callback
      - GHOST_ADMIN_API_KEY=${GHOST_ADMIN_API_KEY}
    networks:
      - ghost-network

Nginx Configuration:

# Member authentication
location /auth/member/ {
    proxy_pass http://ghost-bridge:3000/auth/member/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

# Staff authentication
location /auth/admin/ {
    proxy_pass http://ghost-bridge:3000/auth/admin/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

API Endpoints

Endpoint Description
/health Liveness probe (always 200)
/ready Readiness probe (checks DB connection)
/auth/member/login Initiates member OIDC flow
/auth/member/login?action=signup Redirects to Keycloak registration
/auth/member/logout Clears cookies + Keycloak SLO
/auth/member/callback OIDC callback handler
/auth/member/debug Diagnostic endpoint (API connectivity test)
/auth/admin/login Initiates staff OIDC flow
/auth/admin/callback OIDC callback, creates admin session

Optional: Admin Login Button Injection

The bridge includes a script that patches Ghost’s admin UI to add a “Login with OIDC (Staff)” button:

services:
  ghost:
    image: ghost:6-alpine
    volumes:
      - ./ghost-sso/6.x/custom-start.sh:/var/lib/ghost/custom-start.js:ro
    command: ["node", "/var/lib/ghost/custom-start.js"]
```

Result:
```
┌─────────────────────────────────────┐
│         Ghost Admin Login           │
├─────────────────────────────────────┤
│  Email: [________________]          │
│  Password: [________________]       │
│                                     │
│  [ Sign in ]                        │
│  [ Login with OIDC (Staff) ]  ← NEW │
└─────────────────────────────────────┘

Limitations & Caveats

  • Keycloak-focused: Built and tested with Keycloak. Other OIDC providers may require adjustments.

  • Database access required: The bridge needs direct access to Ghost’s MySQL database (for session/token management).

  • No role sync: Staff roles are not synchronized from Keycloak. Users must exist in Ghost first (sécurity first).

  • Same domain required: Cookie-based authentication requires Ghost and the bridge to share the same root domain.


Roadmap

  • v1.2.0: Prometheus metrics (/metrics), rate limiting, ISO 27001 compliance improvements

  • Future: Generic OIDC support (beyond Keycloak), role mapping from OIDC claims


Links


Feedback Welcome!

This is my first major open-source contribution since years, and I’d love to hear your feedback. If you’re using it, having issues, or want to contribute, feel free to open an issue or PR on GitHub.

Happy self-hosting! :rocket:

1 Like