OAuth and PKCE
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.
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_idandclient_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
- this can be mitigated by
- 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_methodvalue must be set either toS256
- To mitigate this,
- attacker (via the installed application) is able to observe only the
responses from the authorization endpoint
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!