spomky-labs/oauth2-server-public-client
Adds public client support to the spomky-labs OAuth2 server, enabling clients that cannot keep a secret to authenticate safely using authorization code flows with PKCE. Integrates with the existing server grant and client handling.
Pull the package in alongside the core server it extends — it is a companion library, not a standalone OAuth2 server:
composer require spomky-labs/oauth2-server spomky-labs/oauth2-server-public-client
Then look at three places, in this order:
getClientSecret() returning null / a isPublic() style flag) rather than a confidential client with a redacted secret. Read this first; it defines what the rest of the package can do.ClientManagerInterface::createClient(...)-style factory; the public-client variant is the one whose signature drops the secret argument.client_id + client_secret (HTTP Basic or body params). The public-client package exists so that step can be satisfied by client_id alone.First use case: a mobile app or SPA that cannot keep a secret. Register it as a public client with an exact redirect URI, let it run the authorization-code grant without a secret, and require PKCE on the authorization request. Concretely: create the client record, hit /authorize with code_challenge, exchange the code at /token with client_id + code_verifier and no client_secret.
If you only need one public client and you control the server, sanity-check whether the core spomky-labs/oauth2-server already tolerates a null secret — this package is thin, and you may be adding a dependency for a few lines of behaviour.
Register the client at provisioning time, not at request time. Build public clients through the manager during seeding / admin action, storing client_id, redirect URIs, allowed grant types, and allowed scopes. Keep the client_secret column nullable in your schema and assert at the domain layer that a public client never gets one written.
Separate the two client populations. Most deployments have confidential clients (server-to-server, web apps with a backend) and public clients (native, SPA). Keep distinct creation paths and distinct validation rules; a single createClient() with a nullable secret invites accidental secret-less confidential clients.
Bind PKCE unconditionally for public clients. A public client is unauthenticated, so the authorization code is the only thing standing between an attacker and a token. Enforce code_challenge/code_challenge_method=S256 at the authorization endpoint whenever the resolved client is public — do not leave it to the client to opt in. Verify code_verifier at the token endpoint and treat a missing verifier as invalid_grant.
Exact-match redirect URIs. Public clients are the case where wildcard or prefix matching is fatal. Store the full URI including path and query, compare byte-for-byte, and reject anything else with invalid_request before issuing a code.
Refresh tokens: rotate or don't issue. For public clients, either issue no refresh tokens, or rotate on every use and revoke the whole family on reuse detection. A stolen refresh token from a device is otherwise a permanent grant.
Decorator pattern for the client manager. If you need tenancy, rate limits, or audit, wrap the package's manager rather than editing it:
final class AuditingClientManager implements ClientManagerInterface
{
public function __construct(
private ClientManagerInterface $inner,
private LoggerInterface $logger,
) {}
public function createClient(/* ... */): ClientInterface
{
$client = $this->inner->createClient(/* ... */);
$this->logger->info('client.created', ['id' => $client->getPublicId()]);
return $client;
}
}
Test the negative path in your own suite. A short integration test that posts to /token with a client_secret for a known public client (should be ignored or rejected consistently), one with no code_verifier (should fail), and one with a mutated redirect URI (should fail) buys more confidence than the package's own tests.
Integration tip: if you use a framework bundle (Symfony), wire the public-client manager as a distinct service alias and inject it only into the provisioning command — not into the token endpoint, which should treat all clients through the common interface.
spomky-labs/oauth2-server lineage, which was superseded by web-token/jwt-framework and, for many teams, by league/oauth2-server. Pin exact versions, read the source (it's small), and budget for a fork or a migration. Do not build a long-lived product on it without a vendor-out plan.none. If you advertise token_endpoint_auth_method in metadata, public clients must declare none; a mismatch causes invalid_client at the token endpoint and is a common first-hour bug.client_id → invalid_client; a client asking for a grant type it isn't allowed → unauthorized_client; missing verifier → invalid_grant. Endpoint consumers branch on these, so get them right rather than returning a generic 400.code, code_verifier, client_secret, and refresh_token in your PSR-3 processor.software_statement), implement the core ClientInterface yourself and let the package's manager produce your class. The interfaces are the stable contract; the concrete classes are not.spomky-labs/oauth2-server without checking the public-client constraint is the most likely way to break a working install.How can I help you explore Laravel packages today?