majora/oauth-server-bundle
A PHP bundle that adds an OAuth 2.0 authorization server to your application, handling token issuance and validation so you can secure APIs and support standard grant flows with minimal setup.
First, resolve the stack question — it determines whether the rest of this document applies to you.
majora/oauth-server-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle subclass, a DependencyInjection/Configuration.php tree, service-container XML/YAML, Doctrine ORM entity mappings, and route imports. There is no service provider, no config/*.php, no Eloquent. If your composer.json contains laravel/framework, you cannot install this — and shouldn't try. The installable Laravel equivalents are Passport (full OAuth 2.0 server, Eloquent-native) or Sanctum (API tokens, no full OAuth). Budget half a day to confirm this before anything else; if the answer is "Laravel," skip to Implementation Patterns and read it as a mapping exercise rather than an integration guide.
If you are on a legacy Symfony 2.8 service that already depends on it, read the source in this order:
composer.json — declared PHP and Symfony constraints. This tells you immediately whether it will run on anything you currently deploy.DependencyInjection/Configuration.php — the real API surface. Every knob you can turn is here; the README (thin) is secondary.Resources/config/ — service definitions. Look for the token-storage and client-storage service IDs; these are your extension points.Entity/ — expect the canonical OAuth schema: Client, AccessToken, RefreshToken, AuthCode, and likely Scope. This is the most reusable part of the package; read it even if you never install it.Controller/ — the token and authorize endpoints. This is where grant-type dispatch happens.Minimal first run (Symfony):
AppKernel::registerBundles()./oauth).doctrine:schema:update.client_credentials token request with curl -d grant_type=client_credentials, then a second call presenting that token to a protected route. That two-step round trip exercises client storage, grant handling, token issuance, and resource-side validation — the whole spine of the package — in under an hour. If it doesn't work, you have your answer about the migration timeline.Client and scope registration. Treat clients as data you provision, not data you hand-edit. Write a console command or fixture that creates clients idempotently and stores the secret hashed. Define your scope vocabulary in one place (the bundle config) and mirror it in your resource-server middleware so the two cannot drift. The classic failure here is scopes that exist at issuance but are never enforced at the resource — the token carries read:invoices and nobody checks it.
Grant selection is a product decision, not a config toggle. For day-to-day work the practical split is:
authorization_code + refresh_token — user-facing, browser-mediated. Requires a real login form and a session at the authorize endpoint.client_credentials — machine-to-machine. No user, no refresh token, scopes fixed at client registration.Resource-side validation is a separate concern. This bundle is an authorization server; it issues tokens. Protecting your own routes needs a listener/guard that reads the bearer token, looks it up in token storage, checks expiry and scope, and populates the security token. In Symfony that's a firewall entry plus a custom authenticator. The pattern to adopt regardless of framework: one middleware, one place that decides "is this token good and is this scope sufficient," with an explicit fail-closed default.
Extensibility via service override. The bundle's DI wiring is where you swap implementations — re-point the token-storage or client-storage service alias at your own class. This is the legitimate way to adapt the package: implement the expected interface, alias the service, leave the endpoints alone. Forking the controllers is the wrong lever and makes future patches (which you'll be writing yourself) harder to apply.
Cleanup as a first-class job. Expired access tokens, expired auth codes, and revoked-but-still-present refresh tokens accumulate monotonically. Schedule a pruning command from day one. Retrofitting it after the table has 40M rows is a migration project.
If you're the Laravel developer reading this as reference, the mapping is direct and worth internalizing:
| This bundle (Symfony) | Laravel equivalent |
|---|---|
Entity/Client + admin console command |
oauth_clients table + passport:client |
Token controller (/oauth/v2/token) |
Passport's built-in /oauth/token |
| Grant-type dispatch in controller | Passport grant classes (League\OAuth2\Server\Grant\*) |
| Scope config + resource-side check | Passport scopes + tokenCan() in middleware |
| Token-storage service override | Custom AccessToken model / Passport::useAccessTokenModel() |
| Doctrine migrations for token tables | Passport's published migrations |
Read the bundle's entity schema and grant dispatch, then implement against the right-hand column. That's a few days of work with a maintained dependency instead of an unbounded fork.
Archived means you are the maintainer on day one. No CVE response, no PHP 8.x fixes, no issue triage. Before committing, establish: an internal fork with a named owner, CI that runs on your PHP version matrix, and a written security-patch expectation. If none of those are fundable, the correct decision is not to adopt.
Expect hard failures on modern PHP/Symfony. Declared support is Symfony 2.8+, i.e. PHP 5.5–7.0 era. Concrete breakages to probe in a timeboxed spike: dynamic-property deprecations on PHP 8.2, Doctrine\DBAL\Connection/EntityManager API changes in DBAL 3/4, and removed annotation routing in Symfony 5+. Run the spike on your actual target versions; a green install on 7.4 tells you nothing.
Don't let Doctrine auto-create the schema. doctrine:schema:update --force on a package with its own mappings is how you get silently divergent table definitions across environments. Generate the migration once, review it, commit it, own it. Also check for table-name collisions with anything else in the app — token tables are frequently named generically.
Redirect URI matching is strict and unforgiving. Most implementations require exact string match. Trailing slashes, http vs https, port numbers, and query-string ordering all cause "invalid grant" errors that look like credential problems. Log the received redirect URI next to the registered one when rejecting.
Clock skew produces spurious 401s that look like bugs in your code. Token exp is evaluated by the resource server, not the issuer. Enforce NTP on both, and add a small tolerance window in your validator rather than comparing timestamps exactly.
Verify PKCE before allowing any public client. If the package predates RFC 7636 (very likely), mobile and SPA clients are exposed to authorization-code interception. That's not a config fix — it's a fork-and-implement. If public clients are in scope, this alone should push you to a maintained library.
Check the crypto details you can't see from the README: token generation source (random_bytes-equivalent vs. mt_rand), whether client secrets and refresh tokens are stored hashed at rest, default TTLs, and whether refresh-token reuse triggers revocation. Any of these being absent is additive work you own.
Debugging the token endpoint: exceptions from OAuth libraries are usually translated to HTTP status + a JSON error field, and the translation layer often swallows the underlying message. When you get an opaque invalid_request, temporarily log the pre-translation exception in the token controller — that's where the actual cause (bad grant type, mismatched client, expired code) lives. On Symfony, remember cache:clear after every DI/config change; stale container state mimicks configuration bugs.
Firewall configuration is a security boundary. The token endpoint must be reachable unauthenticated (that's the point), while the authorize endpoint must require an authenticated session. Getting this backwards produces a working demo and an open redirect / credential-phishing surface. Write an explicit test that asserts the authorize route 302s to login when no session exists.
The one genuinely good use of this package: it is a compact, readable reference implementation of an OAuth 2.0 server's minimal surface — grants, clients, tokens, scopes. Read it to scope your build-vs-buy question, then build on something with a maintainer.
How can I help you explore Laravel packages today?