Authorization Code + PKCE for Mobile Apps

OAuth 2.0 and OpenID Connect provide an extremely secure platform for authentication and authorization. That being said, however, it must be admitted that OAuth and OIDC, like all technologies, do must be properly implemented and deployed to avoid potential vulnerabilities. For example, with OIDC (or at least with Hosted Login’s implementation of OIDC) the basic workflow for authenticating a user goes something like this:

  1. The user (via an OpenID Connect client) makes an authentication request, logs on, and is authenticated.
  2. The server sends the client an authorization code.
  3. The client exchanges the authorization code for an access token, refresh token, and identity token.

As a general rule, this is a very secure process, especially when carried out by web applications running over a TLS network connection. However, there is at least one potential problem here, a problem exacerbated any time users are connecting by using mobile devices. Let’s take a moment to explain what that problem is.

By default, a client can exchange the authorization code (and get back an access token) without having to prove that they are the rightful owner of that code. For example, suppose Bob successfully logs on to a website and, as a result, the server sends him an authorization code. That’s good: that’s the way things are supposed to work.

However, suppose Toni somehow manages to hijack that authorization code and present it to the token endpoint (something known as an Authorization Code Interception Attack). The token endpoint won’t question this request: as long as the authorization code is valid the server exchanges that code for an access token. That means that Toni now has access to all the resources that Bob has access to. And she has that access for as long as the token remains valid (typically one hour).
Note: Why are mobile apps more vulnerable to having authorization codes hijacked? This is mainly because multiple apps can register as the “handler” for a redirect URI. As a result, an authorization server could unwittingly deliver the authorization code directly to a malicious app that managed to register itself as a redirect handler. Admittedly, this is a rare occurrence.

Mobile devices typically don’t run on such highly-secure networks. In addition to that, multiple apps can register as the “handler” for a redirect URI. As a result, an authorization server could unwittingly deliver the authorization code directly to a malicious app that managed to register itself as a redirect handler. Admittedly, this is a rare occurrence. But if it’s going to happen, it’s much more likely to happen on a mobile network.

Fortunately there’s a way to help avoid code interceptions attacks: the Proof Code for Key Exchange (PKCE, pronounced “pixie”) extension. This extension enables clients to assure the token exchange server that the authorization code they want to exchange really does belong to them. Best of all, the client can do that without having to exchange a client secret with the server, something that is definitely not recommended with mobile devices. (Again, due in large part to the fact that mobile devices run over less-secure networks.)

Hosted Login ships with PKCE compliance built-in: no additional configuration is required to enable PKCE, and no special login pages or login scripts are needed for PKCE sign-ins. As long as you have a PKCE client you can simply point that client towards your authorization URL and allow that client to log on using PKCE authentication. Hosted Login will take care of the rest.

But what exactly does it mean to be PKCE-compatible, or to log on using PKCE authentication? Here’s a brief explanation of how PKCE works:

Before a PKCE client makes an authentication request it creates a “code verifier”: a random string of 43 to 128 characters. For example:
After the client creates the code verifier, if takes that value and “hashes” it using the SHA256 hashing function. That turns the code verifier into a value similar to this:
The client then base64-url encodes the hashed string to create a “code challenge” string. For example:
Note: Hosted login requires the use of SHA256 to hash code verifiers. Although the PKCE standard allows for the use of plain-text code challenges, plain text is notsupported by hosted login. You can verify this by looking at the discovery document to see which code challenge methods are supported.

And now, let’s see how the Authorization Code + PKCE flow actually works.

The Initial Authentication Request

At this point the client will have two important pieces of information, both of which must be included in the authorization request: the code challenge string and the hashing method (SHA256) used to generate that string. Without PKCE, an authorization request might look similar to this:
&scope=openid profile email

With PKCE, there are two additional parameters: code_challenge and code_challenge_method. For example:
&scope=openid profile email

The following table summarizes the parameters used in the request, and also details the optional parameters available for use with Hosted Login:

Parameter Required Description
client_id Yes Unique identifier of the OIDC Client making the request. For example:
response_type Yes Specifies the authentication and authorization flow type.
scope Yes
Indicates the scopes you are requesting access to. For example:
openid profile email

Scopes can be collections of claims, and claims (for the most part) map to individual user profile attributes. For example, the email scope consists of two claims: emailAddress and emailVerified. With OpenID Connect, the openid scope must always be requested. This tells the authorization server that you want to authenticate by using OIDC.

redirect_uri Yes
The URI (e.g., the web page) that clients are redirected to after being authenticated. For example:

The redirect_uri used in the /authorize request must exactly match a whitelisted redirect_uri for the client. Also, the redirect_uri sent in the /authorize request must be the same one that is sent in a subsequent /token request.

state No (but recommended)
Value of the anti-forgery state token. For example:

The state is an opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie.

prompt No

Specifies the system behavior when a user needs to be reauthenticated (for example, because the max_age limit has been exceeded). Hosted Login supports two prompt values:

  • none. When a user needs to be reauthenticated, the authorization server first checks to see if the user currently has a valid Hosted Login session. If true, the user is then “silently” reauthenticated (that is, no authentication dialog box is displayed, and the user never knows he or she was reauthenticated). If false, an error is returned, and the user must reauthenticate in order for their Hosted Login session to continue.

  • login. The user is always asked to reauthenticate, regardless of whether he or not he or she currently has a valid session.
max_age No Specifies the maximum amount of time (in seconds) that a logon session can last before the user is required to reauthenticate.
ui_locales No
Specifies the end user’s preferred language (or languages) for the login and registration user interfaces. Language preferences should be passed as a set of space-delimited RFC5646 language codes; for example:
"en-US en-GB fr-CA"

