Skip to content

Instantly share code, notes, and snippets.

@dustinsoftware
Last active April 3, 2018 16:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dustinsoftware/6b36e517f4f62ac867a1955ebf19aacb to your computer and use it in GitHub Desktop.
Save dustinsoftware/6b36e517f4f62ac867a1955ebf19aacb to your computer and use it in GitHub Desktop.

OAuth 2 Bearer Tokens

Current design:

  • Based on OAuth 1.0a with a central authority
  • Access token / secret generated by central authority, used in combination with consumer token / secret
  • Client creates OAuth header from consumer token/secret and access token/secret. HMAC-SHA1 or PLAINTEXT signatures used
  • Each web service makes a call to central authority to get current user information based on OAuth header

Problem A - Increased latency:

  • Service A needs to verify OAuth header
  • Service A needs to call service B, passes OAuth header
  • Service B needs to verify OAuth header
  • Service A makes another call to service B
  • Because of round robin, service B is running on many different nodes, and may need to validate OAuth header again

Problem B - Lots of load on auth Web API:

  • Many requests are redundant. Service A, B, and downstream requests often validate the same auth header multiple times
  • Beefy hardware required to sustain load from every Web API

Problem C - Generated tokens do not expire in most cases

  • Unless a user signs out, a generated header has no expiration. Use of HMAC-SHA1 mostly curbs this problem for clients, but it's still possible a plaintext header could be captured

Design goals of token-based approach:

  • Uses JWTs signed with ES256. The structure of authorization claims follow JWT claims described in RFC 7519.
  • Web APIs should need only a public key to validate auth headers (no shared secret or private key)
  • Stateless server scalability. Web APIs should not call out to an external service to validate a token in most cases
  • Public key used to sign tokens can be rotated quickly in the case of a compromise
  • Generated auth tokens should be short lived (~10 minutes). These tokens should not be persisted beyond the scope of a single request, as they will be allowed to access any Faithlife API
  • Authorization logic falls back to OAuth API if a token can't be validated

Token structure:

{
	"sub": "2986689", // user id
	"aud": "faithlife-apis", // required by OpenID Spec
	"nbf": 1522361458,
	"exp": 1522362118,
 	"iat": 1522362058,
 	"iss": "auth.faithlife.com",
 	"alias": "Dustin Masters",
	"consumerName": "JWT demo",
	"consumerToken": "XXXXXX", // downstream apps sometimes alter behavior based on whitelisted consumer tokens
	"isAdminConsumer": "true",
	"impersonatorId": "3" // impersonator id, optional
}
  • aud is required by the OpenID spec. We can change the audience later if we want to limit a token to just one web endpoint. The current OAuth 1.0a implementation allows an auth header to be used on any backend API.
  • Unless otherwise noted, the fields above are required.

Proposed changes:

  • OAuth API returns a signed bearer token when validating OAuth header. The bearer token will be a JSON web token signed with ECDSA using P-256 and SHA-256 (also known as ES256). If a token can't be validated locally with this signature algorithm, pass it along to OAuth API (see below). Tokens signed with a different signature algorithm should be rejected by OAuth API.
  • Frontend web app or API, when encountering plaintext OAuth header, indicates to OAuth API that it should create bearer token when validating this header. Returned bearer token is then used for all downstream calls.
  • If a provided bearer token can't be parsed or validated, make an API call to OAuth API to validate the header. Failure scenarios:
    • The token format changed (e.g. different signature algorithm). A code update will be required to validate these without making an OAuth API call, but this future-proofs the tokens used so we can change the format if we need to by treating the header as an opaque token.
    • The API server's clock is too far ahead of the current time. If this happens, raise an error (but still allow the authenticated request) so that the clock skew can be corrected by an on-call developer
    • The public key was changed. If this happens, replace the in-memory public key that was expected with the key returned from the API call so future tokens will not fail
  • During initial rollout, only send bearer tokens to APIs that are known to support them. Plan on phasing out plaintext OAuth entirely for all API calls (will require team coordination).

Example use:

  • Public key: 1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2
  • Header: Authorization Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiaXNBZG1pbkNvbnN1bWVyIjoiZmFsc2UiLCJuYmYiOjE1MjIzNjE0NTgsImV4cCI6MTUyMjM2MjExOCwiaWF0IjoxNTIyMzYyMDU4LCJpc3MiOiJhdXRoLmZhaXRobGlmZS5jb20iLCJhdWQiOiJhbGwtYXBpcy5mYWl0aGxpZmUuY29tIn0.AmfdeftA6Slx36ZCWSzVTbiwYX0jqRNjq70aG-fi8MONcbBxAUVtvataQj11xrtyoiDsALTXfD8ewv3K0gwtJQ

Example response from OAuth API when validating an auth header (fallback scenario when token can't be parsed locally):

{
	"userId": 2986689, // this key is consistent with the API response for validating an OAuth header
	"alias": "Dustin Masters",
	"consumerName": "JWT demo",
	"isAdminConsumer": true,
	"consumerToken": "XXXXXX", 
}

Headers:
X-JWT-Public-Key: 1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2
X-JWT-Current-Time: 2018-03-14T09:00:00

Drawbacks of token-based approach:

  • Private key must be reasonably be protected. The current OAuth database needs the same amount of protection - a database dump of the access_tokens and consumers table would mean any auth header can be recreated for existing user sessions. Storing the private key in a shared database (redis? mysql?) would be a reasonable approach here assuming we can trust the database to be secure.
  • Clocks need be synchronized within a few minutes of current UTC time.

Proof of concept:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment