OAuth and tokens¶
OAuth 2.1¶
- Authorisation code flow with PKCE S256 only.
- Dynamic client registration (RFC 7591) for public clients, when
enabledcris on (default). - Client ID metadata documents over HTTPS, when
allowcimdis 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.phpalso 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.