Skip to content

Matomo + Keycloak Integration#

This guide describes how to integrate a Matomo analytics service with applications protected by Keycloak OIDC authentication, so every page view is tracked with the authenticated user's identity.

Overview#

Applications are protected by an OAuth2 proxy that delegates sign-in to Keycloak. The application embeds the Matomo JavaScript tracker, passing the authenticated user's identity so that Matomo reports can be filtered per user and per application.

Architecture#

Browser
  ├── GET /app/ ──→ Reverse Proxy ──→ OAuth2 Proxy ──→ Your Application
  │                      │                 │
  │                app.example.com        Keycloak        Matomo JS Tracker
  │                                      (OIDC)           (embed in app pages)
  └── Tracking beacon ──→ Reverse Proxy (/analytics/*) ──→ Matomo
         (same-origin)                                      (reverse-proxied)

How it works#

  1. Authentication: OAuth2 Proxy intercepts requests and redirects to Keycloak for OIDC login.
  2. Identity passed to the application: OAuth2 Proxy forwards the user identity via HTTP headers:
  3. X-Forwarded-Preferred-Username — the human-readable username.
  4. X-Forwarded-Email — the user's email address.
  5. Application embeds the Matomo tracker: the app reads these headers and passes the user ID to Matomo via setUserId().
  6. Tracking beacon proxied: a reverse-proxy route makes tracking requests same-origin, avoiding CORS and third-party cookie blocking.

Application integration#

Step 1: Read user identity from OAuth2 Proxy headers#

When OAuth2 Proxy completes authentication, your application receives these headers on every request:

Header Example Value Source
X-Forwarded-Preferred-Username jdoe preferred_username claim
X-Forwarded-Email jdoe@example.com email claim (may also contain the username depending on provider config)
X-Forwarded-User b119e57-... sub (unique ID, not human-readable)

Use X-Forwarded-Preferred-Username as the Matomo user ID. It is human-readable, stable, and requires no token parsing or JWT support.

Email is a nice-to-have, not required. If X-Forwarded-Email contains an @, use it directly. Otherwise, skip email tracking or construct it as {username}@<your-domain>. JWT decoding is not required for the integration.

Step 2: Embed the Matomo tracker#

Add this to your application's page template (after authentication):

<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['setUserId', '{{ PREFERRED_USERNAME }}']);
_paq.push(['setCustomVariable', 1, 'application', '{{ APP_NAME }}', 'page']);
_paq.push(['setCustomVariable', 2, 'email', '{{ EMAIL }}', 'visit']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
  var u = "/analytics/";
  _paq.push(['setTrackerUrl', u + 'matomo.php']);
  _paq.push(['setSiteId', 'YOUR_SITE_ID']);
  var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
  g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
})();
</script>

Key points:

  • setUserId('username') ties all analytics events to a known user.
  • setCustomVariable(1, 'application', ...) records which application the user authenticated with — filter Matomo reports by this variable to see per-app metrics.
  • setCustomVariable(2, 'email', ...) is optional.
  • setTrackerUrl('/analytics/matomo.php') sends tracking through the reverse proxy (same-origin).

Step 3: Configure the reverse proxy (avoids CORS)#

Add these routes to your reverse proxy (for example Traefik) configuration. This serves Matomo at the same domain as your application, eliminating CORS issues and third-party cookie blocking:

http:
  middlewares:
    strip-analytics:
      stripPrefix:
        prefixes:
          - /analytics

  routers:
    # Your existing application router
    app:
      rule: "Host(`app.example.com`)"
      middlewares:
        - oauth2-proxy
      service: app-backend

    # Matomo tracking proxy
    matomo-tracking:
      rule: "Host(`app.example.com`) && PathPrefix(`/analytics/`)"
      middlewares:
        - oauth2-proxy          # optional: require auth for tracking
        - strip-analytics       # strip /analytics before forwarding
      service: matomo-backend

  services:
    matomo-backend:
      loadBalancer:
        servers:
          - url: "https://<your-matomo-instance>/"

Without the proxy (direct tracking):

  • matomo.js loads cross-origin — works via a <script> tag (no CORS check).
  • The tracking beacon uses an image-pixel fallback — works, but Matomo cannot set cookies.
  • Visitor tracking degrades to fingerprint-based on Safari/Firefox.

With the proxy (recommended):

  • All requests are same-origin — full Matomo tracking fidelity.
  • Optionally put tracking behind OAuth2 Proxy for authenticated-only analytics.
  • No DNS changes needed — apps reference /analytics/ as a relative path.

Step 4: Get your Matomo site ID#

  1. Sign in to your Matomo instance.
  2. Go to Administration (gear icon) → WebsitesManage.
  3. Note the ID column for your site — this is your YOUR_SITE_ID.
  4. Use this ID in the tracking code above.

Verification#

After integrating:

  1. Browse your application and sign in via Keycloak.
  2. Open the Matomo dashboard → VisitorsVisits Log.
  3. Confirm visits appear with the correct Username (your setUserId value).
  4. Click a visit → check the Custom Variables tab for the application name (and email if tracked).

What you won't need to change#

Your existing infrastructure stays as-is:

  • Keycloak — no realm/client/user changes needed.
  • OAuth2 Proxy — no configuration changes (already passes X-Forwarded-* headers).
  • Identity provider / directory — no changes (federation continues through Keycloak).
  • Application routing — your routing remains identical.

The only new work is: (1) the reverse-proxy route for /analytics/, and (2) adding the JavaScript tracker to each application's page template.