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#
- Authentication: OAuth2 Proxy intercepts requests and redirects to Keycloak for OIDC login.
- Identity passed to the application: OAuth2 Proxy forwards the user identity via HTTP headers:
X-Forwarded-Preferred-Username— the human-readable username.X-Forwarded-Email— the user's email address.- Application embeds the Matomo tracker: the app reads these headers and passes the user ID to Matomo via
setUserId(). - 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.jsloads 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#
- Sign in to your Matomo instance.
- Go to Administration (gear icon) → Websites → Manage.
- Note the ID column for your site — this is your
YOUR_SITE_ID. - Use this ID in the tracking code above.
Verification#
After integrating:
- Browse your application and sign in via Keycloak.
- Open the Matomo dashboard → Visitors → Visits Log.
- Confirm visits appear with the correct Username (your
setUserIdvalue). - 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.