PKCE, Public Clients and Refresh Token

msingh
6 min readMar 30, 2020

--

In this article we will talk about

  • OAuth 2.0 Public clients with focus on native apps
  • PKCE extension to the Authorization Code grant type- specifically how it mitigates the code interception attack
  • Managing Refresh Tokens for native apps — verifying client Identity

This article assumes some level of familiarity with OAuth2 and Authorization Code Grant flow in particular.

What is a Public Client?

Public clients fall in two categories — native apps or javascript apps (also called SPA or user-agent based application)

Native applications are clients installed and executed on resource owner device e.g. desktop and mobile apps.

The key thing to note about Public clients is that any information contained in the app can’t be considered confidential- e.g a client secret in a native desktop app binary. In fact, OAuth2 spec advises specifically this -

The authorization server MUST NOT issue client passwords or other
client credentials to native application or user-agent-based
application clients for the purpose of client authentication.

Why is PKCE needed?

An OAuth2 authorization flow¹ , is a two step process

  1. Application asks the user to grant consent. Authorization server confirms this grant back to the application by handing it over an authorization code
  2. Application presents this authorization code to the Authorization Server, which issues an access token and optionally a refresh token

There are few things here that won’t work well for Public clients in these two scenarios -

  1. The code is issued by adding it as a query parameter to the registered² redirect_uri of the application. For public clients there is a potential security risk with an attacker hijacking the code. One example in case of mobile clients, is, having a malicious app that register itself as a handler for the same custom scheme in addition to the legitimate OAuth 2.0
    app. When the redirect happens, the malicious app can intercept and extract the code. This picture from PKCE RFC illustrates the point
  1. The exchange of code for an access token requires that the Authorization server validate things like — the code was indeed issued to this client and authenticate the client using its credentials. Note that Client Identifier (client_id) is considered a public information and as we noted earlier a client secret or password can’t really be considered confidential (shouldn’t be issued to public clients anyway).

So once the attacker has the code it’s easy to impersonate the legitimate client and exchange it for access token .

PKCE addresses this security concern. The idea at a high level is something like this -

  • In Step 1, when asking for authorization grant, send something random along with the request. Server remembers this random associates it with the code it sends back.
  • In Step 2, when exchanging the code for token, send the same random. Server validates it with the previously stored random in addition to other standard validations (i.e. code was indeed issued to the client).

So, intercepting the code is not enough to get a token any more. An obvious point is that the random must be very hard to guess 😃. Lets look at this a bit deeper

PKCE Flow

Step 1 : Client creates a code verifier

A code verifier is high entropy cryptographic random string . High Entropy == very hard to guess . Here is some go code that creates a such a high entropy code verifier/random string

//Create a random 32 byte slice
codeVerifier := make([]byte, 32)
rand.Read(codeVerifier)
//base64url-encoded to produce a 43-octet URL safe string to use as //the code verifier
codeVerifier := base64.RawURLEncoding.EncodeToString(randomBytes)

Step 2: Convert this verifier to a code challenge

Code Challenge is a value derived from the code verifier. The logic is

code challenge=BASE64URL-ENCODE(SHA256(ASCII(code_verifier))).

Here is some go code that implements the above logic —

//Create a SHA256 Hasher
hasher := crypto.SHA256.New()
hasher.Write([]byte(codeVerifierBase64))//Generate the SHA256 hash bytes
codeVerifierHash := hasher.Sum(nil)
codeChallenge := base64.RawURLEncoding.EncodeToString([]byte(codeVerifierHash))

Step 3 : Include the code challenge in Authorization request

Client builds the authorize request, including all standard parameters as in authorization code grant flow but also includes the challenge. The Authorize URL would be something like below (example from an Okta Authorization Server request)

https://dev-91886.okta.com/oauth2/ausq8pt46nVxpuZgB0h7/v1/authorize?access_type=offline&client_id=0oaqkhfclp4KWm5iJ0h7&code_challenge=0tEnYUTOm5kOUxVD9k4CtbvpAK_bEm-QcGlb52xOra0&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A9999%2Fcallback&response_type=code&scope=openid+offline_access&state=foo

The highlighted values in the above requests show the code challenge (0tEnYUTOm5kOUxVD9k4CtbvpAK_bEm-QcGlb52xOra0) and the hashing method used to generate the code challenge (S256)

