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
- Application asks the user to grant consent. Authorization server confirms this grant back to the application by handing it over an
authorization code
- Application presents this
authorization code
to the Authorization Server, which issues anaccess token
and optionally arefresh token
There are few things here that won’t work well for Public clients in these two scenarios -
- 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 samecustom scheme
in addition to the legitimate OAuth 2.0
app. When the redirect happens, the malicious app can intercept and extract thecode.
This picture from PKCE RFC illustrates the point
- The exchange of
code
for anaccess 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 thatClient Identifier (client_id)
is considered a public information and as we noted earlier aclient 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-urlencodedgrant_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.
- Authorization Server may issue a
Refresh Token
along withAccess Token
. ThisRefresh Token
is bound to the Client it was issued to. - When client asks for a new access token by presenting the
refresh token,
Authorization Serververifies
the client’s identity, and the binding, and issue a newrefresh 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-urlencodedgrant_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.
- PKCE RFC https://tools.ietf.org/html/rfc7636 doesn’t provide any recommendation on support for
refresh tokens
- OAuth2 RFC makes the following assumptions for Native 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 tokenR2
and also retains refresh tokenR1
(noting that its no longer valid) - If
R1
was leaked, and attacker attempted to get an access token usingR1
, 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
andrefresh token
are issued (access_type=offline) assuming client was registered forrefresh token
grant - Refreshing the
access token
doesn’t require any additional parameter and specifying just theclient_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-urlencodedgrant_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 !