Simon Welsh is a developer at PocketRent, a SaaS using SilverStripe. He is currently working towards completing his Honours degree in Computer Science and Maths, and was recently a finalist in the NZOSS awards for his development efforts within the SilverStripe community. Simon has written for the SilverStripe blog before, documenting his journey of upgrading to 3.0. Follow Simon's alias simon_w on both Twitter and the SilverStripe IRC channel.
The web has moved away from disconnected sites that don't know anything about what's going on in the rest of the world. Now, almost every site that is anything beyond your bog-standard brochure site has connections to at least one other site, though usually substantially more. This can range from simple things, like using Facebook and Twitter for log ins, to more complicated integrations, such as taking payments using Stripe, recording the payment in Xero then uploading a file to SS3. These sorts of sites are said to "consume" others.
Every so often, you want to produce a site that provides information that others can consume. Say your site is selling different types of fluffy pink slippers and you'd like other sites to be able to manage wishlists on behalf of your customers.
The obvious way to do this is to build an API for your site and let other sites consume it. To make the API, you can use a pre-rolled solution like the RestfulServer module, or you can roll your own controllers. No matter which method you choose, you also have to decide on how you are going to authenticate requests to your API.
The simplest way, which is what the RestfulServer uses by default to authenticate requests, is to use basic auth. This involves sending a username and password along with every request. This approach is very easy to implement, in fact the PHP manual has a page explaining how to do it. This approach requires that the consuming service stores the user's password and any change to the user's password means the new password has to be re-entered in every third-party service. The only way for users to revoke access to a service is to change their password.
To deal with some of these problems, OAuth was developed. OAuth uses tokens to authenticate requests. These tokens are issued by the provider (your site) after a user approves the client that is requesting a token. This way, the user only ever provides their password to your site, can change their password and not have to update everything and can revoke an individual app's access by revoking the particular token. OAuth also makes it easier to associate specific information with each authentication token, such as a set of permissions that a particular user has granted a particular application.
OAuth 1.0 requires signing of all your requests in a very specific way that was difficult to get right. This is what Twitter uses. OAuth 2.0 with Bearer (or similar) only uses a unique token. Facebook uses a Bearer-like token system.
Introducing the OAuth server module
When beginning on the design of the PocketRent API, we decided to go with OAuth for authentication, specifically OAuth 2 using Bearer tokens. This was mostly because we decided that the tradeoffs of basic auth outweighed its simplicity. We picked version 2 of version 1, as OAuth 2 just seems more sane. The difficulties of signing and checking signatures in an OAuth 1 setup weren't that important in the decision making, as there are libraries for almost any language that do all the heavy lifting for you.
Our OAuth module was designed to be applied to a controller and handle all the authentication for you. This means you just need to install the module, set up scopes and applications in the CMS, give out the generated access tokens for those applications and then the module does the rest.
This module requires version 3.1 or later of the SilverStripe Framework. Due to the way PHP handles the Authorization header, support for authenticating requests using headers requires that the getallheaders method exists in your SAPI. As of PHP 5.4, this is included with the Apache and FastCGI SAPIs.
The OAuth module comes with support for using Bearer for authentication. If you would like to use some other authentication method, you can create a class that implements the `oauth\service\Service` interface and then set the `Injector`'s `class` value for `oauth\service\Service` after the `authtokenservice` config fragment. For example, if you wanted to use the `MyAwesomeOAuthService` as the class that handles the authentication, you'd use:
The included Bearer authentication supports different length tokens (defaults to 25 characters), token expiration (defaults to no expiration) and enabling/disabling support for authentication using POST (enabled by default) and GET (disabled by default) parameters as well as headers. These can be set by setting the `token_length` (number of characters), `token_life` (number of seconds), `allow_form_body` (boolean) and `allow_url_param` (boolean) config values on `oauth\service\Bearer` respectively. To disable token expiration, set `token_life` to a negative value. For example, to set tokens to be 30 characters, expire after an hour and to disallow both POST and GET parameters for authentication, you'd use:
Setting up the module
Once you have configured the module, the next step is to set up scopes. This is done through the CMS here. Here, you create new and edit existing scopes.
With each scope you can set:
- A name, which must not contain spaces and is what clients use to request that particular scope.
- A description, which is displayed to the user when being prompted to authorise the client.
- If it is a default scope, that is one that is automatically requested if a client doesn't request any scopes
- If it can't be disallowed, that is a user cannot refuse to give this scope to a client if it asks for it.
The next step is to set up clients. This is also done in the CMS here. Here, you can create and edit clients.
With each client you can set:
- A name, description, logo and website that can be displayed to the user whenever the client gets displayed.
- A default endpoint that the user is redirected to in case the client does not supply a redirect URI with its request.
- A list of valid return URIs. At the most lenient, each of these must be a domain name; at the most strict, a full URI.
After saving for the first time a client identifier is automatically generated. The client use this identifier and the return URIs when requesting access to a user.
Once you've set up the scopes and clients, the only step left before your OAuth server is ready for use is to provide some endpoints for clients to actually call.
Setting up your controllers
Everything you need to require that either an entire Controller or just certain actions use OAuth is all done using the `RequireOAuth` extension.
When adding the extension to your controller, you have three options depending on what you pass as arguments to the extension. The default option is to require OAuth for all requests to the controller without requiring any specific scope. This is achieved by simply adding the extension without any arguments:
public static $extensions = array(
If you would like to allow general access to the controller, but still require OAuth for certain actions, pass it `false` as a single argument:
public static $extensions = array(
The third option is to only allow OAuth-authenticated requests with certain scopes. This is done by passing the required scopes as strings to the extension. For example, if you wanted to require the `read` and `write` scopes:
public static $extensions = array(
Using `$allowed_actions` it is possible to further restrict specific actions to requiring specific scopes using the `requireScopes` method made available through the extension and passing in the required scopes as strings. For example, if you wanted the method `delete` to require the `destroyer` scope, you'd have:
public static $allowed_actions = array(
'delete' => '->requireScopes("destroyer")',
The final check the extension allows you to perform is a conditional check for a scope. This allows you to provide additional functionality inside actions if a scope is present. This is done by passing the scopes to check for to `hasScopes`. I.e.:
// you can read!
The only thing left to do now is let clients know how they talk to you. There are three parts to this. In the first, the client asks for permission to a user. They do this by sending the user to `https://yoursite.com/oauth/authorise?response_type=code&client_id=<identifier>&scope=<scope1>+<scope2>&redirect_uri=<uri>&state=<state>`. In this URL, `<identifier>` is the client identifier displayed in the CMS; `<scope1>` and `<scope2>` are scopes that the client would like; `<uri>` is the URI to return the user to once they either accept or reject the permission request; and `<state>` is an optional state parameter. These parameters can also be sent as POST parameters.
The user is then asked if they want to give the client access to their account. If they don't give permission, then they are redirected to `<redirect_uri>?error=access_denied&state=<state>` to notify the client. Otherwise, the user grants permission and is redirected to `<redirect_uri>?code=<code>&state=<state>&scope=<scope1>+<scope2>` where `<code` is an authentication code; `<state>` is the state value passed in at the start of the request; and `<scope1>` and `<scope2>` are scopes that the client has been granted permission for.
Now that the client has the authentication code the second step is to request an access token. This is done by sending a POST request to `https://yoursite.com/oauth/token` with the parameters `grant_type` with the value `authorization_code`, `code` with the authentication code received in the previous step and `redirect_uri` with the redirect uri provided in the previous step. Assuming everything goes well, a JSON object is returned to the client containing `access_token`, `scope` and `token_type` which contain the access token, the scopes the token has access to and the type of token (in a standard install, this will be Bearer) respectively.
The third and final step is for the client to make authorised requests. This is done by making a request to one of the endpoints you set up early with the "Authorization: Bearer <access_token>" header.
You're now set up with an API that utilises OAuth for authentication that is ready for consumption by the world at large! As always, I'm available to answer further questions and can usually be found in the IRC Channel.