Share !

I'm Tav, a 29yr old from London. I enjoy working on large-scale social, economic and technological systems.

Follow
Contact
OAuth 3.0: The Sane and Simple Way To Do It

I'm a big fan of OAuth and I've done my fair share of promoting it — from writing various open source client libraries to implementing services using it. However, the OAuth 2.0 spec is a bit of a mess.

It's overly prescriptive and, given that there isn't a single conformant implementation by a major service provider, perhaps too complex for even the big boys.

Take Facebook. They boldly claim OAuth 2.0 support and yet ignore most of it:

  • The spec states that the oauth_token parameter be used when accessing a protected resource. Facebook use the access_token parameter name instead.
  • The spec states that certain requests MUST include the response_type and grant_type parameters. Facebook enforces no such requirement.
  • The spec states that clients MUST be able to authenticate themselves using both HTTP basic auth and URI query parameters. Facebook only support the latter.

I could go on, but I'm sure you get the picture. The irony here is that David Recordon from Facebook is actually one of the co-authors of the spec. And before anyone starts bashing Facebook for ignoring standards, please read the latest draft of the spec — it is shockingly convoluted for such a simple thing like OAuth.

As it is, what we're seeing is the emergence of incompatible and incomplete implementations by service providers. And when even the whiz kids at GitHub have issues implementing OAuth 2.0, it's time to re-evaluate the specification.

Furthermore, while the spec goes on about rarely used SAML assertions, it fails to standardise service discovery, client registration, management, etc. Sites like Disqus and Ning make good use of APIs like Facebook's Create Application API — it'd be nice if these were standardised!

So, in the interest of simplifying life for developers everywhere, I hereby present OAuth 3.0 — a more comprehensive alternative to the latest OAuth 2.0 draft. It is so simple that even an idiot like me could do a complete client/server implementation in about 300 lines of Python code.

Anyways, let me know what you think — especially if you've already experienced the pain of implementing OAuth. Thanks!

OAuth 3.0

OAuth allows Applications to perform authorized actions on behalf of a User on remote Service Providers. The core process is quite straightforward:

  1. Applications register with the Service Provider and are issued with a client_id and client_secret.
  2. Applications use one of three different mechanisms to get hold of an authorized Access Token for a User.
  3. Applications use the Access Token to perform actions on behalf of the User.

Service Discovery

Service Providers announce their support for OAuth by publishing a /.well-known/oauth.json file on their website. This file lives inside the .well-known directory introduced by [RFC 5796] and is encoded in the JSON format [RFC 4627]. So for a Service Provider supporting OAuth 3.0:

$ curl https://www.serviceprovider.com/.well-known/oauth.json

We'd find something like:

{
"auth_endpoint": "http://www.serviceprovider.com/oauth/authorize",
"auth_management_endpoint": "https://www.serviceprovider.com/oauth/apps",
"client_management_endpoint": "https://www.serviceprovider.com/oauth/clients",
"client_registration_challenge": "sha-1:20:serviceprovider.com",
"client_registration_endpoint": "https://www.serviceprovider.com/oauth/register",
"secure_access": true,
"terms_of_use": "http://www.serviceprovider.com/terms",
"token_endpoint": "https://www.serviceprovider.com/oauth/token",
"version": "3.0.0"
}

The JSON object could include other properties, but the following string properties MUST be present:

  • The auth_endpoint defines the HTTP or HTTPS URI [RFC 3986] which Apps will direct a User to — in order for the User to authorize access.

  • The auth_management_endpoint defines the HTTPS URI where Users will be able to manage the various Apps that they have authorized.

  • The client_management_endpoint defines the HTTPS URI where Users will be able to manage the various App Clients that they have registered.

  • The client_registration_challenge defines a hashcash challenge spec for Apps to use when registering. This MUST be of the format <hash-algorithm>:<hashcash-bits>:<provider-id> where the provider-id is some form of unique identifier for the Service Provider — like its domain name.

  • The client_registration_endpoint defines the HTTPS URI where Apps will be able to register and be issued with client_id and client_secret codes.

  • The terms_of_use defines the HTTP or HTTPS URI for the Service Provider's terms of use and policies for Apps.

  • The token_endpoint defines the HTTPS URI that Apps will communicate with — in order to get or refresh an authorized Access Token.

  • The version defines the Semantic Version of the OAuth Protocol that the Service Provider is currently supporting.

    current_version: "3.0.0draft-01"

