willdurand/oauth-server-bundle
Symfony bundle that turns your application into an OAuth 2 server. It integrates the oauth2-server-php library with Doctrine-backed storage for clients, access tokens, refresh tokens, and authorization codes, plus Symfony Security integration for protecting your APIs and resources.
What it is. willdurand/oauth-server-bundle (BazingaOAuthServerBundle) is the server half of OAuth 1.0a (RFC 5849) for Symfony 2.x. It wires willdurand/oauth-server into a firewall listener, three HTTP endpoints (request-token, authorize, access-token), and Doctrine-mappable Consumer, Token, and Nonce models. It is the spiritual ancestor of FOSOAuthServerBundle (which does OAuth2), so the mental model carries over even though the protocol does not.
Install and register. The package only resolves against Symfony 2.x and PHP 5.3/5.4-era dependencies. If you are not already on that stack, stop here — this is a legacy-application tool, not one you introduce into a Symfony 5+/PHP 8 project.
composer require willdurand/oauth-server-bundle
// app/AppKernel.php
new Bazinga\OAuthServerBundle\BazingaOAuthServerBundle(),
# app/config/config.yml
bazinga_oauth_server:
driver: orm
model:
consumer:
class: Acme\ApiBundle\Entity\Consumer
token:
class: Acme\ApiBundle\Entity\Token
nonce:
class: Acme\ApiBundle\Entity\Nonce
# app/config/routing.yml
bazinga_oauth_server:
resource: "@BazingaOAuthServerBundle/Resources/config/routing.yml"
prefix: /oauth
Where to look first, in this order:
DependencyInjection/Configuration.php — the authoritative list of config keys and their defaults (the README is incomplete here).Resources/config/*.xml — service IDs and the route definitions you will import; this tells you the exact endpoint paths.Controller/ — AuthorizeController and the token controllers, which you will almost certainly subclass or override.Entity/ / Model/ — the abstract Consumer, Token, and Nonce classes you extend.Security/ and DependencyInjection/Security/Factory/ — the firewall factory name and the listener that parses Authorization: OAuth ....First end-to-end use case. Provision a consumer (a client app with consumerKey / consumerSecret), then walk the three-legged flow:
/oauth/request_token with oauth_callback; gets oauth_token + oauth_token_secret (a pending request token)./oauth/authorize?oauth_token=...; the bundle requires IS_AUTHENTICATED_FULLY, renders a confirmation form, and on approval redirects to the callback with oauth_token and oauth_verifier./oauth/access_token with the verifier; receives a long-lived token + secret.There is no bundled "create client" console command in most versions — write a small command or a fixture that instantiates your Consumer entity and fills consumerKey/consumerSecret with hash('sha1', uniqid(mt_rand(), true))-grade randomness (or random_bytes / openssl_random_pseudo_bytes hex, if your PHP allows it).
Extend the abstract models, don't reimplement the interfaces. The bundle's base classes already provide getters/setters; you add a Doctrine mapping (annotation, YAML, or XML) plus any extra fields.
namespace Acme\ApiBundle\Entity;
use Bazinga\OAuthServerBundle\Entity\Consumer as BaseConsumer;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="oauth_consumer")
*/
class Consumer extends BaseConsumer
{
/**
* @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer")
*/
protected $id;
}
Do the same for Token and Nonce. Keep the nonce table's unique index on (nonce, consumer_id, token_id, timestamp) — replay protection depends on it and duplicate rows will otherwise slip through under concurrency.
Firewall layout. The usual shape is two firewalls: an unauthenticated one for the token endpoints and a stateless one for your API.
# app/config/security.yml
firewalls:
oauth_endpoints:
pattern: ^/oauth/(request_token|access_token)$
security: false
stateless: true
api:
pattern: ^/api
stateless: true
bazinga_oauth_server: true # verify the exact factory key in DependencyInjection/Security/Factory
access_control:
- { path: ^/oauth/authorize, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
The authorize endpoint is intentionally not stateless: it is a browser flow and needs the session to know who is logged in.
Reading the authenticated identity in a controller. When the listener validates a signature it plants a token in the security context; from there you can get the consumer and user:
$token = $this->get('security.context')->getToken(); // Symfony 2.0–2.2 style
$consumer = $token->getConsumer();
$user = $token->getUser();
On Symfony 2.3+ use security.token_storage instead of security.context.
Customizing the authorize screen. The controller's default template lives under Resources/views/Authorize/; override it by dropping your own at app/Resources/BazingaOAuthServerBundle/views/Authorize/authorize.html.twig. If you need custom logic (scopes, per-consumer consent, terms acceptance), subclass the controller and point the route/service at your class rather than editing the bundle. Keep the form fields the bundle expects (oauth_token, the confirm/cancel submit) or the flow breaks silently.
Event hooks. Check the bundle's event constants class before writing a listener — token/consumer lifecycle events are the cheapest extension point for audit logging ("consumer X issued token Y to user Z"). If the event you want does not exist, override the service definition and decorate the provider.
Testing signed requests. Do not hand-roll the signature in tests. Use a Guzzle OAuth plugin (or the client classes in willdurand/oauth-server) against a functional test kernel:
$client = new \Guzzle\Http\Client('http://localhost');
$client->addSubscriber(new \Guzzle\Plugin\Oauth\OauthPlugin([
'consumer_key' => 'ck',
'consumer_secret' => 'cs',
'token' => 'at',
'token_secret' => 'ts',
]));
$response = $client->get('/api/me')->send();
$this->assertEquals(200, $response->getStatusCode());
Maintenance jobs. Request tokens and nonces accumulate forever. Schedule a nightly cleanup: delete nonces older than the timestamp tolerance window (typically 5–15 minutes) and request tokens past expiresAt. Without this, the nonce table becomes the largest table in the database.
Client provisioning. Model the client as data, not code. Store name, description, owning user, and a revocation flag on your Consumer extension so you can disable a misbehaving integration without a deploy.
Archived and unmaintained — treat it as a security liability. Zero dependents, no releases since the Symfony 2 era, and it sits directly on an authentication path. Pin it, isolate it behind your firewall config, and have a migration plan. The only realistic migration targets are OAuth2 (FOSOAuthServerBundle, league/oauth2-server-bundle, or trikoder/oauth2-bundle), and OAuth2 is not a drop-in replacement — clients must change, and signed requests become bearer tokens. Plan for a client-by-client cutover, not a swap.
OAuth 1.0 vs 1.0a. The description cites RFC 5849, which is 1.0a — the verifier is part of the flow. If a client was written against 1.0 and skips oauth_verifier on the access-token call, you will get a signature/validation failure that is easy to misdiagnose as a key problem. Log the offending request.
Header vs body vs query string. The signature base string must include parameters from the Authorization header, a application/x-www-form-urlencoded body, and the query string. Two classic failures: (1) sending a body with Content-Type: application/json — the provider correctly ignores those params but the client still signed them; (2) putting params in the query string on a GET where the client only signed the path. Both look like "invalid signature" with no further detail.
Timestamp and clock skew. If the API server and the client are a few minutes apart, every request fails. The tolerance is configured in the underlying library, not always exposed in the bundle config — check willdurand/oauth-server's provider options and widen it if you have clients on untrusted clocks. Do not disable the timestamp check to "fix" it.
Signature method support. HMAC-SHA1 is the well-trodden path; PLAINTEXT works but leaks the secret over the wire if TLS is absent; RSA-SHA1 support in the underlying library is thin. Standardize on HMAC-SHA1 and document it for client authors.
Debugging strategy. When a signature is rejected, dump the exact base string the provider computed and the one the client computed and diff them character by character. That single artifact resolves almost every integration bug. RFC 5849's worked test vectors are the right sanity check for a hand-written client.
Stateless means stateless. The API firewall has no session, so anything that assumes $request->getSession() (flash messages, CSRF, form login) will throw. Ensure the entry point returns a clean 401 with the WWW-Authenticate: OAuth realm="..." header rather than redirecting to a login page — clients cannot follow redirects and will report a confusing error.
Entity mapping quirks. If the abstract base classes use protected $id without an annotation, your mapper must supply it. Mixing a @MappedSuperclass mapping in the bundle with your own annotation mapping causes silent column omissions — map every inherited property explicitly in your entity or use the bundle's XML mappings as the reference.
Nonce uniqueness under concurrency. Two simultaneous requests with the same nonce must not both succeed. If you are not enforcing a unique constraint at the database level, Doctrine's check-then-insert race will let one through. Add the constraint even if it means handling UniqueConstraintViolationException in the request path.
Overriding the bundle's services is supported and expected. bazinga.oauth.server (the provider) is a normal service. Decorate it if you need custom token storage, scoped permissions, or audit logging; do not fork the bundle. The absence of pull requests in years means upstream fixes are not coming — keep local patches minimal and documented.
Version pinning. Because the dependency tree is old (Twig 1.x/2.x, Doctrine 2.x, Symfony 2.x), Composer resolution is fragile. Commit your composer.lock and pin the bundle to an exact tag in any legacy app you keep alive.
How can I help you explore Laravel packages today?