Yesterday marked the beta release of OAuth for the Heroku Platform API, which we hope will empower users to develop apps against the API by providing a simple and powerful authentication framework that’s consistent with other providers across the web.
One interesting discussion that developed while we were building this out was around OAuth scoping, the mechanism that allows OAuth clients to tell an authorization server what permissions they’ll need on resources they’re accessing. I thought this might be a good opportunity to talk a little about OAuth scoping, what the spec has to say about it, how it’s implemented elsewhere on the web, and our own design considerations.
RFC 6749 describes how scope should be implemented according to the proposed OAuth 2 standard. I’ve tried to summarize the main points presented in the document:
scope
.scope
in its response to inform a client of their actual scope.The spec describes the format that a scope should have and how the server should handle it, but is open-ended in respect to what strings in a scope should actually look like. This decision allows providers to define their own strings, and gives them enough flexibility to ensure that OAuth 2 scoping is a good fit for accessing a wide variety of different resources.
The open-ended spec has resulted in all kinds of creative implementations across the web, with no two being exactly alike. I’ve compiled a few examples to demonstrate the range of ideas out there.
App.net allows developers to define a basic set of scopes in snake_case. This pretty standard scoping implementation is simple and effective.
basic stream update_profile
http://developers.app.net/docs/authentication/#scopes
Facebook deviates from spec a bit by suggesting that scope strings be
comma-delimited. The two other interesting characteristics of Facebook scopes
are that more specific strings are namespaced under their broader category
(e.g. user_actions.video
), and that some strings are dynamic (e.g.
APP_NAMESPACE
scopes to a particular app in user_actions:APP_NAMESPACE
).
Facebook also offers a very extensive variety of available scopes so that apps
can be very precise about what powers they’ll require.
email,read_stream,user_actions.video,user_actions:APP_NAMESPACE
https://developers.facebook.com/docs/reference/login/#permissions
GitHub provides a concise set of scopes with some namespacing using the colon
character. For example, user:email
is a subset of the permissions allowed by
user
.
gist repo user user:email
Another interesting innovation here is that for any API requests, GitHub passes
back the response headers X-OAuth-Scopes
and X-Accepted-OAuth-Scopes
to
indicate to the user what scope strings their token has, and what strings this
endpoint will accept. This makes their APIs self-documenting in that it
provides users an easy alternative to looking up documentation when choosing
scope for their apps and tokens.
http://developer.github.com/v3/oauth/#scopes
Google mandates that scopes should start with the openid
string, then include
either or both of email
and profile
. From there, scope is extended across
Google’s flourishing ecosystem by defining other strings as extensible URIs.
openid profile email https://www.googleapis.com/auth/drive.file
https://developers.google.com/accounts/docs/OAuth2Login
Another fairly simple implementation, with the notable use of plus signs rather than spaces for delimitation.
likes+comments
http://instagram.com/developer/authentication
LinkedIn reserves the underscore to separate types of resources from the
read/write permissions to that type, with an r
specifying read privileges and
w
write.
r_basicprofile r_emailaddress rw_groups w_messages
https://developer.linkedin.com/documents/authentication#granting
An uncommon trait here is that Salesforce requires a particular scope string for the privilege of being granted a refresh token.
api refresh_token web
http://help.salesforce.com/help/doc/en/remoteaccess_oauth_scopes.htm
Shopify also mixes read and write permissions into scope strings. Their system
is fairly intuitive in that write_
also implies read_
permission, so that
developers don’t need to specify both.
read_customers write_script_tags, write_shipping
http://docs.shopify.com/api/tutorials/oauth
Defines scope strings that are prefixed with wl.
(for Windows Live);
presumably so that scopes are unique across Microsoft’s entire product space.
wl.basic wl.offline_access wl.contacts_photos
http://msdn.microsoft.com/en-us/library/live/hh243646.aspx
The end product for Heroku OAuth scope was shaped by a few major product and engineering design goals:
Taking these goals into account, along with the spec and the web’s other implementations, we came up with a starting point for our scope system which is what was released yesterday:
identity
: Allows access to GET /account
for basic user info, but nothing
else.read
: Read access to all a user’s apps and their subresources, except for
protected subresources like config vars and releases.write
: Write access to apps and unprotected subresources. Superset of
read
.read-protected
: Read including protected subresources. Superset of read
.write-protected
: Write including protected subresources. Superset of
read-protected
and write
.global
: Global access encompassing all other scope.These strings map to a much more granular set of permissions in the backend, which will allow us to continue evolving the public interface as need be.
Like a few other providers, we also elected for self-documenting API endpoints
that help developers along by specifying their accepted scope strings as
response headers (we tend to drop any X-
prefixes as they’re effectively
deprecated):
Oauth-Scope: global
Oauth-Scope-Accepted: global read read-protected write write-protected
This isn’t a finalized design, and are looking forward to collecting requirements from the community and internal consumers and iterating on it, the hope being that we can provider a system which offers a powerful amount of flexibility and granularity, while staying simple to use and true to the original spec.
Did I make a mistake? Please consider sending a pull request.