This plugin adds a first-party OAuth 2.0 Authorization Server to Matomo, allowing external applications to securely access Matomo APIs using OAuth2 access tokens instead of token_auth.
It supports standard OAuth 2.0 flows including Authorization Code (PKCE), Client Credentials, and Refresh Token.
The OAuth 2.0 plugin replaces static authentication with a token-based flow tied to your existing login system. Each application requests permission, receives scoped access, and operates within defined limits. No need to distribute or manage long-lived credentials across tools and services.
Tokens expire by default, can be refreshed when needed, and revoked instantly without affecting other integrations. This reduces exposure and simplifies access management.
For teams running multiple integrations, OAuth 2.0 is the practical choice for secure, maintainable access to Matomo data. Every connection is authorised, bounded, and straightforward to control.
- OAuth 2.0 Authorization Server integrated with Matomo
- Manage OAuth clients via Administration → Platform → OAuth 2.0 (For Matomo Cloud it will be Administration → Export → OAuth 2.0)
- Supported grant types:
- Authorization Code (with PKCE)
- Client Credentials
- Refresh Token
- OAuth scopes:
matomo:readmatomo:writematomo:adminmatomo:superuser
- RSA signing keys for JWT tokens
- Built using league/oauth2-server
- Bearer token authentication for Matomo APIs
- Client management UI with create, edit, pause/resume, delete, and secret rotation for confidential clients
| Endpoint | Description |
|---|---|
/index.php?module=OAuth2&action=authorize |
Authorization endpoint |
/index.php?module=OAuth2&action=token |
Token endpoint |
Optional cleaner routes can be added:
/oauth2/authorize
/oauth2/token
The plugin exposes an RFC 8414 OAuth 2.0 Authorization Server Metadata document so clients can discover the endpoints and capabilities automatically:
/.well-known/oauth-authorization-server
It returns JSON describing the issuer, authorization/token endpoints, supported scopes, grant types, response types, PKCE methods, and token endpoint authentication methods. The contents reflect your current OAuth 2.0 system settings (e.g. disabled grant types are omitted).
Example response:
{
"issuer": "https://matomo.example.com",
"authorization_endpoint": "https://matomo.example.com/index.php?module=OAuth2&action=authorize",
"token_endpoint": "https://matomo.example.com/index.php?module=OAuth2&action=token",
"scopes_supported": ["matomo:read", "matomo:write", "matomo:admin"],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256", "plain"],
"grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"]
}Other plugins (for example an MCP or AI integration) can add or override fields in the discovery document by listening to the OAuth2.authorizationServerMetadata event. It is fired after the base document is built and receives the metadata array by reference:
public function registerEvents()
{
return [
'OAuth2.authorizationServerMetadata' => 'extendAuthorizationServerMetadata',
];
}
public function extendAuthorizationServerMetadata(array &$metadata)
{
$metadata['registration_endpoint'] = 'https://matomo.example.com/index.php?module=MyPlugin&action=register';
}This is the recommended way to contribute optional RFC 8414 fields such as registration_endpoint, introspection_endpoint, revocation_endpoint, or jwks_uri.
The discovery path lives at the domain root, outside Matomo's index.php, so the web server has to serve something there.
Root installs (Matomo at https://example.com/): no web server configuration needed in most cases. The plugin writes the document as a static file to .well-known/oauth-authorization-server in the Matomo directory, which Apache, nginx and IIS serve like any other file. It also writes a .htaccess (Apache) and a web.config (IIS) next to it, so the file is sent as JSON. The .htaccess needs AllowOverride FileInfo, as Matomo's own .htaccess files do; if your server doesn't apply .htaccess files, the System Check shows a virtual host block instead. The file is written when the plugin is activated or updated to 6.0.5, and again whenever the system settings change or another plugin is activated or deactivated. A daily scheduled task rewrites it when its content has changed for any other reason, for example after a move to a new domain or to HTTPS, or after a plugin update that adds fields. It is removed when the plugin is deactivated.
The file is only written for an https Matomo URL, as RFC 8414 requires an https issuer. Matomo stores the URL it was last opened with, so on an install that is also reachable over http, set force_ssl = 1 in the [General] section of config/config.ini.php: otherwise a single http request would change it, and the file would not be updated until Matomo is opened over https again (the System Check warns about this).
Nginx ignores both helper files and serves the extensionless file with its default_type, usually application/octet-stream, which some clients reject. Add this inside the Matomo server { ... } block so it is sent as JSON:
location = /.well-known/oauth-authorization-server {
default_type application/json;
}This needs the Matomo directory to be writable by PHP. If it is not, or to update the file by hand, run:
./console oauth2:write-discovery-file
The file is never written over a file that was not written by Matomo, unless --force is given. It describes the Matomo URL stored for the install, which follows the host Matomo was last opened under. If Matomo is reachable under several host names, the file names whichever was used last, so use the web server rule below instead.
Several web servers each serve their own copy of the Matomo directory, but the file is only written or removed on the server that runs the action. Run ./console oauth2:write-discovery-file on each of them (or include the file in your deployment), or use the web server rule below.
Several Matomo instances on one codebase (host specific config files) share the Matomo directory but not the Matomo URL, so one file can't describe all of them. Matomo doesn't write the file when it runs with a host specific config, so use the web server rule below. On other shared setups, turn writing it off in each instance's config.ini.php:
[OAuth2]
write_discovery_file = 0Setting write_discovery_file = 1 writes the file even with a host specific config.
Check it: Administration → System → System Check shows OAuth 2.0 discovery endpoint. It requests the discovery URL and reports whether clients can discover the authorization server and whether the document is sent as JSON. If they can't, it shows the web server rule for your server, with your subdirectory filled in.
Use a rule forwarding the discovery path to the plugin's metadata action (index.php?module=OAuth2&action=metadata) when Matomo is installed in a subdirectory, when the static file can't be written, or when Matomo is reachable under several host names. Use an internal rewrite (not a redirect), matching the exact path with no trailing slash.
Until this is fixed in Matomo core, each forwarded request makes Matomo store a URL under /.well-known/ as its own URL (e.g. https://example.com/.well-known/), which links in emails and reports use until Matomo is opened in a browser again. The plugin ignores such a URL for the discovery document.
Apache (.htaccess in the Matomo root, or the vhost):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^/?\.well-known/oauth-authorization-server$ /index.php?module=OAuth2&action=metadata [L,QSA]
</IfModule>Nginx (inside the Matomo server { ... } block):
location = /.well-known/oauth-authorization-server {
rewrite ^ /index.php?module=OAuth2&action=metadata last;
}IIS (requires the URL Rewrite module; inside <system.webServer> of the root web.config):
<rewrite>
<rules>
<rule name="OAuth2 discovery" stopProcessing="true">
<match url="^\.well-known/oauth-authorization-server$" />
<action type="Rewrite" url="index.php?module=OAuth2&action=metadata" />
</rule>
</rules>
</rewrite>Without the URL Rewrite module, IIS rejects a web.config containing a <rewrite> section with a 500.19 error for the whole site, so install the module first.
In all cases the rewrite is internal. The plugin derives the issuer from the original request path, or uses the Matomo base URL when the server passes on the rewritten one. Do not use a redirect (return 301 / rewrite ... permanent), as that would change the path and break discovery.
A web server rule takes precedence over the static file, so with a rule in place the document is always served by Matomo itself.
DDEV uses nginx-fpm by default. Add a config snippet so DDEV includes it inside the site's server { ... } block — create .ddev/nginx/oauth2-well-known.conf:
location = /.well-known/oauth-authorization-server {
rewrite ^ /index.php?module=OAuth2&action=metadata last;
}Then run ddev restart to apply it. (If your project uses the apache-fpm webserver type instead, put the Apache RewriteRule above into .ddev/apache/apache-site.conf and restart.)
Notes:
-
Subdirectory installs: the
issueris the Matomo base URL including the subdirectory (e.g.https://example.com/matomo). Per RFC 8414 §3.1, the well-known string is inserted between the host and the issuer's path component, so the canonical discovery URL ishttps://example.com/.well-known/oauth-authorization-server/matomo. Note this lives at the domain root, not under the Matomo directory, so the static file can't be used and the rewrite must go in the root server config and route to the subdirectory's front controller, e.g. Apache:RewriteRule ^/?\.well-known/oauth-authorization-server/matomo$ /matomo/index.php?module=OAuth2&action=metadata [L,QSA]
The plugin derives the
issuercorrectly from either layout, so it also accepts the appended formhttps://example.com/matomo/.well-known/oauth-authorization-server— handy when you can only edit Matomo's own.htaccess— but the RFC 8414 form above is what spec-compliant clients request.
Navigate to:
Administration → Platform → OAuth 2.0 (On-premise)
Administration → Export → OAuth 2.0 (Matomo Cloud)
Create a client and configure:
- Client type
- Confidential (requires client secret)
- Public (no secret)
- Allowed grant types
- Allowed scopes
- Redirect URI (required for Authorization Code flow)
From the client list, you can pause or resume a client, open it for editing, or delete it. For confidential clients, secret rotation is available from the edit screen.
Client secrets are shown in full only when a confidential client is created or when its secret is rotated. After that, the secret is masked and must be rotated again if you need a new value.
Example client:
Client ID: analytics_app
Client Secret: 7fa9c0f81b8b4a12
Redirect URI: https://example-app.com/oauth/callback
- Client redirects the user to
/authorize - User logs into Matomo and approves access
- Matomo redirects back with an authorization
code - Client exchanges the
codefor an access token at/token - Client calls Matomo APIs using
Authorization: Bearer ACCESS_TOKEN
PKCE requires two values:
code_verifiercode_challenge
Example:
code_verifier = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
Where:
code_challenge = BASE64URL(SHA256(code_verifier))
https://matomo.example.com/index.php?module=OAuth2&action=authorize
&response_type=code
&client_id=analytics_app
&redirect_uri=https://example-app.com/oauth/callback
&scope=matomo:read
&state=abc123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
https://matomo.example.com/index.php?module=OAuth2&action=authorize
&response_type=code
&client_id=analytics_app
&redirect_uri=https://example-app.com/oauth/callback
&scope=matomo:read
&state=abc123
The user will:
- Log in to Matomo
- Review requested permissions (if the request lists multiple space-separated scopes, pick exactly one to grant — the least privileged scope is preselected)
- Click Allow
Matomo will redirect back:
https://example-app.com/oauth/callback?code=AUTHORIZATION_CODE&state=abc123
Note: The authorize request may list multiple space-separated scopes (e.g. scope=matomo:read matomo:write). The user grants exactly one of them on the consent screen, and the issued tokens always carry that single scope. The scope configured for a client is the maximum a user can grant it, so a client configured with matomo:admin can also be granted matomo:write or matomo:read. Tokens obtained with the client credentials grant always use the exact configured scope.
curl -X POST 'https://matomo.example.com/index.php?module=OAuth2&action=token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'client_id=analytics_app' \
-d 'redirect_uri=https://example-app.com/oauth/callback' \
-d 'code=AUTHORIZATION_CODE' \
-d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
curl -X POST 'https://matomo.example.com/index.php?module=OAuth2&action=token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'client_id=analytics_app' \
-d 'client_secret=7fa9c0f81b8b4a12' \
-d 'redirect_uri=https://example-app.com/oauth/callback' \
-d 'code=AUTHORIZATION_CODE'
The Client Credentials grant is used when a backend service needs to access Matomo APIs without user interaction.
Typical use cases:
- Internal analytics dashboards
- Scheduled data exports
- Backend integrations
curl -X POST 'https://matomo.example.com/index.php?module=OAuth2&action=token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d 'client_id=analytics_app' \
-d 'client_secret=7fa9c0f81b8b4a12' \
-d 'scope=matomo:read'
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "def50200a1b9"
}Use a refresh token to obtain a new access token.
curl -X POST 'https://matomo.example.com/index.php?module=OAuth2&action=token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'client_id=analytics_app' \
-d 'refresh_token=def50200a1b9'
curl -X POST 'https://matomo.example.com/index.php?module=OAuth2&action=token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'client_id=analytics_app' \
-d 'client_secret=7fa9c0f81b8b4a12' \
-d 'refresh_token=def50200a1b9'
Once an access token is obtained, call Matomo APIs using the Bearer token.
Example:
curl https://matomo.example.com/index.php \
-H "Authorization: Bearer ACCESS_TOKEN" \
-d "module=API" \
-d "method=VisitsSummary.get" \
-d "idSite=1" \
-d "period=day" \
-d "date=today" \
-d "format=json"
Example response:
{
"nb_visits": 124,
"nb_actions": 210
}- Never expose client secrets in frontend applications.
- Public clients must use PKCE.
- Always use HTTPS.
- Store access tokens securely.
- Rotate client secrets periodically.