If this step was successful i.e. User granted the authorization to the application (authenticated with the server, consented to scope etc. etc.), the Authorization Server then redirects the application to its redirect_uri adding code and state to the query string — e.g.

http://appserver:9999/callback?code=RrJkcBegGNOpZJUoXgOR&state=foo

Step 4 : Exchange code for token

The application uses the code received from the redirect and makes a POST request to the Authorization Server /token endpoint- e.g.

POST /oauth2/ausq8pt46nVxpuZgB0h7/v1/token
Content-Length: 188
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&client_id=0oaqkhfclp4KWm5iJ0h7&code_verifier=TWRKgJBRV-8s6s3yvjzfZAUCNb4Qcsb3QeFGfbUJGIQ&code=RrJkcBegGNOpZJUoXgOR&redirect_uri=http://localhost:9999/callback

The main difference between PKCE exchange of code for access token, vs. standardauthorization code grant flow is the method used by server to verify the client, in the absence of a client secret. Can’t explain better than the RFC itself —

Upon receipt of the request at the token endpoint, the server verifies it by calculating the code challenge from the received “code_verifier” and comparing it with the previously associated “code_challenge”, after first transforming it according to the “code_challenge_method” method specified by the client.

A successful exchange request should end up in App getting an AccessToken.

But what about Refresh Token?

Refresh Token

Refresh Token are credentials that can be used to “refresh” access tokens, when the current access token expires or becomes invalid or application needs additional access tokens.

So the idea is very simple.

  1. Authorization Server may issue a Refresh Token along with Access Token. This Refresh Token is bound to the Client it was issued to.
  2. When client asks for a new access token by presenting the refresh token, Authorization Server verifies the client’s identity, and the binding, and issue a new refresh token

Verifying the Client Identity

Its well defined for confidential clients

The authorization server MUST require client authentication for confidential clients or for any client that was issued client credentials (or with other authentication requirements)

The most common mechanism is Clients using HTTP Basic authentication scheme. with the issued client_id and client_secret . A token refresh request from confidential Client would look like something like this-

POST /token HTTP/1.1
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

But what about Native Applications?

This is where things become tricky. Recall that Public Clients are not issued passwords so they can’t authenticate in the same way as confidential clients

The authorization server MUST NOT rely on public client authentication for the purpose of identifying the client.

It is assumed that any client authentication credentials included in the (native) application can be extracted. On the other hand, dynamically issued credentials such as access tokens or refresh tokens can receive an acceptable level of protection.

It kind of means it’s the responsibility of the application to protect issued refresh and access tokens, since they are dynamic.

In the absence of strong verification method for native clients, there are two other recommendations which focus on reducing the blast radius -

Detecting abuse of Refresh Token

  • App asks for access token refresh with Refresh Token= say R1
  • Auhtorization Server returns access token A2, new refresh token R2 and also retains refresh token R1 (noting that its no longer valid)
  • If R1 was leaked, and attacker attempted to get an access token using R1, then authorization server knows that the token was compromised

Issue ClientID and Secret on a per-install basis

The idea is to have each deployment of the native app treated as a different client. In such a scenario, client id and secret can be issued to the native app in some way during the installation process. This allows for selective revocation of secret. However, depending on the number of installations and usage, issuing and maintaining deployment specific credentials can be a challenge.

Section 3.7 of OAuth 2.0 Threat Model RFC describes different deployment models in detail

Testing the waters

A quick test using Okta Authorization Server gets me this —

  • At the end of PKCE flow, an access token and refresh token are issued (access_type=offline) assuming client was registered for refresh token grant
  • Refreshing the access token doesn’t require any additional parameter and specifying just the client_id is enough. So following request to refresh the access token succeeds ( request body is highlighted)
POST /oauth2/ausq8pt46nVxpuZgB0h7/v1/token 
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&client_id=0oaqkhfclp4KWm5iJ0h7&refresh_token=RKKjNsRJHc6EgrqYsOw82AyWrbILmnhaDuDqL5arEBg"
  • The refresh token are not rotated and the call returns a new access token but the same refresh token

Detailed analysis about how different providers in the next post !

--

--

msingh
msingh

Responses (3)