Securing client-side public API access with OAuth 2 and Symfony

♥ You can tap this article to like, comment, and share.

Attention developers! We have an open vacancy for a Senior Full-Stack Web Developer.

The OAuth standard is a staple in many APIs today, with Microsoft, Twitter, GitHub, Facebook, etc. all using the protocol in their implementations of authorising access to users’ data. A typical use-case is that you (the “resource owner”) may wish to use a third-party application which requires access to your Twitter timeline. You wouldn’t trust this application with your username & password, so instead OAuth provides a mechanism for the application to request authorisation from you, e.g. redirecting to Twitter for you to login and approve the request. If you agree with the permissions the application is asking for (such as write access) it’ll receive an access token back, which it can then use to authenticate against the Twitter API and post tweets to your timeline. This is one of several protocol flows available (see Mitchell Anicas’ excellent introduction to OAuth 2) and isn’t suitable for all use-cases.

Setting the scene

Say you’ll be developing a web application for a customer to create and manage restaurant bookings, exposing restaurant information (name, opening times, menu contents etc.) and booking creation as RESTful API endpoints, which are consumed by secure admin backend. You’ll need to authorise access to the API, but there is no end-user involved since the web app is its own resource owner, so the previous flow doesn’t apply. In this case the “client credentials” grant type is suitable, as the client-side app simply sends its own credentials (a client ID & secret) to the server-side API for authorisation.

However, you also need to develop a booking widget that will be embedded in a company or restaurant’s website for visitors to use. In this case, the client-side is no longer trusted enough to share the OAuth client secret that’s required to authenticate with your API. You may also want to limit its scope of access so that the widget can only fetch a subset of its own restaurant’s information, or deny access to certain endpoints entirely. To this end, we need another mechanism of requesting an access token that is scoped appropriately.

We encountered a similar use-case for a client project recently, and this blog post details the steps taken to address it. A simple companion project which provides a working implementation of the code examples given can be found on our GitHub account. It’s expected that you’ve got some prior knowledge of Symfony and its bundle ecosystem so you can find your way around.

Integrating OAuth into your Symfony project

The easiest and most robust way of adding OAuth support to your Symfony project is by using the FOSOAuthServerBundle. If your project isn’t already using it, follow the documentation to install & configure the bundle, making sure to create the required model classes (you’ll notice in the example project that the OAuth entities have omitted the user field for the sake of brevity.)

Your firewall configuration must include the oauth_token and api entries, with the /api route under access control:

Assigning a client ID to a company

In our application domain, restaurants belong to a company, so in terms of API access the company is the resource owner and therefore an OAuth client should be assigned to them. To do that we first need our company entity to declare the association.

We’ll also need some method of creating an OAuth client and persisting the association with a company. In this example we can get by with a simple Symfony console command, however in your own application you’ll probably want this to happen in a post-persist event when first creating a company (see “How to Register Event Listeners and Subscribers” on how to do this.)

Running this command with a valid company ID will then return a client ID and secret, which we will use later to test that API authentication is working.

Connecting access tokens to restaurants

Before we can start handing out access tokens, we need to solve two problems. Firstly, we can no longer rely on having the client credentials secret available to us, so we need another method of publicly identifying a restaurant for the authorisation request. Secondly, we’ll want to know which token grants access to which restaurant, so that we can properly scope API requests.

For publicly identifying a restaurant we can use the Hashids library to encode its primary key into a unique string. To do this we’ll add a new field to our Restaurant entity and create another command (again, I recommend this step happens in a post-persist event in your project after creating a restaurant.)

Running this command with a valid restaurant ID will return the hashid (note that your result will differ, depending on the ID and also the secret configured in parameters.yml)

Created hashid for restaurant: JxMZdw

The second part of the puzzle is being able to find an OAuth client from a restaurant’s hashid, useful for when we start handling authorisation requests. To do this we extend FOSOAuthServerBundle’s client manager service and add another repository method.