The JSON object MAY also include the following properties:

  • The presence of a secure_access boolean property with the value of true indicates that the Service Provider will only accept Access Tokens over HTTPS and that it wants Applications to never send them using plain HTTP.

Core API Overview

Besides the auth_endpoint which accepts URI-encoded parameters via GET requests and returns responses directly to the User, there are four OAuth-specific “Core” APIs exposed at the following Service Provider endpoints:

  • auth_management_endpoint
  • client_management_endpoint
  • client_registration_endpoint
  • token_endpoint

They accept URI-encoded parameters via HTTP GET or POST with the application/x-www-form-urlencoded content type and all successful calls return a JSON encoded response with a 200 OK HTTP status code and application/json content type.

Unsuccessful calls will have either 4xx or 5xx HTTP status codes and these may or may not have JSON-encoded error responses — this can be determined by looking for application/json in the Content-Type response header.

The structure of these JSON-encoded error responses are not standardized beyond the required presence of an error property. This property can be of arbitrary type and can be accompanied by other properties, e.g.

{
"error": "The client_secret parameter was not specified.",
"error_type": "ValueError"
}

Or even something like:

{
"error": {
"type": "InvalidClientCredentials",
"message": "Matching client credentials could not be found."
}
}

It is left up to the Service Providers to be as helpful or unhelpful as they desire to be with their error responses.

And, finally, Service Providers MAY also expose “web pages” at some of the endpoints:

  • auth_management_endpoint
  • client_management_endpoint
  • client_registration_endpoint

For example, a Service Provider may allow authenticated Users to manually manage the various apps that they have authorized by visiting the auth_management_endpoint with their web browser. If a Service Provider chooses to do this, then they MUST use the Accept header to distinguish between a direct request by a User and an API call by an Application.

The lack of an Accept header or one with just application/json should be used to identify a request from an Application. As such, Applications MUST only send an Accept: application/json header — if they send one at all.

Client Registration

Apps register with the Service Provider via the client_registration_endpoint in order to be issued with client_id and client_secret codes. Unlike previous versions of OAuth, this can now be done programmatically by POSTing to the endpoint with the following parameters:

  • name — the name of the Application encoded as a UTF-8 string [RFC 3629] with a maximum length of 100 bytes. REQUIRED.
  • website — the website URI for the Application with a maximum length of 200 bytes. OPTIONAL.
  • description — a brief description of the Application encoded as a UTF-8 string with a maximum length of 500 bytes. OPTIONAL.
  • organization — the name of the Organization behind the Application encoded as a UTF-8 string with a maximum length of 100 bytes. OPTIONAL.
  • accept_terms — this MUST be set to yes and indicates acceptance of the Service Provider's terms_of_use. REQUIRED.
  • hashcash — a valid hashcash payment for registering with the Service Provider with a maximum length of 100 bytes. See below for more info. REQUIRED.
  • redirect_uri_prefix — the expected prefix of the “callback” HTTP or HTTPS URI to redirect to after a User has authorized an OAuth request. Maximum length of 200 bytes. OPTIONAL.
    • If the App will only use the Standard Flow OAuth Request, then it is OPTIONAL and will only be checked if set and can be either an HTTP or HTTPS URI.
    • Otherwise, this parameter is REQUIRED and MUST be an HTTPS URI.

A successful registration will result in a JSON response with the following string properties:

  • client_id
  • client_secret

So, for example, if one did:

$ curl -d 'name=Awesome%20App' -d 'accept_terms=yes' \
-d 'hashcash=1:20:100629:serviceprovider.com::e302ac179846:18b51f' \
https://www.serviceprovider.com/oauth/register

Then a successful response would look something like:

{
"client_id": "c6cbeb7f4ede64c1615ff2d27307030f9612",
"client_secret": "4d1e7bd053385838838d5c2dea60b531a2c7"
}

The above example demonstrated an “anonymous registration” which will not be associated with any specific User. Applications wanting to associate a registration with a specific User MUST provide an Access Token authorized for the :client_registration scope, e.g.

$ curl -d 'name=Awesome%20App' -d 'accept_terms=yes' \
-d 'hashcash=1:20:100629:serviceprovider.com::e302ac179846:18b51f' \
-H 'Access-Token: abcd' \
https://www.serviceprovider.com/oauth/register

