Authorization Code + PKCE for Mobile Apps


We're Moving ... Again

Yes, the Identity Cloud documentation is once more on the move. And this time we're headed to two different locations:

As for the Education Center, well, the Center itself will disappear on July 23rd. We apologize for any inconvenience, but we look forward to becoming a full-fledged member of the Akamai family.
 

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 and is authenticated.
  2. The server sends the client an authorization code.
  3. The client exchanges the authorization code for an access 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 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:

AdleUo9ZVcn0J7HkXOdzeqN6pWrW36K3JgVRwMW8BBQazEPV3kFnHyWIZi2jt9gA

After the client creates the code verifier, if takes that value and “hashes” it using the S256 hashing function. That turns the code verifier into a value similar to this:

E88B32EBB77AE452C6503E5C8D9987B2025A571A59E2E46032AB61FC8644C7A7

The client then base64-url encodes the hashed string to create a “code challenge” string. For example:

RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYxRkM4NjQ0QzdBNw

Note. Hosted login requires the use of S256 to hash code verifiers. Although the PKCE standard allows for the use of plain-text code challenges, plain text is not supported 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 (S256) used to generate that string. Without PKCE, an authorization request might look similar to this:

https://v1.api.us.janrain.com/00000000-0000-3000-8000-000000000000/login/authorize?
client_id=55c9604-x457-464f-bgf5-83hj229ju5rf
&redirect_uri=https://documentation.akamai.com
&scope=openid profile email
&response_type=code
&state=3bd5262737237ef4a

With PKCE, there are two additional parameters: code_challenge and code_challenge_method. For example:

https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/authorize?
client_id=55c9604-x457-464f-bgf5-83hj229ju5rf
&redirect_uri=https://documentation.akamai.com
&scope=openid profile email
&response_type=code
&state=3bd5262737237ef4a
&code_challenge= RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYxRkM4NjQ0QzdBNw
&code_challenge_method=S256

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:

55c9604-x457-464f-bgf5-83hj229ju5rf

response_type 

Yes

Specifies the authentication and authorization flow type. For Hosted Login, the response_type must be set to code, which means that, after a successful authentication, the user is sent an authorization code. That code must then be exchanged for an access token before the user can access resources.

scope

Yes

Indicates the scopes you are requesting access to. For example:

openid profile email

Scopes are 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_url

Yes

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

https://documentation.akamai.com

As you make your way through the authentication process, you’ll see that the redirect URI appears in several different API requests and API responses. The redirect URI must be the same in every request/response or the API call will fail.

state

Yes

Value of the anti-forgery state token. For example: 

99846266547289293014765635352342

The anti-forgery state token (also referred to as the nonce) is a randomly-generated value included in an authorization request and then returned along with the authorization code. The client can check the value returned from the authorization server with the value of the state parameter from the original request. If the values match, that helps verify that the authorization code is legitimate. If they don’t that suggests that something has gone wrong with the authentication process. In that case, authentication should be canceled and then restarted.

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.

id_token_hint

No

Specifies that the client should use a token previously issued by the authorization server as part of the authentication request. If the end user associated with the token is already logged on, then no new authentication is required and the user remains logged on. If the end user is not logged on, then he or she will be required to sign in.

If you decide to use the id_token_hint parameter you should also include the prompt=none parameter in your request.

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= RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg
3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYxRkM4Nj
Q0QzdBNw

code_challenge_method

Yes (with PKCE)

Hashing algorithm used to generate code challenge:

For example:

code_challenge_method=S256

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:

https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/code?state=security_token%3bd5262737237ef4a %url%https://documentation.akamai.com&code=4/JR27W91a-ofgCe9ur2m6bTghy77

Note that the response includes the authorization code (highlighted in red), 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:

https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/token
grant_type=authorization_code
&client_id=55c9604-x457-464f-bgf5-83hj229ju5rf
&redirect_url=https://documentation.akamai.com
&code=tpKqJ7c_g2bOKBpl
&code_verifier=AdleUo9ZVcn0J7HkXOdzeqN6pWrW36K3JgVRwMW8BBQazEPV3kFnHyWIZi2jt

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. 

Period.

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": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImE5NjRhNjE3YTc0YjZjZWNlMDM4NTdkYWExZThlMTQ0ZDExMTMyYTkiLCJ0eXAiOiJKV1QifQ.eyJhdF9oYXNoIjoibklpWVRQaG9TaWs4Rmt0ZFl5cktZZyIsImF1ZCI6WyJhMjJjOTYwNC03YjI3LTQ2NGYtYmZmNS04M2JhMjI5MzIzYWYiLCJodHRwczovL29wZW5pZGNvbm5lY3QubmV0L2NhbGxiYWNrIl0sImF1dGhfdGltZSI6MTU1MjU5OTgyOCwiYXpwIjoiYTIyYzk2MDQtN2IyNy00NjRmLWJmZjUtODNiYTIyOTMyM2FmIiwiZXhwIjoxNTUyNjAzNDQyLCJnbG9iYWxfc3ViIjoiY2FwdHVyZS12MTovL2NhcHR1cmUtYWxiLWJvcmRlci5tdWx0aS5kZXYub3IuamFucmFpbi5jb20veDNnbW5uamV5enlycnQybm01ZHJmNW5rbjgvdXNlci8yZWRkMmYzMi0xZTQ5LTRiZjItYjE2NC03NjM3ODE3NjFiNTIiLCJpYXQiOjE1NTI1OTk4NDIsImlzcyI6Imh0dHBzOi8vYXBpLm11bHRpLmRldi5vci5qYW5yYWluLmNvbS8wMDAwMDAwMC0wMDAwLTMwMDAtODAwMC0wMDAwMDAwMDAwMDAvbG9naW4iLCJzdWIiOiIyZWRkMmYzMi0xZTQ5LTRiZjItYjE2NC03NjM3ODE3NjFiNTIifQ.kKPbex5j3ADyxZ_t8B8wiWUoDB7o8tamMjswCxMQKaTEJBpJBiYVATMdLvnd5HpZ5Hj_I0omt7Zq3svPFLvdy1xHC95KWyJu3HK65ZP8Hc0tM3oLFjWhLYcRoJZVi5ButzP4RZr6QJgfUyKF3QT-GECFLXgOyRy1DP4j4Xev7F_MJ_nX4xdAutNsDvu6PGyI752nS-4cJ13kAbyD0puaoLwg1aAoMSa4wm1limPvv5HcnRAAZ-cyMQhaC13vHMnvCCRWzuHl94oNl2_ZblEtDQv_q_GfCvhXLrd1VH7azarkeOtCNrD1aTyQ9owXJDxYJrcs2UTaop9tyA7_HgctWQ"
}

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.

expires_in

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 article Hosted Login 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.