Finally, we’ll need to update our access token entity to be associated with a restaurant.

Handling token requests

So far we can get an access token using the standard client credentials grant flow, e.g.

However, as mentioned previously, this isn’t acceptable as the token would be unscoped and the secret would be in the hands of an untrusted client. It’d be rather neat if we could do this instead:

To achieve this, we’ll have to override FOSOAuthServerBundle’s TokenController with our own implementation. What follows this paragraph may seem like a lot of code, but the concept itself is quite simple. If our restaurant_id parameter is present in the GET/POST request, we use the repository method we created earlier to find an OAuth client and reshape the request into a standard client credentials grant flow. We then call upon the parent controller to handle this new request, and decode the response so that we can associate the access token with the restaurant before returning it to the requesting client.

Since Symfony’s service container is compiled to check for problems such as circular references or missing dependencies, we have the opportunity to get the service definition for FOSOAuthServerBundle’s token controller, which is used by other parts of the bundle. By setting it to our new class and injecting our extra dependencies, we cleanly override it with our own controller.

For this compiler pass to take effect, we must also register it.

You may have noticed we added an extra property called “scope” to the client credentials request in our token controller. The default behaviour of the FOSOAuthServerBundle is to treat OAuth scopes as Symfony security roles (see “Dealing With Scopes” for more information), which can be useful when securing API endpoints. To add support for our new “widget” scope, we’ll need to update the application config. We’ll also add a “client” scope, which we can treat as having full access by not doing anything special.

Finally, if we make an authentication request to /oauth/v2/token using our new restaurant_id parameter, we should get an access token back!

Scoping API requests

Now that our application is handing out public access tokens for our hypothetical booking widget to use, it’s important that we adhere to the correct scope in our API controller actions. Thanks to the groundwork we did earlier, we can get two crucial pieces of information: the scope of the token (which handily translates to ROLE_WIDGET in the Symfony role hierarchy), and the restaurant that the token grants access to.

Let’s use FOSRestBundle and JMSSerializerBundle to quickly develop an endpoint to demonstrate how this can be achieved. To start with, require both bundles and configure FOSRestBundle to just use JSON.

Then we’ll create an API controller with a simple action to get a restaurant, updating our routing config accordingly.

We’ve declared the API endpoint with FOSRestBundle’s @Get annotation, and so behind the scenes Symfony automatically tries to find a restaurant by a hashid equal to the value in the route’s placeholder. In the controller action itself we check if the authenticated token is scoped to “widget”, because we then want to ensure that the token can only access its own restaurant’s data.

Finally, we’ll need to annotate our restaurant entity with serialiser groups so the widget only has access to certain properties. My preferred method of doing this is to exclude all object properties by default, and then choose which ones I want to expose.

Calling the API

Now we can see the result of our hard work. Let’s first authenticate with a client credentials grant flow and call our endpoint (note that I’ve now had to add a scope parameter with the value “client”.)

Now we’ll try calling the same endpoint with an access token that has the “widget” scope, obtained using the same method our widget will use to authenticate with the API.

Can you spot the difference? When using our widget token, the restaurant’s ID is no longer returned in the response!

Wrapping up

So, what have we achieved? Looking at a slightly different response may not seem much, but we’ve in fact laid the groundwork for the widget to consume the same API as our restaurant booking management backend, but with a reduced set of permissions. By adding a layer on top of the typical client credentials grant flow to protect the client secret and setting up token scopes, we’ll be able to take advantage of Symfony’s security system and serialisation groups to cleanly segregate access to our API.

What did you think? Rate this article below.

We don't yet have enough ratings yet


Leave a comment

Simple markdown syntax supported: *italic*, **bold**, > quote, `code`, etc.

Subscribe to our newsletter

We don’t send many emails. You’ll just be notified of new blog articles (for now)

Let’s talk about how our software can work for you.

Get in touch