The returned client_id and client_secret values from a successful registration MUST be less than 100 bytes long and be composed of characters in the range abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789. This is so that they can be used as primary keys in a wide range of datastores.

If a random number generator is used to generate the values, bear in mind that 18 random bytes (which is 36 bytes long in hex-encoding) should be more than enough — and 36 random bytes gives you more permutations than current estimations of the number of atoms in the universe.

The hashcash payment value is used to mitigate against any resource exhaustion attacks against Service Providers — who have to provide long-term storage of the registered values. The use of hashcash forces an Application to use up some CPU cycles as a proof of work.

The hashcash is generated according to the Service Provider's client_registration_challenge property which is of the format:

  • <hash-algorithm>:<hashcash-bits>:<provider-id>

The hash-algorithm defines the algorithm to be used (normally SHA-1 [RFC 3174]), the provider-id defines a unique identifier for the Service Provider (normally its domain name) and the hashcash-bits specifies the number of leading bits of the hashcash digest which must be zero for it to be valid.

Applications generate hashcash in a manner similar to:

algorithm, bits, provider_id = challenge.split(':')
if algorithm == 'sha-1':
hash = sha1
elif algorithm == 'sha-256':
hash = sha256
else:
raise ValueError("Unknown hash algorithm: %s" % algorithm)
bits = int(bits)
hashcash = generate_hashcash(provider_id, bits, hash)

Where generate_hashcash() creates an appropriate version 1 format hashcash stamp:

from binascii import hexlify
from datetime import datetime
from hashlib import sha1
from math import ceil
from os import urandom
from time import time

def generate_hashcash(resource, bits=20, hash=sha1):
date = datetime.utcnow().strftime("%y%m%d")
rand = hexlify(urandom(6)) # a 48-bit random string
prefix = "1:%s:%s:%s::%s:" % (bits, date, resource, rand)
counter = 0
n = int(ceil(bits/4.)) # rounds up to the closest 4-bits
lead_zeros = '0' * n
while 1:
digest = hash("%s%x" % (prefix, counter)).hexdigest()
if digest[:n] == lead_zeros:
return "%s%x" % (prefix, counter)
counter += 1

Service Providers verify that the hashcash is valid and hasn't been used before with something like:

def valid_hashcash(hashcash, resource, bits=20, hash=sha1):
n = int(ceil(bits/4.))
digest = hash(hashcash).hexdigest()
if digest[:n] != ('0' * n):
return False
_, _bits, date, _resource, _, _, _ = hashcash.split(':')
if resource != _resource:
return False
timestamp = int(datetime(
int(date[:2]) + 2000, # map a 2-digit YY into a YYYY
int(date[2:4]),
int(date[4:6])
).strftime('%s'))
now = time()
time_difference = now - timestamp
# allow for clock skew of up to 3 days
if not (-259200 <= time_difference <= 259200):
return False
# check if the hashcash has been used before
stored_hashcash = HashCash.get_by_key_name(hashcash)
if stored_hashcash:
raise ValueError("The given hashcash has already been used.")
db.put(HashCash(key_name=hashcash, created=timestamp))
return True

So, from the previous example, we'd get:

>>> valid_hashcash(
... '1:20:100629:serviceprovider.com::e302ac179846:18b51f',
... 'serviceprovider.com'
... )
True

The recommended data structure for storing such valid hashcash payments for checking against double spending is something like:

# primary key: hashcash
class HashCash(db.Model):
created = db.IntegerProperty() # UNIX timestamp

Service Providers can reclaim used space by removing all entries older than 3 days. And, similarly, the recommended data structure for storing registered clients is something like:

# primary key: client_id
class OAuthClient(db.Model):
created = db.IntegerProperty()
name = db.StringProperty()
description = db.TextProperty()
organization = db.TextProperty()
website = db.ByteStringProperty()
redirect = db.ByteStringProperty()
user = db.StringProperty(default='')
secret = db.ByteStringProperty()
last_used = db.IntegerProperty()

The associated user field should be set to a distinct value, e.g. null, for “anonymous registrations” or to some representation of the User if there was a valid Access Token used as part of the registration.

Anonymous registrations are generally expected to be short-lived and Service Providers may remove them after a minimum of 30 days of inactivity. The presence of the last_used field is intended to be used to help with such clean up.

And finally, for the love of the Gods of SGML, Service Providers, please escape angle brackets and related characters when displaying the submitted data to end users, e.g.

