Skip to content

Grav Web Services — Webhooks, OAuth, SSO and LDAP Integration

DodaTech Updated 2026-06-27 7 min read

In this tutorial, you'll learn Grav web services — integrating webhooks for real-time events, configuring OAuth providers for social login, setting up single sign-on (SSO), LDAP authentication, and connecting Grav with external APIs.

What You'll Learn

  • Webhook integration: sending and receiving webhooks
  • OAuth authentication with GitHub, Google, and Facebook
  • Single sign-on (SSO) configuration
  • LDAP authentication for enterprise environments
  • External API integrations and service connections
  • Event-driven automation with webhooks

Why It Matters

In WordPress, webhooks and integrations require plugins or custom code. In Grav, you build integrations directly in plugins using events and HTTP clients. Webhooks let Grav notify external services when events happen (page saved, user registered). OAuth and SSO let users log in with existing accounts. LDAP integrates with corporate directories. These integrations turn Grav from a standalone CMS into a connected platform.

Real-World Use

A developer documentation site needs to: notify a Slack channel when new documentation is published, allow users to log in with their GitHub accounts, sync user accounts with the company LDAP directory, and automatically deploy to production when pages are saved. All of these integrations are handled by a single plugin that subscribes to Grav events and makes API calls to external services.

Learning Path

flowchart LR
    A["Grav API"] --> B["Web Services
← You are here"]:::current B --> C["E-commerce with Grav"] C --> D["Caching Deep Dive"] D --> E["Performance Optimization"] E --> F["Security"] F --> G["Git Workflow"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Sending Webhooks

Send webhook notifications when Grav events occur:

user/plugins/webhooks/webhooks.php:

<?php
namespace Grav\Plugin;

use Grav\Common\Plugin;

class WebhooksPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onPluginsInitialized' => ['onPluginsInitialized', 0],
        ];
    }

    public function onPluginsInitialized()
    {
        if ($this->isAdmin()) {
            $this->enable([
                'onAdminAfterSave' => ['onAdminAfterSave', 0],
            ]);
        }
    }

    public function onAdminAfterSave($event)
    {
        $page = $event['page'];
        $webhookUrl = $this->grav['config']->get('plugins.webhooks.page_update_url');

        if (!$webhookUrl) {
            return;
        }

        $data = [
            'event' => 'page_saved',
            'title' => $page->title(),
            'route' => $page->route(),
            'url' => $page->url(true),
            'modified' => $page->modified(),
            'timestamp' => time(),
        ];

        $this->sendWebhook($webhookUrl, $data);
    }

    private function sendWebhook($url, $data)
    {
        $client = new \GuzzleHttp\Client();
        try {
            $client->post($url, [
                'json' => $data,
                'timeout' => 5,
                'headers' => [
                    'User-Agent' => 'Grav-Webhook/1.0',
                    'X-Webhook-Event' => $data['event'],
                ],
            ]);
        } catch (\Exception $e) {
            $this->grav['log']->error('Webhook failed: ' . $e->getMessage());
        }
    }
}

Webhook Configuration

user/plugins/webhooks/webhooks.yaml:

enabled: true
page_update_url: 'https://hooks.slack.com/services/TXXXX/BXXXX/XXXXX'
deploy_webhook: 'https://api.netlify.com/build_hooks/XXXXX'
notify_on:
    - page_save
    - page_delete
    - user_register

Deploy Webhook Example

public function onAdminAfterSave($event)
{
    $deployUrl = $this->grav['config']->get('plugins.webhooks.deploy_webhook');
    if ($deployUrl) {
        $this->sendWebhook($deployUrl, [
            'trigger' => 'content_update',
            'page' => $event['page']->route(),
        ]);
    }
}

Receiving Webhooks

Accept incoming webhooks from external services:

