This small Sinatra app is a proof of concept OAuth service that proxies to a third-party OAuth provider using the authorization code grant web application flow, but extending it to support PKCE for public clients for services that do not currently support it.
Warning: this is not currently a production-ready service but could be used as the basis for one.
In the normal OAuth web application flow, a confidential client can obtain an access token by:
- Directing the user to an OAuth endpoint for a service, passing along a
client_id
and aredirect_uri
- The user authenticates and authorises the client and the service redirects to the redirect_uri, passing along a
code
parameter. - The client makes a second
POST
request to the service's access token endpoint, passing along theclient_id
,client_secret
and thecode
obtained in the previous step. - The service verifies the client ID and secret and returns an
access_token
(and perhaps arefresh_token
) in response.
This flow works fine for confidential clients, like web servers, as they can keep the client secret secure. It is essential that the client secret is kept secret as it is used (sometimes along with a registered redirect URI) to prevent man-in-the-middle attacks by verifying that the client requesting the access token is the same client that originally requested the authorization code.
By definition, native apps (mobile, desktop etc.) and client-side web apps are considered "public" clients. It is impossible for these clients to use a client_secret
without exposing it (e.g. client-side Javascript web apps will expose the secret in their source in the browser, similarly embedded strings within native app code can be easily extracted).
As well as taking steps such as checking redirect URIs against pre-registered values, the OAuth 2 spec recommends the use of "Proof Key for Code Exchange", however not all OAuth providers currently support this. In this model, clients do not require a client_secret
. Instead, they they generate their own high-entropy random code_verifier
every time they require an authorization code. The flow when using PKCE is:
- The client generates a
code_verifier
and a hashed and encodedcode_challenge
derived from thecode_verifier
using SHA256 and Base64 URL encoding. - The client directs the user to the OAuth endpoint as before, but also passes the
code_challenge
. - The user authenticates and is redirected back to the
redirect_uri
with the authorization code - the server keeps a record of this code along with the originalcode_challenge
. - The client makes a second
POST
request to obtain an access token, this time sending the originalcode_verifier
instead of aclient_secret
. - The service verifies the
code_verifier
by hashing and encoding it and comparing it with the originalcode_challenge
for the givencode
. - If the
code_verifier
is correct, it returns anaccess_token
.
Unfortunately, not all services support PKCE, which often leaves implementations of a public client with two options:
- Embed the
client_secret
in their application source or, - Implement their own hosted service that their client can obtain the access token from, which acts as a proxy to the original OAuth endpoint and attaches the client secret to each request.
The second option seems like a reasonable solution but it's still not ideal on it's own. Whilst it doesn't expose your client_secret
, having a web service that simply forwards on any requests for an access token with the client_secret
attached means you're effectively giving an attacker access to your client_secret
even though they don't know it's actual value.
This proof of concept aims to solve this problem by effectively acting like a PKCE-supporting OAuth service in it's own right, which can be configured to proxy to a specific OAuth endpoint. To do this, it simply requires knowledge of the authorize and access_token endpoints for the target OAuth service as well as the client secret. It then:
- Requires clients make a request to it's own
/oauth/authorize
endpoint, passing along all the parameters it would have sent to the original endpoint but also including acode_challenge
. - The proxy stores a reference to the
code_challenge
and the originalredirect_uri
in it's session before redirecting the user to the original endpoint with a newredirect_uri
pointing back to itself. - When the original service redirects back to the proxy server with it's
code
', the server persists thecode_challenge
(currently stored in it's session) to a key-value store, keyed against thecode
, before redirecting to the originalredirect_uri
for the public client with thecode
. - The client makes a
POST
request to the proxy server to obtain anaccess_token
, along with the originalcode_verifier
. - The proxy server uses the
code
in the access token request to lookup the originalcode_challenge
from the key-value store and compares it against thecode_verifier
. If they match, it forwards the request on to the original access token endpoint along with theclient_secret
and returns the response as-is to the client.