def escape(s)
return s.replace("&", "&amp;").replace("<", "&lt;").replace(
">", "&gt;").replace('"', "&quot;")

Don't be like Twitter.

Authorization Request

Following

def authorize(
client_id, redirect_uri, scope, state=None, request_type='application'
) -> HTTPRedirect

Scopes beginning with : are referred to as “system scopes” and are reserved for use by the Core OAuth-related APIs. Service Providers MUST therefore not define such colon-prefixed scopes for their own use. The following system scopes have been defined so far:

  • :auth_management
  • :client_management
  • :client_registration

The scope :* can be used as a synonym for all such system scopes.

User Agent

And:

  • display — either default or compact
var randomString = (Math.random() * 0x100000000).toString(16);
document.cookie = 'expectedState=' + randomString;

Client Management

Apps can manage a User's client registrations by sending requests to the client_management_endpoint with an Access Token authorized for the :client_management scope.

A GET request returns a listing of the current client registrations for the User — batched into “pages” of anything up to 500 items each. To indicate that there are more results than can be fit into the current “page”, the Service Provider would include a next property in its JSON response.

An Application would then need to pass in the provided value as the optional next parameter to a future GET request to retrieve the next “page”, e.g.

$ curl -H 'Access-Token: abcd' \
https://www.serviceprovider.com/oauth/clients?next=1234

Of course, there's no next parameter for the very first “page”.

In addition to the optional next string property, the returned JSON object MUST contain a clients property which will be an array of objects representing the registered clients. So, doing:

$ curl -H 'Access-Token: abcd' \
https://www.serviceprovider.com/oauth/clients

Will return something like:

{
"clients": [
{
"client_id": "c6cbeb7f4ede64c1615ff2d27307030f9612",
"client_secret": "4d1e7bd053385838838d5c2dea60b531a2c7",
"name": "Awesome App",
"website": "",
"description": "",
"organization": "",
"redirect_uri_prefix": "",
}
],
"next": "1234"
}

Each individual client data object will have properties for client_id, client_secret, name, website, description, organization and redirect_uri_prefix — with empty values being represented by an empty string instead of null.

Apps can update the registered values for any of the clients by POSTing to the client_management_endpoint with the following parameters:

  • client_id — this needs to match the specific client registration that the App wants to update. REQUIRED.
  • name — this parameter has the same semantics as in the registration process. REQUIRED.
  • website — this parameter has the same semantics as in the registration process. OPTIONAL.
  • description — this parameter has the same semantics as in the registration process. OPTIONAL.
  • organization — this parameter has the same semantics as in the registration process. OPTIONAL.
  • redirect_uri_prefix — this parameter has the same semantics as in the registration process. OPTIONAL.

On a successful update, the Service Provider will return a JSON object with a single action property with the value of updated, e.g.

$ curl -H 'Access-Token: abcd' \
-d 'name=Awesome%20App' \
-d 'description=This%20lets%20you%20do%20awesome%20things' \
-d 'client_id=c6cbeb7f4ede64c1615ff2d27307030f9612' \
https://www.serviceprovider.com/oauth/clients

Would return:

{ "action": "updated" }

Any optional parameters not sent in the update call will result in them being over-written with empty values. For example, if the registration in the example above had previously had the website property set to http://www.aweseomeapp.com, then it's website property will be empty after the update.

Apps can delete a registered client by POSTing to the client_management_endpoint with the following parameters:

  • action — this needs to be set to delete. REQUIRED.
  • client_id — this needs to match the specific client registration that the App wants to delete. REQUIRED.

On a successful delete, the Service Provider will return a JSON object with a single action property with the value of deleted, e.g.

$ curl -H 'Access-Token: abcd' \
-d 'action=delete' \
-d 'client_id=c6cbeb7f4ede64c1615ff2d27307030f9612' \
https://www.serviceprovider.com/oauth/clients

Would return:

{ "action": "deleted" }

Authorization Management

Apps can manage a User's authorizations by sending requests to the auth_management_endpoint with an Access Token authorized for the :auth_management scope.

A GET request returns a listing of the current authorizations by the User — batched into “pages” of anything up to 500 items each. The batching works in the exact same way as for Client Management and the GET request takes an optional next parameter to support this.

In addition to the optional next string property, the returned JSON object will contain an auth property which will be an array of objects representing the authorizations by the User. So, doing:

$ curl -H 'Access-Token: abcd' \
https://www.serviceprovider.com/oauth/apps

Will return something like:

{
"auth": [
{
"auth_id": "xyz123",
"client_id": "c6cbeb7f4ede64c1615ff2d27307030f9612",
"app_name": "Awesome App",
"app_website": "http://www.awesomeapp.com",
"scope": "photos messages",
"created": 1270000000,
"expiry": 1272592000,
"renewal": 1271742400
}
],
"next": "6789"
}

Each individual item about an authorization will have properties for:

  • auth_id — an identifier representing the authorization and the underlying Access Token.
  • client_id — the client_id of the Application which requested the authorization in the first place.
  • app_name — the name of the Application which requested the authorization.
  • app_website — the website URI of the Application which requested the authorization.
  • scope — a space separated list of scopes that the authorization is valid for.
  • created — the UNIX timestamp when the authorization took place, represented as an integer.
  • expiry — the UNIX timestamp when the authorization will naturally expire, represented as an integer.
  • renewal — the UNIX timestamp when the authorization will next need to be renewed, represented as an integer.

Applications can revoke an authorization on the User's behalf by POSTing to the auth_management_endpoint with the following parameters:

  • action — this needs to be set to revoke. REQUIRED.
  • auth_id — this needs to match the specific authorization that the App wants to revoke. REQUIRED.

On a successful revoke, the Service Provider will return a JSON object with a single action property with the value of revoked, e.g.

$ curl -H 'Access-Token: abcd' \
-d 'action=revoke' \
-d 'auth_id=xyz123' \
https://www.serviceprovider.com/oauth/apps

Would return:

{ "action": "revoked" }

As soon as an authorization has been revoked, the Service Provider MUST invalidate the underlying Access Token.

Using Access Tokens

Once an Access Token has been obtained, the Application should store it securely and use it to perform actions on a User's behalf. It is intended to be compatible with whatever APIs the Service Provider offers and it is HIGHLY RECOMMENDED that it only be sent to HTTPS URIs.

The last point is worth re-iterating — the access_token MUST only be sent across HTTPS channels. Service Provides MUST NOT process Access Tokens sent in the clear. Processing power is now cheap enough to warrant such a requirement in the interest of security.

For requests supporting OAuth, Service Providers MUST accept:

  • a URI-encoded access_token parameter as part of either the request URI or as part of the request body for POST requests. If both are present, then the value sent in the request URI should be discarded.
  • a Access-Token request header with the Access Token as the value (without any URI encoding).

If both are present, then the URI-encoded parameter should be discarded.

Here's an example GET request:

$ curl "https://www.serviceprovider.com/news?type=feed&access_token=abcd"

And another:

$ curl -G -d 'access_token=abcd' \
https://www.serviceprovider.com/messages

And yet another:

$ curl -H 'Access-Token: abcd' \
https://www.serviceprovider.com/messages

Here's an example POST request with the Content-Type set to application/x-www-form-urlencoded:

$ curl -d 'text=Hello%20world' -d 'access_token=abcd' \
https://www.serviceprovider.com/message

Here's an example POST request with the Content-Type set to multipart/form-data:

$ curl -F 'file=@profile.jpg' -F 'access_token=abcd' \
https://www.serviceprovider.com/profile-image

And another:

$ curl -F 'file=@profile.jpg' -H 'Access-Token: abcd' \
https://www.serviceprovider.com/profile-image

For requests needing authorization, Service Providers should look for and validate the Access Token for the appropriate scope before returning a successful response. If the token needs to be renewed, then the Service Provider MUST respond with a 412 Precondition Failed HTTP status code.

If the Access Token is invalid for any other reason — e.g. it has expired, is authorized for the required scope, etc. — then the Service Provider MUST respond with a 401 Not Authorized HTTP status code.

The Content-Type header of these responses can be whatever and need not contain a JSON object with a descriptive error property — although that would be helpful.

Object Capability

If a Service Provider chooses to follow the highly recommended object capability security model, then Access Tokens can be considered equivalent to capability references.

Thanks

  • Mamading Ceesay — reviewed the pre-published drafts.
  • Chris Fuenty — defined the policy for using Access Tokens over HTTPS.
  • Justin Hart — defined the delegation of Access Tokens across Applications.
  • James Arthur
  • Everyone who has ever worked on OAuth.

Well-behaving applications should not store the user's credentials for future use.