How to Securely Fetch Unpublished CraftCMS Entries with a Custom API Header

Vadim KononovVadim Kononov
4 min read

CraftCMS provides a robust GraphQL API for querying your content, but sometimes you need to tailor the API for different user roles. For example, you might want admins to fetch all entries (including unpublished ones) while the public can only see published entries. In this blog post, we’ll show you how to use a custom API header to securely control access to your entries.


Why Use a Custom Header?

A custom API header allows you to:

  • Tailor API responses for different user roles without creating separate endpoints.

  • Securely differentiate access levels by validating the header’s value.

  • Keep your public-facing API clean and free of admin-specific logic.

For this tutorial, we’ll configure CraftCMS to:

  1. Fetch only published (live) entries for public users.

  2. Fetch all entries (published and unpublished) for admins with a valid header.


Step 1: Define the Custom Header

We’ll use a header called X-Admin-Access to signal admin-level requests. Requests with this header will return all entries, while requests without it will default to fetching only published entries.

Example Headers:

  • Public Request:

      GET /graphql
    
  • Admin Request:

      GET /graphql
      X-Admin-Access: your-secure-admin-key
    

Step 2: Create a Custom Module in CraftCMS

CraftCMS’s modular architecture lets you hook into its GraphQL system. We’ll use a custom module to intercept GraphQL queries and adjust them based on the presence of X-Admin-Access.

Custom Module Code

Add this to your module’s init method:

namespace modules\custommodule;

use Craft;
use craft\gql\base\Resolver;
use craft\gql\resolvers\elements\Entry;
use GraphQL\Type\Definition\ResolveInfo;

class CustomModule extends \yii\base\Module
{
    public function init()
    {
        parent::init();

        // Hook into GraphQL resolvers for entries
        Entry::setCustomResolver([$this, 'customEntryResolver']);
    }

    public function customEntryResolver($source, array $arguments, $context, ResolveInfo $resolveInfo)
    {
        $request = Craft::$app->getRequest();
        $headers = $request->getHeaders();

        // Validate the X-Admin-Access header
        $adminKey = Craft::parseEnv('$ADMIN_API_KEY'); // Secure key from .env
        $isAdminAccess = $headers->get('X-Admin-Access') === $adminKey;

        // Adjust query based on header
        if ($isAdminAccess) {
            $arguments['status'] = null; // Fetch all entries
        } else {
            $arguments['status'] = 'live'; // Fetch only published entries
        }

        // Use the default CraftCMS resolver
        return Resolver::defaultResolver($source, $arguments, $context, $resolveInfo);
    }
}

Step 3: Add the Admin API Key to .env

For security, store your admin key in the .env file. Never hardcode sensitive data in your codebase.

Add the following to your .env file:

ADMIN_API_KEY=your-secure-admin-key

Step 4: Using the Custom Header

Public Request

When a public user queries your GraphQL API, they will only see live (published) entries.

Example Public Query:

query {
  entries(section: "blog") {
    title
    slug
  }
}

Response:

[
  {
    "title": "Published Entry 1",
    "slug": "published-entry-1"
  },
  {
    "title": "Published Entry 2",
    "slug": "published-entry-2"
  }
]

Admin Request

When an admin queries the API with the X-Admin-Access header, all entries (published and unpublished) are returned.

Example Admin Query:

query {
  entries(section: "blog") {
    title
    slug
    status
  }
}

Request Header:

X-Admin-Access: your-secure-admin-key

Response:

[
  {
    "title": "Published Entry 1",
    "slug": "published-entry-1",
    "status": "live"
  },
  {
    "title": "Unpublished Entry 2",
    "slug": "unpublished-entry-2",
    "status": "disabled"
  }
]

Step 5: Secure Your API

1. Use HTTPS

Always serve your API over HTTPS to encrypt headers during transmission.

2. Validate the Admin Key

Ensure the X-Admin-Access header matches the secure key stored in .env. Use hash_equals for secure string comparison to prevent timing attacks.

3. Rotate the Admin Key

Periodically change the admin key to mitigate the risk of compromise. Update the .env file and notify authorized users accordingly.


Step 6: Test Your Implementation

Test both public and admin requests to ensure:

  • Public users only see published entries.

  • Admin users with a valid header see all entries.

  • Invalid or missing headers result in the default behavior (public view).


Conclusion

By leveraging CraftCMS’s extensibility and GraphQL capabilities, you can implement secure, role-based API access with a simple custom header. This approach keeps your public API lightweight while providing full access to admins when needed.

If you have additional security needs, consider combining this approach with rate limiting, IP whitelisting, or Craft’s user roles for even finer control.

0
Subscribe to my newsletter

Read articles from Vadim Kononov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vadim Kononov
Vadim Kononov

I am an accomplished Solution Architect, Full Stack Developer and DevOps Specialist with a passion for creative leadership and mentorship, business optimization and technical direction, and ingenious solutions to complex problems. I am especially interested in App & Web Development, Cyber Security, Cloud Computing, Data Science, Open Source Software, Statistical Analysis and Discrete Mathematics.