Moodle 5.3: the new API finally gets real OAuth (the old one keeps its tokens)

Moodle 5.3: the new API finally gets real OAuth (the old one keeps its tokens)
ContenidoContents

Moodle 5.3 came out on 5 October 2026 and it is the new LTS release. Among all the visual changes there’s a small section in the release notes that interests me more than the rest: “OAuth 2 support for routed web services”.

Honest summary: Moodle’s new API finally has real OAuth. But the old stuff, the classic external-function web services, still works with the same tokens as always.

What exactly is arriving

For a few versions Moodle has been building a “routed” REST API (classes in the route\api namespace), separate from the classic external functions system. In 5.3 that API gets its own OAuth 2 server in core. The tracker issues are pretty clear:

  • MDL-89251: the OAuth 2 server backend (database schema, entities, repositories and dynamic scope discovery).
  • MDL-89181: a client manager. According to UPGRADING.md, \core\oauth2\server\client_manager manages the lifecycle of clients, their secrets and their redirect URIs.
  • MDL-88468: a UI to manage the server and its clients.
  • MDL-88472: the login flow screens. The login page shows which client is asking for access, there’s a new “Continue as [user]” screen and another one to confirm scopes.
  • MDL-88473: token revocation through a single /revoke endpoint following RFC 7009. It always returns 200, even for unknown tokens, so it doesn’t leak information. Revoking a refresh token also kills the access tokens issued from it.

So: registered clients, an authorization flow with user consent, refresh tokens and revocation. What you’d expect from normal OAuth.

Personal tokens, done properly

Besides the full OAuth flow there are personal access tokens (MDL-87706). Each user manages them at /user/personalaccesstokens.php, and creating them requires the new moodle/api:createtoken capability (again, per UPGRADING.md).

When you create one you give it a name, description, expiry and scopes. The value can only be copied once. After that it can’t be edited, only revoked. For my own scripts against my aula, this is exactly what I wanted.

Per-route scopes, and they’re mandatory

This is the part that affects plugin authors. The scopes documentation explains that when a request arrives with an OAuth 2 token or a personal token, the router also checks scopes. And it does so in addition to capabilities, not instead of them. A token with the right scope doesn’t bypass the user’s permissions.

Each route declares what it needs with the #[scopeset] attribute. Several scopes in the same #[scopeset] are an AND; repeating the attribute is an OR. If a route needs no scope (a status endpoint, for example), you have to say so explicitly with #[unscoped_resource].

And it’s not optional. As the 5.3 dev update says, a core PHPUnit test scans every route in route\api and fails if any lacks both attributes. If you have a plugin with routes, it’s time to review it.

A detail I like: if a #[scopeset] references a scope that doesn’t exist in the installed Moodle version, it isn’t ignored. It’s replaced with a placeholder that can never be satisfied. It fails closed, which is the right call.

Also note: scope validation only applies to token-authenticated requests. With a normal Moodle session nothing changes.

It needs Composer

The server uses the league/oauth2-server package, which isn’t bundled in core. MDL-88576 adds a check so that, if you haven’t run composer install, Moodle shows a warning instead of breaking. On my server that means adding the step to my upgrade process. Anyone deploying by just copying the zip will need to keep it in mind.

What doesn’t change

Classic web services (external functions, webservice/rest/server.php, the good old token) stay as they were. This OAuth is for the routed API, not the old one.

That’s why my local_sepe connector for SEPE online training gains nothing. It’s SOAP, it talks to SEPE’s services and doesn’t touch the new REST API.

Where I do see the point

New integrations. I’m thinking mainly of MCP servers: giving an assistant access to Moodle with a token limited to, say, reading courses, which the user approves on a screen and can revoke whenever they want. Until now that meant a web service token with too much power and little granularity.

Also any external tool that today asks for “an admin token” because there’s no other way. With per-route scopes you can ask for only what’s needed.

My plan

Upgrade the aula to 5.3 calmly, install the Composer dependencies, create a personal token with minimal scopes and try something small against the REST API. If it works as the docs promise, the next step will be a simple MCP on top. I’ll report back.

CompartirShare