OAuth and PKCE

Jul 13, 2023 · 4 min read

OAuth is a common mechanism used to communicate with APIs and its Authorization Code Grant is the most-used grant type. In writing a CLI application to talk to these APIs, the interactions look like the following.

code grant

To improve security, many API providers using Authorization Code Grant requires PKCE (Proof Key for Code Exchange, pronounced as “pixy”). This post is about how I write CLI clients implementing PKCE. But before diving into this, let’s go through what PKCE does first.

The Attack

From RFC 7636, the potential attack which PKCE helps to mitigate is

In this attack, the attacker intercepts the authorization code returned from the authorization endpoint within a communication path not protected by Transport Layer Security (TLS), such as inter- application communication within the client’s operating system.

The conditions for this attack to succeed are

  • attacker manages to register a malicious application on the client device and registers a custom URI scheme that is also used by another application. The operating systems must allow a custom URI scheme to be registered by multiple applications.
  • authorization code grant is used
  • attacker has access to client_id and client_secret
  • and either
    • attacker (via the installed application) is able to observe only the responses from the authorization endpoint
      • this can be mitigated by code_challenge_method
    • attacker is able to observe requests (in addition to responses) to the authorization endpoint. The attacker is, however, not able to act as a man in the middle due to TLS connection. This was caused by leaking http log information in the OS.
      • To mitigate this, code_challenge_method value must be set either to S256

From the above description of attacking vectors, it should be obvious that PKCE is not a replacement of OAuth client authentication as PKCE requires client_id and client_secret in order to work. For a longer explanation, you can read Client Authentication vs. PKCE: Do you need both from Scott Bardy.

PKCE mechanism

As described above, the aim of such attack is to steal the authorization code so that it can be used to pretend to be a client to ask for an access token. PKCE is to ensure the traffic to authorization server is made by the true client. The mechanism of PKCE is not difficult to understand.

The client first generate a random code verifier code_verifier.

The client then send an authorization request with code challenge code_challenge which is a transformed code verifier t(code_verifier).

To ensure token request is made by the same client, the client send a token request with code verifier code_verifier. Upon receiving a token request, the server applies the same transformation to verify if the transformed code verifier is the same as the one it received in the authorization request.

In practice, transformation t involves hashing such as SHA256.

Code verifier is only being used once and cannot be reused. Thus, there is no chance of the code verifier being replayed.

PKCE OAuth client implementation in Go

const lengthCodeVerifier = 64

// character ~ is not included as some of the server implementations
// do not support it
//
// rune is a type alias to int
// effective this is an array of int
// each integer represents a Unicode code point
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._")

func generatePKCEVerifier() string {
  generator := rand.New(rand.NewSource(time.Now().UnixNano()))

  b := make([]rune, lengthCodeVerifier)
  for i := range b {
    b[i] = letterRunes[generator.Intn(len(letterRunes))]
  }
  return string(b)
}

func generatePKCEChallenge(verifier string) string {
  hash := sha256.Sum256([]byte(verifier))

  // see https://www.rfc-editor.org/rfc/rfc7636#appendix-A
  // for no-padding requirement
  return base64
    .URLEncoding
    .WithPadding(base64.NoPadding)
    .EncodeToString(hash[:])
}

The challenge can be sent in an authorization request as one of the parameters.

codeVerifier := generatePKCEVerifier()
codeChallenge := generatePKCEChallenge(codeVerifier)
oauth2.SetAuthURLParam("code_challenge", codeChallenge)

Both challenge and verifier should be stored on client side so that challenge can be verified in redirection request and verifier can be sent in the subsequent token request.

ctx = context.WithValue(ctx, codeVerifierContextKey, codeVerifier)
ctx = context.WithValue(ctx, codeChallengeContextKey, codeChallenge)

Upon redirection from authorization service with authorization code, the challenge previously sent will be included in the URL of the redirection and verification is required.

queryParts, _ := url.ParseQuery(r.URL.RawQuery)
codeChallenge := queryParts.Get("code_challenge")
expectedCodeChallenge := ctx.Value(codeChallengeContextKey).(string)

codeChallenge should match the previously stored expectedCodeChallenge and the same applies to code_challenge_method (not shown in code snippets). If the validations pass, the verifier can also be sent in a token request as one of the parameters.

oauth2.SetAuthURLParam(
  "code_verifier",
  ctx.Value(codeVerifierContextKey).(string),
)

For an actual implementation of verification of codeChallenge and sending codeVerifier in a token request, you can find it in function getTokenHandler.

I have written a function GetToken using channels and the above getTokenHandler to make it easier for a CLI application to utilise the OAuth workflow.

The End

That is all about implementing PKCE on client side. As you can see, it is not difficult. Hopefully this helps you understand the mechanism and allows you to implement yours.

Happy coding!

Alex Ho
Authors
Software Developer
Experienced software engineer with interests in web and cloud technologies