Authorization Code + PKCE

With Hosted Login’s implementation of OpenID Connect (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 that can be exacerbated if users are connecting by using mobile devices. Let’s take a moment to explain what that problem is, and then we'll detail how Hosted Login deals with that problem.

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 authorization 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).

Fortunately there’s a way to to help avoid code interceptions attacks: the Proof Key for Code 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. That's the value of using PKCE, and that's why Akamai recommends that you use PKCE, a public clients (OIDC clients that don't even have client secrets) for user authentication and authorization.

But what exactly does PKCE do and how does it differ from the standard OIDC authorization flow? Here’s a brief explanation of how the process works:


PKCE Setup and Configuration

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:

RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYx
RkM4NjQ0QzdBNw


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.

As soon as you have the code verifier and code challenge string you're right to make an authorization request.


The Initial Authorization Request

To make an authorization request using PKCE, two pieces of information must be included in that request: the code challenge string and the hashing method (S256) used to generate that string. This information is specified in the request as the code_challenge and code_challenge_method parameters, respectively. 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= RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYx
RkM4NjQ0QzdBNw

&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 login client used to make the authorization request. For example:

cliient_id=22c9604-7b27-464f-bff5-83ba229323af


response_type 

Yes

Specifies the type of response expected from the authorization server; at this point in time the Identity Cloud only supports the code response type. Note that this parameter is required even though there’s only one supported response type:

response_type=code

The code response indicates that the client expects to get an authorization code back following a successful authentication. In turn, the client exchanges that code for a set of tokens.

scope

Yes

Specifies the OpenID Connect scopes to be accessible from the userinfo endpoint following a successful authentication and login. Note that you must include the scope parameter and, at a minimum, request the openid scope; this tells the authorization server that you want to authenticate by using OpenID Connect.

Other scopes supported by the Identity Cloud are detailed in the article OpenID Connect Scopes and Claims. You can request multiple scopes by separating each scope using a blank space:

scope=openid email profile

You can include any (or all) the supported scopes in your authentication request. However, that doesn’t mean that you’ll get back all of those scopes. Instead, the scopes made accessible from the userinfo endpoint depend on the value of the allowedScopes property found in the token policy applied during a user login. 

For example, suppose the allowedScopes property only specifies the openid and email scopes. In that case you can only get back those two scopes; any other scopes mentioned in your authorization request (such as profile or address) are ignored and are not returned.

redirect_uri

Yes

Specifies the URL of the page the user is redirected to following a successful authentication and login. For example:

redirect_uri=https://identitydocs.akamai.com/redirect

Note that the specified URL must be exactly match one of the URLs listed in the OIDC login client’s redirectURIs property. If the URL isn’t included in the redirectURIs property then the authorization request fails with an Invalid client error and the user will not be authenticated.

code_challenge

Yes

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

For example:

code_challenge= RTg4QjMyRUJCNzdBRTQ1MkM2NTAzRTVDOEQ5OTg
3QjIwMjVBNTcxQTU5RTJFNDYwMzJBQjYxRkM4NjQ0QzdBNw


code_challenge_method

Yes

 
Hashing algorithm used to generate the value of the code_challenge parameter. For Hosted Login, this will always be S256:

code_challenge_method=S256


state

No (but reccommended) 

A random string that helps guard against cross-site request forgery (CSRF). For example, suppose your authentication includes the following state parameter and parameter value: 

state=GA-ISU_6CwFn0tQTFiYD_-Gvy39Nb6iTdugdGIzTUng

After a successful authentication, you’ll be redirected to the URL specified by the redirect_uri parameter. If you were redirected by the authorization server then the state parameter and value will be included in the URI:

https://identitydocs.akamai.com/redirect_uri?code=AH6S2WG_XchALC-p&state=GA-ISU_6CwFn0tQTFiYD_-Gvy39Nb6iTdugdGIzTUng

If the state parameter in the redirect URI doesn’t match your original parameter value then you might be the victim of CSFR attack (defined as an attack in which malware tries to trick you into carrying out some sort of action you never intended to carry out). In that case, you should restart the authentication process.

prompt

No

Specifies which screen (if any) is displayed when a user makes an authorization request. Allowed values are:

  • none (the default value). When set, Hosted Login checks to see if the user has a valid session before displaying a screen. If the user does not have a valid session then the sign-in screen is displayed and the user must log in. However, if a valid session is found then the sign-in screen is not displayed and the user is automatically logged in based on the current session.
     
  • login. The sign-in screen is always displayed first, even if a valid session is found. This ensures that users log in each time they access the site.
     
  • create. The traditional registration screen (used for creating new account) is always displayed first. Note, however, that the Sign In link isn’t found on the traditional registration screen. That means that setting the prompt to create represents a dead-end for existing users: they don’t need to create account, but they can’t log on using their existing account.

For example:

prompt=login


max_age

No

Specifies the amount of time, in seconds, that can elapse before a user is required to reauthenticate. For example, suppose the max_age parameter is set to 3600 seconds (one hour). A user logs on, leaves the website, then returns 30 minutes later. Because the max_age limit of 1 hour has not been reached, the user will automatically be authenticated and resume their previous session.

Now, suppose a second user logs on, leaves the website, then comes back 2 hours later. because the max_age value has been exceeded, this user will be forced to reauthenticate.

Note that the max_age parameter applies only to logins. Suppose a third user logs on and stays on the site for 2 hours. That user will not be forced to reauthenticate halfway through their session. As noted, max_age only applies to logins.

ui_locales

No

Specifies the language/locale used when displaying Hosted Login login, registration, and user profile screens. Language preferences are passed as a space-delimited set of RFC 5646 language codes. For example:

ui_locales= fr-FR es-ES

In the preceding example, Hosted Login first tries to render screens by using French (fr-FR); if that fails, Hosted Login tries to render the screens by using Spanish (es-ES). If that fails, then Hosted Login defaults to displaying all screens in English.

Why would an attempt to render screens fail? This is almost always because you specified a language/locale that can’t be found in your flow: you can specify any language or locale that you want, but to actually display screens using that language/locale requires you to have the locale (and the accompanying translations) in your flow. See this article for more information.

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


nonce

No (but recommended)

Helps ensure that the identity token you receive is the same identity token that you requested (in other words, you got back a token sent in direct response to your authentication request). 

To use the nonce parameter, simply enter a random string in the Nonce field and then make your authentication request, When you decode the returned identity token, you should see a nonce property. The value in the identity token should be the same as the value included in your authentication request.

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:

https://v1.api.us.janrain.com/
e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize?
&client_id=a123ef65-83dc-4094-a09a-76e1bec424e7
&redirect_uri=https%3A%2F%2Fwacky-harmonious-bike.dev.or.janrain.com%2Fredirect_uri
&code_challenge=MJm7VEGLvMtD4Mi1SGUc2QPRPVKqyaoEbBTxYKC4UJk
&code_challenge_method=S256
&response_type=code
&scope=openid
&state=5TK5-3LXryr8EIxn6kV4mgqEa3KqRA4-HwHJbyzlgU0
&login_hint=gmstemp@hotmail.com

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.

claims

No

Specifies the claims (i.e., user profile attributes) to be included in the identity token or to be made accessible from the userinfo endpoint (or both). These claims can either be standard OpenID Connect claims (see OpenID Connect Scopes and Claims for more information) or custom claims created by your organization and defined in your login policies.

For example, this syntax makes the birthdate claim accessible from the userinfo endpoint:

&claims={"userinfo":{"birthdate":null}}

Meanwhile, this syntax adds a custom claim named organization to the identity token:

&claims={"id_token":{"organization":null}}

And this syntax makes the organization claim accessible from the userinfo endpoint and adds that same claim to the identity token:

&claims={"userinfo":{"organization":null}, "id_token":{"organization":null}} 

display

No

Specifies where (and how) the sign-in screen is displayed. Allowed values are:

  • page (the default value)., When you submit your authorization request, you’ll be redirected to a separate page that contains the sign-in screen (and nothing but the sign-in screen). After you’ve successfully logged on you'll be redirected to the page specified in the redirectURIs property of your OIDC client.
     
  • popup. When you submit your authorization request you are not redirected to a standalone login page. Instead, the login page appears in a pop-up window, no redirection required. After you’ve successfully logged on then you’ll be redirected to the page specified in the redirectURIs property of your OIDC client.

For example:

display=page


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, a curl command for exchanging an authorization code for a set of tokens might look similar to this:


curl -X POST \
https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'client_id=55c9604-x457-464f-bgf5-83hj229ju5rf
  -d 'code=tpKqJ7c_g2bOKBpl' \
  -d 'code_verifier=AdleUo9ZVcn0J7HkXOdzeqN6pWrW36K3JgVRwMW8BBQazEPV3kFnHyWIZi2jt9gA'
 

Two things to note here. First, no authentication is required. That's because this is a PKCE flow. Instead of configuring, say, Basic authentication, just be sure and include the code_verifier parameter. Second, remember that code_verifier, and all the other parameters, must be passed as an xxx-www-urlencoded body parameter. 

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 not have 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": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImE5NjRhNjE3YTc0YjZjZWNlMDM4NTdkYWExZThlMTQ0ZDExMTMyYTkiLCJ0e
XAiOiJKV1QifQ.eyJhdF9oYXNoIjoibklpWVRQaG9TaWs4Rmt0ZFl5cktZZyIsImF1ZCI6WyJhMjJjOTYwNC03YjI3LTQ2NGYtYmZm
NS04M2JhMjI5MzIzYWYiLCJodHRwczovL29wZW5pZGNvbm5lY3QubmV0L2NhbGxiYWNrIl0sImF1dGhfdGltZSI6MTU1MjU5
OTgyOCwiYXpwIjoiYTIyYzk2MDQtN2IyNy00NjRmLWJmZjUtODNiYTIyOTMyM2FmIiwiZXhwIjoxNTUyNjAzNDQyLCJnbG9iYW
xfc3ViIjoiY2FwdHVyZS12MTovL2NhcHR1cmUtYWxiLWJvcmRlci5tdWx0aS5kZXYub3IuamFucmFpbi5jb20veDNnbW5uamV5
enlycnQybm01ZHJmNW5rbjgvdXNlci8yZWRkMmYzMi0xZTQ5LTRiZjItYjE2NC03NjM3ODE3NjFiNTIiLCJpYXQiOjE1NTI1OTk4
NDIsImlzcyI6Imh0dHBzOi8vYXBpLm11bHRpLmRldi5vci5qYW5yYWluLmNvbS8wMDAwMDAwMC0wMDAwLTMwMDAtOD
AwMC0wMDAwMDAwMDAwMDAvbG9naW4iLCJzdWIiOiIyZWRkMmYzMi0xZTQ5LTRiZjItYjE2NC03NjM3ODE3NjFiNTIifQ.k
KPbex5j3ADyxZ_t8B8wiWUoDB7o8tamMjswCxMQKaTEJBpJBiYVATMdLvnd5HpZ5Hj_I0omt7Zq3svPFLvdy1xHC95KWyJu3
HK65ZP8Hc0tM3oLFjWhLYcRoJZVi5ButzP4RZr6QJgfUyKF3QT-GECFLXgOyRy1DP4j4Xev7F_MJ_nX4xdAutNsDvu6PGyI752
nS4cJ13kAbyD0puaoLwg1aAoMSa4wm1limPvv5HcnRAAZcyMQhaC13vHMnvCCRWzuHl94oNl2_ZblEtDQv_q_GfCvhXLrd1
VH7azarkeOtCNrD1aTyQ9owXJDxYJrcs2UTaop9tyA7_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.