public function onPageInitialized()
{
    $route = $this->grav['uri']->route();

    if ($route === '/webhook/receive') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            http_response_code(405);
            exit;
        }

        // Verify webhook secret
        $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
        $secret = $this->grav['config']->get('plugins.webhooks.secret');
        $payload = file_get_contents('php://input');
        $expected = hash_hmac('sha256', $payload, $secret);

        if (!hash_equals($expected, $signature)) {
            http_response_code(401);
            echo json_encode(['error' => 'Invalid signature']);
            exit;
        }

        // Process webhook
        $data = json_decode($payload, true);
        $this->processIncomingWebhook($data);

        http_response_code(200);
        echo json_encode(['status' => 'received']);
        exit;
    }
}

private function processIncomingWebhook($data)
{
    $event = $data['event'] ?? 'unknown';

    switch ($event) {
        case 'github.push':
            $this->handleGitHubPush($data);
            break;

        case 'stripe.payment_succeeded':
            $this->handlePaymentSuccess($data);
            break;
    }
}

OAuth Authentication

Configure OAuth providers for social login:

user/config/plugins/login.yaml:

oauth:
    enabled: true
    providers:
        github:
            enabled: true
            client_id: 'your-github-client-id'
            client_secret: 'your-github-secret'
            options:
                scope: ['user:email']

        google:
            enabled: true
            client_id: 'your-google-client-id'
            client_secret: 'your-google-secret'
            options:
                scope: ['email', 'profile']

        facebook:
            enabled: true
            client_id: 'your-facebook-app-id'
            client_secret: 'your-facebook-app-secret'

Customizing OAuth Behavior

<?php
namespace Grav\Plugin;

class OAuthPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onOAuthAfterLogin' => ['onOAuthAfterLogin', 0],
        ];
    }

    public function onOAuthAfterLogin($event)
    {
        $provider = $event['provider'];
        $userData = $event['user'];
        $gravUser = $event['grav_user'];

        // Automatically create user if not exists
        if (!$gravUser) {
            $accounts = $this->grav['accounts'];
            $gravUser = $accounts->add([
                'username' => $userData['email'],
                'email' => $userData['email'],
                'fullname' => $userData['name'] ?? $userData['login'],
                'access' => ['site' => ['login' => true]],
                'state' => 'enabled',
            ]);
            $accounts->save($gravUser);
        }
    }
}

Single Sign-On (SSO)

For SSO with SAML or OpenID Connect, use a plugin:

# user/config/plugins/sso.yaml
enabled: true
provider: saml
saml:
    idp:
        entity_id: 'https://idp.example.com/metadata'
        sso_url: 'https://idp.example.com/sso'
        x509_cert: 'path/to/cert.pem'
    sp:
        entity_id: 'https://grav.example.com/saml'
        acs_url: 'https://grav.example.com/saml/acs'

LDAP Authentication

Configure LDAP in a custom plugin:

# user/config/plugins/ldap.yaml
enabled: true
server:
    host: 'ldap.example.com'
    port: 389
    use_ssl: false
    base_dn: 'dc=example,dc=com'
    user_dn: 'uid=%s,ou=users,dc=example,dc=com'
    filter: '(uid=%s)'
options:
    auto_create: true
    default_access:
        site:
            login: true

LDAP authentication handler:

<?php
namespace Grav\Plugin;

class LdapPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onUserAuthenticate' => ['onUserAuthenticate', 0],
        ];
    }

    public function onUserAuthenticate($event)
    {
        $credentials = $event['credentials'];
        $username = $credentials['username'];
        $password = $credentials['password'];

        // Try LDAP first, fall back to local auth
        $ldapUser = $this->authenticateLdap($username, $password);

        if ($ldapUser) {
            $event->setUser($ldapUser);
            $event->setStatus('success');
        }
    }

    private function authenticateLdap($username, $password)
    {
        $config = $this->grav['config']->get('plugins.ldap');

        $ldap = ldap_connect($config['server']['host'], $config['server']['port']);
        ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);

        $userDn = str_replace('%s', ldap_escape($username, '', LDAP_ESCAPE_DN), $config['server']['user_dn']);

        if (@ldap_bind($ldap, $userDn, $password)) {
            // LDAP authentication successful
            if ($config['options']['auto_create']) {
                $this->autoCreateUser($username);
            }
            return $this->grav['accounts']->load($username);
        }

        return null;
    }
}