In the preceding example, Hosted Login will first attempt to render the UI in US English. If that fails, the UI will be rendered in British English and then, if necessary, Canadian French.

If this parameter is not present, the default language and local will be used. Note that no error will be returned if you specify an invalid language code, or if your application does not support a specified language.

code_challenge Yes (with PKCE)

Hashed and encoded value generated by the client. This value will need to be verified before the client will be allowed to exchange an authorization code for a set of tokens.

For example:
code_challenge_method Yes (with PKCE)

Hashing algorithm used to generate code challenge:

For example:
nonce No (but recommended) String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token.
login_hint No Provides a way to prepopulate the email address field on the Hosted Login sign-in screen. In your authorization request, include the login_hint parameter followed by the email address of the user who needs to be authenticated. For example: e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize?

When you submit your authorization request, the email address will be included on the sign-in screen:

Note that Hosted Login cannot determine the email address to be included in the authorization request. Instead, you will need to use an alternate approach to determine the email address (for example, getting the email address when the user logs on to the computer) and then take the steps needed to add that address to the authorization request.

As noted, for a Hosted Login end user, the preceding activities are carried out by clicking a Login button that takes them to the login page. Once there, the user is asked to log on to their existing account, either by logging on to a social login identity provider (social login) or by supplying a username and password (traditional login):

After supplying their email address and password (in the case of a traditional login) the user clicks Sign In and authentication takes place. To the end user, nothing has changed: they still log on to your website the way they log on to most websites.

Meanwhile, the authorization server uses the supplied credentials (or the social login token received from the social identity provider) and attempts to log the user on.

The Redirect URI and Authorization Code

When the authorization server receives the PKCE request, the server saves a copy of the code challenge and the code challenge method before authenticating the user. If authentication is successful, the server returns the standard authorization response:

Note that the response includes the authorization code (highlighted in bold), but it does notinclude either the code challenge or the code challenge method. You’ll see why in just a moment.

Exchanging the Authorization Code for an Access Token

Let’s assume that Bob made the authorization request and that, after successfully logging on, he received his authorization code. It’s now time for the Open ID Connect client to exchange that code for an access token. When he presents the code to the token exchange server, he must also present the code verifier (the original string value AdleUo9ZVcn0J7HkXOdzeqN6pWrW36K3JgVRwMW8BBQazEPV3kFnHyWIZi2jt9gA).

For example:

As you no doubt recall, when Bob made his initial authorization request the server took note of the code challenge and the hashing method associated with that request. Because of that, the server can now take the code verifier, hash the verifier using SHA 256, then base64url-encode the hashed string. If the value derived by the server matches the code challenge included in the original request, then the exchange will be approved and Bob will be sent his tokens. Why? That’s right: if Bob’s original code challenge and the code challenge calculated by the server match, the authorization server can be confident that it is communicating with the correct client.

To use a simple (and, admittedly, unrealistic) example, suppose Bob’s original code challenge was 1234ABCD. Bob submits his token exchange request, and the server calculates it’s version of the code challenger. Let’s see if they match:

Bob    1234ABCD
Server 1234ABCD

Looks like we have a winner!

But suppose that, somewhere along the way, Toni intercepted Bob’s authorization code in the hopes of also snagging Bob’s access token. That’s going to be tough: after all, the authorization response does not include the code verifier, the code challenge, or the code challenge method. Toni can try including a code verifier but if it’s not the right code verifier (and the right algorithm) then the server won’t be able to recreate the code challenge. For example:
Bob    1234ABCD
Server EF432KLO1

Those two values don’t match. And that’s because, even though Toni was able to hijack the authorization code, she does nothave possession of the code verifier. In turn, that means that server will not honor her exchange request.


If the authorization code is accepted, the token exchange endpoint returns an API response similar to this:
   "access_token": "03v-eeodppPrrHXXIx56pRLyDBaOldDxqEwI59MFCFGVuSkLRapzgmfwmEHyKWle",
   "refresh_token": "uHs1rLqRSpSyBpRpfplTI44Oh3gdkjJAa8Gzs3C5uDulN2yOnxU9mg1L6CaUAqz5",
   "expires_in": 3600,
   "token_type": "Bearer",
   "scope": "address email openid phone profile",
   "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImE5NjRhNjE3YTc0YjZjZWNlMDM4NTdkYWExZThlMTQ0ZDExMTMyYTkiLCJ0eXA

Here's what the different name-value pairs in that response represent:

Property Description
access_token The newly-issued access token.
refresh_token The refresh token that accompanies the access token.

Amount of time (in seconds) before the access token expires. In this case, that's 1 hour (60 seconds x 60 minutes = 3,600 seconds).

Incidentally, identity tokens also expire after 1 hour (although that doesn’t matter too much because identity tokens are rarely used after they have been issued). Refresh tokens have a default lifespan of 90 days.

token_type Access token type. The token type will always be set to bearer, meaning that whoever has possession of the token is considered the rightful owner of that token. To gain access to resources, you only have to present the access token: you do not have to do anything to “prove” that the token belongs to you.
scope The OIDC scopes that the token has permission to retrieve. Scopes represent different sets of user profile attributes; for example, the profile scope enables you to return such things as the user’s name, his or her gender, his or her birthdate, etc.
Id_token The user’s identity token.

If you’re curious about the actual contents of a token, see the articleOpenID Connect Token Reference. In addition to that, you can decode an access token or a refresh token by using the introspection endpoint, and you can use any of a number of different JSON Web Token(JWT) decoders in order to view the contents of an identity token.