Skip to content

OAuth and tokens

OAuth 2.1

  • Authorisation code flow with PKCE S256 only.
  • Dynamic client registration (RFC 7591) for public clients, when enabledcr is on (default).
  • Client ID metadata documents over HTTPS, when allowcimd is on (default). Hosts that resolve to private or link-local addresses are refused.
  • Registration and metadata fetch are rate-limited.
  • The user signs in to Moodle and approves the scopes on a consent page. Tools then run as that user.

Endpoints

The issuer is {wwwroot}/local/mcp.

Role URL
Protected resource metadata {wwwroot}/local/mcp/wellknown.php?doc=resource
Authorisation {wwwroot}/local/mcp/authorize.php
Token {wwwroot}/local/mcp/token.php
Registration {wwwroot}/local/mcp/register.php
Revocation {wwwroot}/local/mcp/revoke.php

Discovery

The WWW-Authenticate header of a 401 always includes a resource_metadata URL under /local/mcp/. Clients that honour it need nothing else.

Clients that follow RFC 8414 also request:

  • {wwwroot}/.well-known/oauth-authorization-server/local/mcp
  • {wwwroot}/.well-known/openid-configuration/local/mcp
  • {wwwroot}/local/mcp/.well-known/openid-configuration
  • {wwwroot}/.well-known/oauth-protected-resource/local/mcp/server.php (protected resource metadata)

For those, add rewrites. Apache, with the document root at the Moodle web root (public/ on 5.x):

RewriteEngine On
RewriteRule ^\.well-known/oauth-protected-resource /local/mcp/wellknown.php?doc=resource [L,QSA]
RewriteRule ^\.well-known/oauth-authorization-server /local/mcp/wellknown.php?doc=as [L,QSA]
RewriteRule ^\.well-known/openid-configuration /local/mcp/wellknown.php?doc=as [L,QSA]
RewriteRule ^local/mcp/\.well-known/openid-configuration /local/mcp/wellknown.php?doc=as [L,QSA]

nginx:

location ~ ^/\.well-known/oauth-protected-resource {
    rewrite ^ /local/mcp/wellknown.php?doc=resource last;
}
location ~ ^/\.well-known/(oauth-authorization-server|openid-configuration) {
    rewrite ^ /local/mcp/wellknown.php?doc=as last;
}
location = /local/mcp/.well-known/openid-configuration {
    rewrite ^ /local/mcp/wellknown.php?doc=as last;
}

PHP's built-in server does not read .htaccess. Pass the plugin router:

php -S 127.0.0.1:8081 -t /path/to/moodle /path/to/moodle/local/mcp/router.php
# Moodle 5.x:
php -S 127.0.0.1:8082 -t /path/to/moodle/public /path/to/moodle/public/local/mcp/router.php

Scopes

Scope Includes Capability needed
read Read tools local/mcp:read
write read + write tools local/mcp:write
admin write + read local/mcp:admin

A token without the scope a tool needs gets HTTP 403 with WWW-Authenticate: Bearer error="insufficient_scope".

Token lifetime and revocation

  • Access token: 3600 s by default (60–86400). Authorisation code: 300 s (30–600). Refresh token: 30 days (1 hour–365 days). See Settings.
  • Refresh tokens rotate on every use. Presenting a refresh token that was already used revokes the whole token family.
  • Revoking an access token at revoke.php also revokes its refresh token and the rest of the family.
  • Administrators can list and revoke tokens with the CLI.
  • Tokens, authorisation codes and client secrets are stored only as SHA-256 hashes.
  • A daily cleanup task (03:15) removes expired codes, sessions and old rate-limit rows. Revoked tokens are kept two weeks so refresh-token reuse can still be detected; expired access tokens are deleted after a week.

Web service token fallback

With Accept Moodle web service tokens (allowwstoken) on, Authorization: Bearer may carry a Moodle wstoken instead of an OAuth access token. The token is checked in external_tokens (expiry, IP restriction, user status). The call then has every MCP scope that the user's capabilities allow.

Leave it off unless you already manage web service tokens and accept that they are not scoped as finely as OAuth.