Learning Path

flowchart LR
    A["Grav API"] --> B["Web Services
← You are here"]:::current B --> C["E-commerce with Grav"] C --> D["Caching Deep Dive"] D --> E["Performance Optimization"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Not verifying webhook signatures: Incoming webhooks should verify a signature or secret to ensure the request is from the expected service. Without verification, anyone can send fake webhooks.

  2. Blocking on webhook delivery: Webhook HTTP calls should have a short timeout (5 seconds) and be wrapped in try-catch. A slow or failing webhook should not delay the page response.

  3. Storing OAuth secrets in templates: OAuth client secrets should be in plugin configuration, not in templates or version-controlled files. Use user/config/ files added to .gitignore.

  4. Not handling OAuth token refresh: OAuth access tokens expire. Implement token refresh logic to avoid suddenly losing access to the provider's API.

  5. LDAP connection pooling: Opening a new LDAP connection on every request is slow. Use persistent connections or Caching for LDAP authentication results.

Practice Questions

  1. How do you send a webhook when a page is saved in Grav? Answer: Subscribe to onAdminAfterSave event, extract page data, and use Guzzle HTTP client to POST the data to the webhook URL.

  2. How do you verify an incoming webhook is legitimate? Answer: Compare a hash-based message authentication code (HMAC) of the payload against a shared secret. The webhook sender includes the HMAC in a header.

  3. What OAuth providers does Grav's Login plugin support? Answer: GitHub, Google, Facebook, and any provider that supports OAuth 2.0. Additional providers can be added through custom plugins.

  4. How do you set up LDAP authentication as a fallback? Answer: Subscribe to onUserAuthenticate, try LDAP first, and if it fails, let the default authentication handler Process the credentials as local authentication.

  5. Challenge: Build a complete integration plugin that connects Grav with three external services. Implement: a Slack webhook that notifies a channel whenever a new page is published (include page title, URL, and author), a GitHub OAuth login that creates Grav user accounts from GitHub profiles, an incoming webhook endpoint that accepts JSON payloads from a CI/CD service and triggers a cache clear, and an email notification integration that sends a digest of daily content changes. Include proper error handling, logging, configuration in YAML, and security verification for incoming webhooks.

FAQ

Can I use webhooks with the Form plugin?

Yes. The Form plugin supports a webhook process action. When a form is submitted, the data is POSTed to the specified URL as JSON.

How do I test webhooks locally?

Use a tool like ngrok (https://ngrok.com) to expose your local Grav site to the internet. External services can then send webhooks to your ngrok URL.

What is the difference between OAuth and SSO?

OAuth is an authorization protocol often used for 'Login with Google' style buttons. SSO (Single Sign-On) is an authentication system that lets users log in once and access multiple applications.

Can I use multiple authentication methods simultaneously?

Yes. Grav's authentication system supports multiple handlers. You can have local accounts, OAuth, and LDAP all active at the same time. Each handler is tried in order.

How do I secure webhook endpoints from abuse?

Use secret verification (HMAC), IP whitelisting (only accept requests from known IP ranges), rate limiting (max requests per minute), and require HTTPS for all webhook URLs.

Mini Project

Goal: Build an integration hub plugin with multiple web services.

  1. Create a webhook sender that notifies Slack when pages are published
  2. Create an incoming webhook receiver for CI/CD deployment triggers
  3. Configure GitHub OAuth login with automatic account creation
  4. Create a Git webhook handler that auto-deploys on push
  5. Add a Stripe webhook handler for payment notifications
  6. Implement webhook signature verification for all incoming hooks
  7. Add a dashboard widget showing recent webhook activity
  8. Create a log viewer for webhook delivery status
  9. Add retry logic for failed webhook deliveries
  10. Test the entire integration system end-to-end

What's Next

Now you can integrate Grav with external services. Next, learn e-commerce:

Continue to Lesson 34: E-commerce with Grav — SimpleCart, payment gateways, and product management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro