401 Unauthorized PKCE Code Flow

Banging my head against the wall over here trying to get the PKCE Code Flow working for access tokens. It’s failing with 401 unauthorized (no details in response) when I try to submit the code and code_verifier to the token URI at https://www.warcraftlogs.com/oauth/token.

Here is my initial request to the authorization URI.

https://www.warcraftlogs.com/oauth/authorize?client_id=<client_id>&code_challenge=NzMwM2FlMTdhZDVmNjI0MzZiN2NlNTBkYTM3MjA5NjNlNzE5NDE3MjVmZGI5NzkzNGVjZDA5NjBmMmM4Yjg4Zg&code_challenge_method=S256&state=ECJhAEcmVvb0mrLXk3hYI9K8MzVo0IgjZgPUNv2DACJk_GPevvMazzRPmCXPCt1B&redirect_uri=http://localhost:59107&response_type=code

I authorize my application and am redirected to my listening app which serves up a page with some javascript to complete the rest of the flow. I succesfully pull the returned code from the URL fragment. Here is the POST request I make to the token URI.

POST /oauth/token HTTP/1.1
Host: www.warcraftlogs.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:59107/?code=<big_long_code_here_from_url_fragment>&state=ECJhAEcmVvb0mrLXk3hYI9K8MzVo0IgjZgPUNv2DACJk_GPevvMazzRPmCXPCt1B
Content-type: application/x-www-form-urlencoded
Content-Length: 1255
Origin: http://localhost:59107
DNT: 1
Connection: keep-alive

Note that I’m setting the content-type header. But I’ve tried it without that set, or set to text/plain and that doesn’t work either.

Here is the data I send with the POST.

client_id=<client_id>&code_verifier=RUNKaEFFY21WdmIwbXJMWGszaFlJOUs4TXpWbzBJZ2paZ1BVTnYyREFDSmtfR1BldnZNYXp6UlBtQ1hQQ3QxQn5TNTI0TC5-Z0hKTHRmcX5ZS3V0Ri5MTF9ZTjNCZTFVT0xxbA&redirect_uri=http%3A%2F%2Flocalhost%3A59107&grant_type=authorization_code&code=<big_long_code_here_from_url_fragment>

Here is my actual javascript xmlhttprequest I perform.

var re_code = /\?code=(\w+)/;
var code = re_code.exec(location)
var requestToken = new XMLHttpRequest();
var urlToken = "https://www.warcraftlogs.com/oauth/token";
var paramsToken = "client_id=" + client_id + "&" +
                  "code_verifier=" + code_verifier + "&" +
                  "redirect_uri=" + encodeURIComponent("http://localhost:"+PORT) + "&" +
                  "grant_type=authorization_code&" +
                  "code=" + code[1];
 requestToken.open("POST", urlToken, true);
 requestToken.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
 requestToken.send(paramsToken);

The documentation is unclear on how exactly to generate the code_verifier and code_challenge (the PHP and Javascript examples contradict each other), but a close reading of the RFC specification shows it should be done like so.

code_verifier = generateRandomStringOfLength(128)
code_challenge = sha256Hash(code_verifier)

Then, when sending each value in POST requests, base64 encode each.

code_verifier = base64Encode(code_verifier)
code_challenge = base64Encode(code_challenge)

I have tried many different orders of operation on when to use the base64 encoding (as I said, the PHP and Javascript examples differ from the RFC specifications) but nothing seems to work. Here is an example of my generated values I’m using, removing trailing ‘==’ and converting all ‘+’ to ‘-’ and ‘/’ to ‘_’ as the documents specify.

code_verifier: ECJhAEcmVvb0mrLXk3hYI9K8MzVo0IgjZgPUNv2DACJk_GPevvMazzRPmCXPCt1B~S524L.~gHJLtfq~YKutF.LL_YN3Be1UOLql
code_challenge: 7303ae17ad5f62436b7ce50da3720963e71941725fdb97934ecd0960f2c8b88f

and in base64

code_verifier: RUNKaEFFY21WdmIwbXJMWGszaFlJOUs4TXpWbzBJZ2paZ1BVTnYyREFDSmtfR1BldnZNYXp6UlBtQ1hQQ3QxQn5TNTI0TC5-Z0hKTHRmcX5ZS3V0Ri5MTF9ZTjNCZTFVT0xxbA
code_challenge: NzMwM2FlMTdhZDVmNjI0MzZiN2NlNTBkYTM3MjA5NjNlNzE5NDE3MjVmZGI5NzkzNGVjZDA5NjBmMmM4Yjg4Zg

Where are things going wrong that I’m always getting 401 unauthorized?

You are the first person to try to use PKCE (it’s brand new, just added a few days ago to the docs), so it’s possible there are issues with it.

This is the Laravel documentation for it that includes PHP sample code, so this is what it expects:

Based on the Laravel documentation there, the code verifier shouldn’t be base64 encoded. Changing that got me an actual JSON response in the body of the 401.

{"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"}

Otherwise it looks like I’m doing everything right. I don’t know what other information I can provide to figure this out.

Yeah, I think maybe a client has to be designated as a PKCE client, but I can’t see how Laravel decides this. The command line client creation has a --public option, but I see nothing in the API for creating public clients programmatically, nor do I see anything in the database structures that would designate a client as “public”.

It looks like the distinction is that the client has no secret, so my guess is the code keys off a client secret being present or absent to decide whether or not PKCE is the appropriate flow. The API doesn’t give me a way to create a public client though, so I’ll need to actually add some custom code to null out the secret for people wanting to use PKCE. Not sure when I’ll get to that, so for now, I’d recommend just using v1.

Looks like the API does support this… will need to add stuff to the client creation UI though.

Ok I have deployed new UI when creating clients that allows you to designate that the client uses the PKCE code flow.

I haven’t yet gotten it to work with PKCE. I swapped over to the authorization flow with client_id/secret and I was able to get tokens. I have a feeling the issue lies in what I’m passing in the POST header ‘Authorization’. I have tried:

  • Authorization: Basic <client_id> // No secret
  • Authorization: Basic <client_id:> // Blank for secret
  • Authorization: Basic <client_id: > // Space for secret
  • Authorization: Basic <client_id:-> // Dash for secret since the creation page technically says thats my secret.

All of these had the values in base64. Also tried removing any = in the values as the code challenge stuff says to do.

PKCE should work now as long as you create a PKCE client to use with it. If you had an existing client, you’ll have to make a new one and check the box saying it’s PKCE-only.