Repository navigation
Use Laravel Passport's OAuth table layout and scope-bound clients - #20154
Merged
Merged
Conversation
Craft 6 uses Passport, so 5.12’s OAuth tables now use Passport’s column names and layout (plus resource_id), and clients record their grant types. Clients can only use the grant types they were registered with. Amp-Thread-ID: https://ampcode.com/threads/T-ae777030-d792-49ae-8853-87e83195c88f Co-authored-by: Amp <amp@ampcode.com>
This was referenced Oct 29, 2026
brandonkelly
marked this pull request as ready for review
October 29, 2026 04:57
brandonkelly
marked this pull request as draft
October 29, 2026 05:05
…by scope like Passport Tokens belong to the resource that defines their scopes, scope IDs must be unique across resources, and registered clients store their resource's scopes in Passport's optional oauth_clients.scopes column. Amp-Thread-ID: https://ampcode.com/threads/T-ae777030-d792-49ae-8853-87e83195c88f Co-authored-by: Amp <amp@ampcode.com>
brandonkelly
marked this pull request as ready for review
October 29, 2026 05:18
riasvdv
approved these changes
Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The goal is an upgrade path for OAuth tokens issued by Craft 5.12 to keep working in Craft 6, which uses Laravel Passport 13 for OAuth. Nobody uses 5.12's OAuth tables yet, so this PR changes them now to match Passport's layout exactly. Craft 6 can then adopt the rows as they are, with no migration.
There's no
resource_idanymore. A Craft OAuth resource (MCP, a plugin API) is now identified the way Passport identifies an API, by its scopes:Access tokens can carry over without changes. 5.12 and Craft 6 lock the same
league/oauth2-server(9.4.1) andlcobucci/jwt(5.6.0), so a 5.12 token is the same JWT that Passport validates:audis the client,jtiis the token row, andsubis the user. Passport'sTokenGuardonly needs the client row and the token row, and this PR stores both in the layout it reads. Themcp:*scope names already match 6.x'sMcpScope, and 6.x'sOAuthRegisterControlleralready fillsoauth_clients.scopeswhen the column exists.Changes:
m261007_100000_oauthcreates Passport 13's columns plus the optionaloauth_clients.scopes. Foreign keys are plain (client_idtooauth_clients.id). Columns 5.12 doesn't use (owner_*,secret,provider,updated_at) are created anyway so Craft 6 doesn't have to add them.OAuth::getResources()throws if two resources define the same scope ID. Docs recommend prefixing scopes with the resource ID.oauth_clients.scopes.craft\oauth\Client::hasScope()works like Passport's. A client with no scope in common with a resource can't use that resource's endpoints, and the authorize and token endpoints reject scopes outside the client's list.Repositoriesfinds tokens, codes and refresh tokens by ID only. A credential is valid for a resource when all of its scopes belong to that resource, the same check that already handled removed scopes.grant_typesthe way Passport does, andClient::supportsGrantType()enforces it. Authenticated GraphQL queries via OAuth #20147 needs this for personal access tokens.craft\oauth\AccessToken::$namefills the token'snamecolumn, which Passport uses for personal access tokens.docs/oauth.mdcovers unique scopes, client scopes, and an "Upgrading to Craft 6" note.Still needed outside this PR:
ensurePassportTables()skips existing tables, so an upgraded site keeps 5.12's tables, includingoauth_clients.scopes. Fresh 6.x installs don't create that column, which is fine because Passport treats a missing value as "any scope".storage/oauth-private.keyandstorage/oauth-public.key, which is where Passport reads them by default. Move OAuth signing keys into storage/ craft6-revamp#6 moves the keys whenCRAFT_STORAGE_PATHpoints elsewhere.securityKey, and Passport usesAPP_KEY. Clients authorize again once their access token expires. MCP clients do that on their own.Evidence
OAuthTest::testTablesHavePassportColumnscompares each table's columns with Passport 13's migrations (laravel/passport v13.8.0, as locked by 6.x) plusoauth_clients.scopes. It now allows no extra columns.OAuthTest::testClientsCanOnlyRequestTheirScopes: the MCP client getsinvalid_clientat the API resource's authorize endpoint. A client limited to["api:read"]getsinvalid_scopewhen it exchanges a code forapi:read api:write. A client with nullscopescan be granted both.Client::hasScope()always returning true, the test fails with "Expected OAuth error invalid_scope."OAuthTest::testCredentialsCannotCrossResourceBoundariesstill passes withoutresource_id. MCP codes, access tokens and refresh tokens are rejected at the API resource.OAuthTest::testScopesMustBeUniqueAcrossResources: anapiresource that also definesmcp:readthrowsInvalidConfigException.OAuthTest::testClientsCanOnlyUseTheirGrantTypes: a client whosegrant_typesdropsrefresh_tokengetsunauthorized_clientwhen it refreshes.vendor/bin/codecept run unit oauth: OK (22 tests).unit mcp: OK (29 tests).functional: OK (29 tests).