Dynamic client registration & MCP
The provider can let OAuth clients register themselves at runtime (RFC 7591) and advertise the discovery documents resource servers and their clients need to find it (RFC 8414 authorization server metadata, RFC 9728 protected resource metadata). Together these enable the connect flow MCP clients such as Claude or Cursor drive:
- The client calls the protected resource (e.g.
POST /mcp) without a token and receives a401whoseWWW-Authenticateheader points at the resource metadata. GET /.well-known/oauth-protected-resource/mcpnames this provider as the authorization server.GET /.well-known/oauth-authorization-server(or/.well-known/openid-configuration— both serve the same document) lists the authorize, token, and registration endpoints.- The client registers itself via
POST /oauth/registerand receives aclient_id. - A standard authorization-code flow with PKCE (
S256, enforced for every client) yields the access token the client then presents to the resource.
Protected resource metadata (RFC 9728)
Section titled “Protected resource metadata (RFC 9728)”Declare the resources this provider protects in oidc.resources, keyed by the resource’s path
relative to the issuer origin:
'resources' => [ 'mcp' => ['scopes' => ['mcp:use']],],GET /.well-known/oauth-protected-resource/mcp then serves:
{ "resource": "https://id.example.com/mcp", "authorization_servers": ["https://id.example.com"], "scopes_supported": ["mcp:use"], "bearer_methods_supported": ["header"]}The resource value is built from oidc.issuer (falling back to app.url), never from the
request host — strict clients compare it byte-for-byte against the URL they derived the
metadata from. Paths that are not configured return 404; an empty-string key serves the
issuer root itself as the resource.
Authorization server metadata (RFC 8414)
Section titled “Authorization server metadata (RFC 8414)”GET /.well-known/oauth-authorization-server serves the same document as
/.well-known/openid-configuration (RFC 8414 permits the additional OIDC members). The
path-insertion form /.well-known/oauth-authorization-server/{path} is also routed; with realms
routed by path the realm follows the well-known segment
(/.well-known/oauth-authorization-server/realms/{realm}). When
dynamic client registration is enabled, both documents advertise the
registration_endpoint.
Dynamic client registration (RFC 7591)
Section titled “Dynamic client registration (RFC 7591)”Registration is disabled by default. Enable and scope it via oidc.clients.registration:
'registration' => [ 'enabled' => env('OIDC_DCR_ENABLED', false), 'allowed_redirect_schemes' => [], // e.g. ['claude', 'cursor', 'vscode'] 'allowed_redirect_domains' => ['*'], // exact hosts, or '*' for any],POST /oauth/register registers an authorization-code client from these RFC 7591
and OIDC metadata fields; every other field (application_type, software_id, and whatever
else MCP clients send) is ignored:
| Field | Default | Rule |
|---|---|---|
redirect_uris |
required | Absolute, no user-info or fragment; http(s) with a host in allowed_redirect_domains, or a scheme in allowed_redirect_schemes (non-HTTP schemes still need a host, so cursor://anysphere.cursor-retrieval/… passes while cursor:/callback is rejected). |
client_name |
first redirect host | Trimmed. |
token_endpoint_auth_method |
none |
none registers a public client. client_secret_basic or client_secret_post registers a confidential client; the response then carries client_secret and client_secret_expires_at: 0. Anything else is rejected. |
grant_types |
["authorization_code", "refresh_token"] |
Must include authorization_code and may only add refresh_token. |
response_types |
["code"] |
Must be exactly ["code"]. |
post_logout_redirect_uris |
[] |
Validated like redirect_uris; honoured by RP-initiated logout. |
backchannel_logout_uri |
none | Absolute https URL without a fragment — see Back-channel logout. |
backchannel_logout_session_required |
false |
Stored as given. |
PKCE is enforced by the grant for every client. The endpoint is unauthenticated (as the RFC and MCP clients expect) but throttled; keep the redirect allowlists as tight as your clients allow.
A registered client receives the realm’s scope assignment
(oidc.clients.default_scopes and oidc.clients.optional_scopes) like every other client; the
request’s scope member is ignored. The response’s scope echoes that assignment unless it
contains *: MCP clients send the value back as the authorize scope, so a client that may
request every catalog scope gets no hint and falls back to scopes_supported. A successful
registration returns 201 echoing every stored value (RFC 7591 §3.2.1):
{ "client_id": "01hf…", "client_id_issued_at": 1765465200, "client_name": "Claude", "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"], "post_logout_redirect_uris": [], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", "scope": "mcp:use"}backchannel_logout_uri and backchannel_logout_session_required appear when a back-channel
URI was registered. Validation failures return 400 with an RFC 7591 error body:
invalid_redirect_uri for a rejected redirect URI, invalid_client_metadata for everything else.
Wiring a Laravel MCP server
Section titled “Wiring a Laravel MCP server”For an app that serves its MCP endpoint with laravel/mcp and authenticates it through this provider’s guard, two pieces of app-side wiring complete the flow:
- Configure the resource:
'resources' => ['mcp' => ['scopes' => [/* … */]]]and enableoidc.clients.registration. - laravel/mcp’s
AddWwwAuthenticateHeadermiddleware only emits theresource_metadatapointer on 401 responses when a route namedmcp.oauth.protected-resource.nestedexists. Alias it onto this package’s controller in your routes file:
use Bambamboole\LaravelOidc\Server\Protocol\Http\Controllers\ProtectedResourceController;
Route::get('/.well-known/oauth-protected-resource/{path}', ProtectedResourceController::class) ->where('path', '.*') ->name('mcp.oauth.protected-resource.nested');