loading...
Cover image for Working with Proof Key for Code Exchange (PKCE)

Working with Proof Key for Code Exchange (PKCE)

flippedcoding profile image Milecia McG ・7 min read

We all know how important web security is because it protects our personal information across the internet. Typically when a request is sent to a server to get access to private information, there is some kind of access token included in the headers. That authorization request is usually someone trying to sign in to one of their accounts.

Usually we create an access token to send back when their credentials are confirmed in the form of a JWT or some other encrypted key. This is the normal authorization workflow and for the most part it works well. The problem is that there are a few cracks starting to show in this flow. That's what PKCE is here to fix.

What PKCE is

PKCE stands for Proof Key for Code Exchange and it is the new standard for more secure authorization. It's also commonly pronounced like "Pixie". It was created to make use of OAuth 2.0 to ensure that apps are safer. OAuth 2.0 is an authorization framework that explains how unrelated services can grant access to each other. It's like when you log in to Medium or something and you can use your Gmail credentials.

The problem with using the regular authorization flow is that the authorization code can be stolen in an authorization code interception attack. That's when someone steals the authorization code by using a malicious app that registers a URI scheme that matches the response of the authorization request. It could also mean that someone has gained access to the HTTP request or response logs, which they could probably check in using browser developer tools.

This is a huge problem that PKCE fixes because it checks that a client app actually owns the authorization code. The way to establishes this proof of possession is by adding a code verifier, a code challenge, and a code challenge method.

The code verifier is a random cryptographic string used to connect the authorization request with the token request.

The code challenge is created from the code verifier sent in the authorization request and it'll be used to verify against later in the process. It's made by SHA256 hashing the code verifier or if you don't want to do this transformation, you can just use the code verifier as the code challenge.

The code challenge method is what was used to make the code challenge. It's an optional parameter. You can specify it as "S256" or "plain". If you don't specify anything, then the default value is "plain".

This different authorization flow allows for more secure access on native and browser-based apps because those typically suffer from storing a client secret that is used across every instance of the app. This includes things like mobile apps and single page web apps. The typical authorization flow that we implement that stores the client secret is called the Implicit Flow.

Understanding the new authorization flow

There are multiple authorization specified by OAuth 2.0. OAuth 2.0 is a framework that explains how unrelated services can grant access to resources. An example of this would be if you use your Gmail account to login to Twitter or some other service.
OAuth 2.0 has a number of authorization flows and here's a quick overview of some of the options.

Authorization Code Flow

This is typically used on server-side apps because the source code isn't exposed to the public. The reason this is almost exclusively used on server-side apps is because you have to pass your app's Client Secret. The Client Secret is a key that needs to be kept private and secure or else someone could use it to gain access to your system.

The Authorization Code Flow works by exchanging an Authorization Code for a token. JWTs (JSON Web Tokens) are commonly used as the access tokens users receive after they have been authenticated by a service. You can see more about this particular flow in the official IETF docs: https://tools.ietf.org/html/rfc6749#section-4.1

Authorization Code Flow
https://auth0.com/docs/flows/concepts/auth-code

Implicit Flow

This was the most common authorization flow before PKCE was introduced. It was used on web-based and native apps to give a user an access token immediately without authenticating the client. The Implicit Flow used to be the recommended flow until PKCE came along. It's still ok to use for login purposes only, but if you want call an API you should use the PKCE flow.

Implicit Flow
https://auth0.com/docs/flows/concepts/implicit

Authorization Code Flow with PKCE

This flow is like the advanced version of the Authorization Code Flow because it addresses the few bits of security concerns it leaves open. In web-based apps, a Client Secret can't be securely stored because all of the source code is available in the browser. On native apps, decompiling the code will show the Client Secret that's used across all users and devices. You can learn more about the details here: https://tools.ietf.org/html/rfc7636

The PKCE Flow adds a secret made by the calling app that can be verified by the authorization server. This is what protects the Client Secret from any malicious attackers.

PKCE Flow
https://auth0.com/docs/flows/concepts/auth-code-pkce

Client Credentials Flow

When you're working with things like multiple CLIs, Docker daemons, or other services that are machine-to-machine apps, this is an authorization flow you can use. It lets the system authenticate and authorize the apps instead of the users. If you want more details, check out the IETF specifications here: https://tools.ietf.org/html/rfc6749#section-4.4

Client Credentials Flow
https://auth0.com/docs/flows/concepts/client-credentials

Device Authorization Flow

When you're working with an app in the IoT space, then this is the probably an authorization flow you've seen. This is how your device and its accompanying app communicate with each other. If you want to know more about it, you can read up on it here: https://tools.ietf.org/html/rfc8628

Device Authorization Flow
https://auth0.com/docs/flows/concepts/device-auth

Example of PKCE in code

We'll do an example of how to implement the PKCE Flow. There are a few different services that make it easy for you, so we'll use Auth0 to set this up. To get started, you can make a free account here: https://auth0.com/signup?&signUpData=%7B%22category%22%3A%22button%22%7D&email=undefined

Once you're logged in, find the "Applications" menu in the left sidebar. Then click the "Create Application" button and choose the type of application you're building. I'm going to use a React app for this demo, so select "Single Page Web Applications" for your application type. You'll be redirected to a "Quick Start" page and there will be multiple tutorials you can go through.

Once you have an account created, all you have left is to use the React SDK. Here's what an implementation looks like in a real React app using react-router-dom. This should be in the component that gets rendered in your index.js file. It's usually the App component.

import React from 'react'
import { Route, Router, Switch } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import { Auth0Provider, withAuthenticationRequired } from '@auth0/auth0-react'

import Header from './common/Header'
import Home from './Home'
import Goals from '../components/Goals'
import Items from '../components/Items'
import Logout from '../components/Logout'
import Settings from '../components/Settings'

export const history = createBrowserHistory()

const onRedirectCallback = (appState) => {
  // Use the router's history module to replace the url
  history.replace(appState?.returnTo || window.location.pathname)
}
const ProtectedRoute = ({ component, ...args }) => (
  <Route component={withAuthenticationRequired(component)} {...args} />
)
const App = () => {
  return (
    <Auth0Provider
      domain={process.env.REACT_APP_AUTH_DOMAIN}
      clientId={process.env.REACT_APP_AUTO_CLIENT_ID}
      redirectUri={window.location.origin}
      onRedirectCallback={onRedirectCallback}
    >
      <Header />
      <Router history={history}>
        <Switch>
          <ProtectedRoute exact path="/" component={Home} />
          <ProtectedRoute path="/goals" component={Goals} />
          <ProtectedRoute path="/items" component={Items} />
          <ProtectedRoute path="/logout" component={Logout} />
          <ProtectedRoute path="/settings" component={Settings} />
        </Switch>
      </Router>
    </Auth0Provider>
  )
}
export default App

The main thing to note here is that you'll need to set the right values for REACT_APP_AUTH_DOMAIN and REACT_APP_AUTH_CLIENT_ID in a .env file. Your .env file will look something like this.

HTTPS=true
REACT_APP_AUTH_DOMAIN=somekindofdomainname.us.auth0.com
REACT_APP_AUTO_CLIENT_ID=s0m3cl13nt1d

You also need to include the login and logout buttons for users. Those can go anywhere it makes sense in your app, but they should probably be visible on every page or view. As an example, I've added the buttons to a header component that displays on every view in a React app.

import React, { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBars } from '@fortawesome/free-solid-svg-icons'
import { useAuth0 } from '@auth0/auth0-react'

import Menu from './Menu'

const Header = () => {
  const [showMenu, setShowMenu] = useState(false)
  const { isAuthenticated, loginWithPopup, logout } = useAuth0()

  return (
    <>
      <HeaderWrapper>
        <Box>
          {!isAuthenticated && (
            <button onClick={loginWithPopup}>Log in</button>
          )}
          {isAuthenticated && (
            <button
              onClick={() => {
                logout({ returnTo: window.location.origin })
              }}
            >
              Log out
            </button>
          )}
          <LinkWrapper href="/">McG</LinkWrapper>
          <MenuButtonWrapper onClick={() => setShowMenu(!showMenu)}>
            <FontAwesomeIcon icon={faBars} id="menu-icon" />
          </MenuButtonWrapper>
        </Box>
      </HeaderWrapper>
      {showMenu ? <Menu /> : ''}
    </>
  )
}

Once you have these elements in place, your app is officially following the PKCE Flow!

Other thoughts

Things are changing in web security every day. New attacks are created, patches are released, and new tools come to light. It's always best to try and stay up with best security practices, even though it takes time. It's better to spend the extra effort upfront instead of try to recover from an attack later.


Make sure that you follow me on Twitter @FlippedCoding! I'm always posting useful, random tech stuff there.

Discussion

pic
Editor guide