spomky-labs/oauth2-server-token-endpoint
Ready-to-use OAuth2 token endpoint for PHP from spomky-labs. Integrates with the OAuth2 server to process token requests, validate grant types, and issue access tokens via a PSR-7 compatible, middleware-friendly implementation for secure API authorization and token lifecycle management.
spomky-labs/oauth2-server-token-endpoint is a thin piece of the (now abandoned) Spomky-Labs OAuth2 server split: it provides the handler behind POST /token — the code that reads grant_type from a PSR-7 request, dispatches to a registered grant type, and returns a PSR-7 response. It ships no storage, no client repository, no grant types, and no Laravel service provider. Those come from spomky-labs/oauth2-server and its companion grant packages.
Before writing any code, treat the health signals as a design input: no stars, zero dependents, unmaintained. Prototype only if you have a hard constraint; otherwise evaluate Laravel Passport (framework-native) or league/oauth2-server (maintained standalone equivalent) first. If you proceed, pin exact versions and audit with composer why-not.
Minimal steps:
composer require spomky-labs/oauth2-server spomky-labs/oauth2-server-token-endpoint (plus whichever grant packages you need).Request::toPsrRequest() / Response::toPsrResponse() via nyholm/psr7):// routes/api.php
Route::post('/oauth/token', function (Illuminate\Http\Request $request) {
$psrRequest = $request->toPsrRequest();
$psrResponse = response()->toPsrResponse(); // empty PSR-7 response
$endpoint = app(\OAuth2\Server\TokenEndpoint\TokenEndpointInterface::class);
$result = $endpoint->getResponse($psrRequest, $psrResponse);
return response(
(string) $result->getBody(),
$result->getStatusCode(),
$result->getHeaders()
);
});
client_credentials grant — it is the least moving parts (no user table, no consent flow), so it validates your repository wiring, client authentication, and PSR-7 bridge in one go.Register grants explicitly, one per flow. The endpoint only dispatches; each grant type is a constructor-injected object carrying its repositories (client, access token, refresh token, user). A small service provider or config file that builds the endpoint with only the grants you actually offer is the right shape — every extra grant is attack surface (password and refresh_token especially).
$this->app->singleton(TokenEndpoint::class, function ($app) {
$server = new TokenEndpoint();
$server->addGrantType(new ClientCredentials(
$app->make(ClientRepository::class),
$app->make(AccessTokenRepository::class)
));
// add Password, RefreshToken, AuthorizationCode only if needed
return $server;
});
Keep the endpoint stateless. It reads the request, delegates, returns a response. Build it as a singleton; construct grant types once. Anything request-scoped (the authenticated client) lives in the PSR-7 request or the repositories, not on the endpoint.
Client authentication via HTTP Basic. RFC 6749 §2.3.1 expects Authorization: Basic base64(client_id:client_secret). Confirm the parent package reads that header off the PSR-7 request rather than Laravel’s auth guard — pass the raw header through the bridge unchanged.
Implement repository interfaces over Eloquent. Write small adapters (ClientRepository, AccessTokenRepository, RefreshTokenRepository) that satisfy the parent package’s interfaces and delegate to models. Store refresh tokens hashed, rotate on use, and revoke the whole chain on reuse — the package will not do this for you.
Let OAuth errors pass through. Failed grant attempts produce a PSR-7 JSON body with error, error_description, and a 400/401 status. Map that response straight to Laravel; do not let the exception handler rewrite it into a generic 500 or an HTML page.
Form-encoded, not JSON. Per RFC 6749 the token request body is application/x-www-form-urlencoded. Laravel handles that natively; JSON bodies will (probably) land as a null getParsedBody() and fail confusingly.
Test at the route level. Feature tests that POST form params to /oauth/token, assert the JSON envelope and status, and then assert DB state (token row created, refresh token rotated) catch nearly all integration bugs. Add one test per grant type plus a negative case for unsupported_grant_type.
composer update --dry-run and composer why-not before adopting.getContents() without rewinding, you get an empty string. Use (string) $response->getBody() or rewind() first when converting back to an Illuminate response.getParsedBody() can be null. That happens when nothing parsed the body — check the Content-Type and that the bridge you chose actually parses form data into the PSR-7 request.Content-Type: application/json, Cache-Control: no-store, Pragma: no-cache. Verify the endpoint sets them; if not, add them when you build the Illuminate response.unsupported_grant_type first. An unregistered or misspelled grant_type yields that error, not a 500 — good confirmation that the endpoint is reachable and dispatching. A 500 usually means a repository or grant constructor is misconfigured.league/oauth2-server mostly means replacing the route and repository adapters — another reason to prefer those from the start for anything long-lived.How can I help you explore Laravel packages today?