If you've ever created a login page or auth system, you might be familiar with OAuth 2.0, the industry standard protocol for authorization. It allows an app to access resources hosted on another app securely. Access is granted using different flows, or grants, at the level of a scope.

For example, if I make an application (Client) that allows a user (Resource Owner) to make notes and save them as a repo in their GitHub account (Resource Server), then my application will need to access their GitHub data. It's not secure for the user to directly supply their GitHub username and password to my application and grant full access to the entire account. Instead, using OAuth 2.0, they can go through an authorization flow that will grant limited access to some resources based on a scope, and I will never have access to any other data or their password.

Using OAuth, a flow will ultimately request a token from the Authorization Server, and that token can be used to make all future requests in the agreed upon scope.

Note: OAuth 2.0 is used for authorization, (authZ) which gives users permission to access a resource. OpenID Connect, or OIDC, is often used for authentication, (authN) which verifies the identity of the end user.

Grant Types

The type of application you have will determine the grant type that will apply.

Grant Type Application type Example
Client Credentials Machine A server accesses 3rd-party data via cron job
Authorization Code Server-side web app A Node or Python server handles the front and back end
Authorization Code with PKCE Single-page web app/mobile app A client-side only application that is decoupled from the back end

For machine-to-machine communication, like something that cron job on a server would perform, you would use the Client Credentials grant type, which uses a client id and client secret. This is acceptable because the client id and resource owner are the same, so only one is needed. This is performed using the /token endpoint.

For a server-side web app, like a Python Django app, Ruby on Rails app, PHP Laravel, or Node/Express serving React, the Authorization Code flow is used, which still uses a client id and client secret on the server side, but the user needs to authorize via the third-party first. This is performed using both an /authorize and /token endpoints.

However, for a client-side only web app or a mobile app, the Authorization Code flow is not acceptable because the client secret cannot be exposed, and there's no way to protect it. For this purpose, the Proof Key for Code Exchange (PKCE) version of the authorization code flow is used. In this version, the client creates a secret from scratch and supplies it after the authorization request to retrieve the token.

Since PKCE is a relatively new addition to OAuth, a lot of authentication servers do not support it yet, in which case either a less secure legacy flow like Implicit Grant is used, where the token would return in the callback of the request, but using Implicit Grant flow is discouraged. AWS Cognito is one popular authorization server that supports PKCE.

PKCE Flow

The flow for a PKCE authentication system involves a user, a client-side app, and an authorization server, and will look something like this:

  1. The user arrives at the app's entry page
  2. The app generates a PKCE code challenge and redirects to the authorization server login page via /authorize
  3. The user logs in to the authorization server and is redirected back to the app with the authorization code
  4. The app requests the token from the authorization server using the code verifier/challenge via /token
  5. The authorization server responds with the token, which can be used by the app to access resources on behalf of the user

So all we need to know is what our /authorize and /token endpoints should look like. I'll go through an example of setting up PKCE for a front end web app.

GET /authorize endpoint

The flow begins by making a GET request to the /authorize endpoint. We need to pass some parameters along in the URL, which includes generating a code challenge and code verifier.

Parameter Description
response_type code
client_id Your client ID
redirect_uri Your redirect URI
code_challenge Your code challenge
code_challenge_method S256
scope Your scope
state Your state (optional)

We'll be building the URL and redirecting the user to it, but first we need to make the verifier and challenge.

Verifier

The first step is generating a code verifier, which the PKCE spec defines as:

Verifier - A high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "*" / "~" from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters.

I'm using a random string generator that Aaron Parecki of oauth.net wrote:

function generateVerifier() {
  const array = new Uint32Array(28)
  window.crypto.getRandomValues(array)

  return Array.from(array, (item) => `0${item.toString(16)}`.substr(-2)).join(
    ''
  )
}

Challenge

The code challenge performs the following transformation on the code verifier:

Challenge - BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

So the verifier gets passed into the challenge function as an argument and transformed. This is the function that will hash and encode the random verifier string:

async function generateChallenge(verifier) {
  function sha256(plain) {
    const encoder = new TextEncoder()
    const data = encoder.encode(plain)

    return window.crypto.subtle.digest('SHA-256', data)
  }

  function base64URLEncode(string) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+\$/, '')
  }

  const hashed = await sha256(verifier)

  return base64URLEncode(hashed)
}

Build endpoint

Now you can take all the needed parameters, generate the verifier and challenge, set the verifier to local storage, and redirect the user to the authentication server's login page.

async function buildAuthorizeEndpointAndRedirect() {
  const host = 'https://auth-server.example.com/oauth/authorize'
  const clientId = 'abc123'
  const redirectUri = 'https://my-app-host.example.com/callback'
  const scope = 'specific,scopes,for,app'
  const verifier = generateVerifier()
  const challenge = await generateChallenge(verifier)

  // Build endpoint
  const endpoint = `${host}?
    response_type=code&
    client_id=${clientId}&
    scope=${scope}&
    redirect_uri=${redirectUri}&
    code_challenge=${challenge}&
    code_challenge_method=S256`

  // Set verifier to local storage
  localStorage.setItem('verifier', verifier)

  // Redirect to authentication server's login page
  window.location = endpoint
}

At what point you call this function is up to you - it might happen at the click of a button, or automatically if a user is deemed to not be authenticated when they land on the app. In a React app it would probably be in the useEffect().

useEffect(() => {
  buildAuthorizeEndpointAndRedirect()
}, [])

Now the user will be on the authentication server's login page, and after successful login via username and password they'll be redirected to the redirect_uri from step one.

POST /token endpoint

The second step is retrieving the token. This is the part that is usually accomplished server side in a traditional Authorization Code flow, but for PKCE it's also through the front end. When the authorization server redirects back to your callback URI, it will come along with a code in the query string, which you can exchange along with the verifier string for the final token.

The POST request for a token must be made as a x-www-form-urlencoded request.

Header Description
Content-Type application/x-www-form-urlencoded
Parameter Description
grant_type authorization_code
client_id Your client ID
code_verifier Your code verifier
redirect_uri The same redirect URI from step 1
code Code query parameter
async function getToken(verifier) {
  const host = 'https://auth-server.example.com/oauth/token'
  const clientId = 'abc123'
  const redirectUri = `https://my-app-server.example.com/callback`

  // Get code from query params
  const urlParams = new URLSearchParams(window.location.search)
  const code = urlParams.get('code')

  // Build params to send to token endpoint
  const params = `client_id=${clientId}&
    grant_type=${grantType}&
    code_verifier=${verifier}&
    redirect_uri=${redirectUri}&
    code=${code}`

  // Make a POST request
  try {
    const response = await fetch(host, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params,
    })
    const data = await response.json()

    // Token
    console.log(data)
  } catch (e) {
    console.log(e)
  }
}

Once you obtain the token, you should immediately delete the verifier from localStorage.

const response = await getToken(localStorage.getItem('verifier'))
localStorage.removeItem('verifier')

When it comes to storing the token, if your app is truly front end only, the option is to use localStorage. If the option of having a server is available, you can use a Backend for Frontend (BFF) to handle authentication. I recommend reading A Critical Analysis of Refresh Token Rotation in Single-page Applications.

Conclusion

And there you have it - the two steps to authenticate using PKCE. First, build a URL for /authorize on the authorization server and redirect the user to it, then POST to the /token endpoint on the redirect. PKCE is currently the most secure authentication system that I know of for a front-end only web or mobile app. Hopefully this helps you understand and implement PKCE in your app!