Select to view content in your preferred language

Invalid request - Invalid PKCE code_challenge_verifier when using oauth2/token

5012
4
Jump to solution
07-29-2022 02:46 PM
JoeHershman
MVP Regular Contributor

Hi,

I am trying to get a token using OAuth2 from a web app.  I am able to do it using the /authorize endpoint if using response_type=token.  However, this returns the token in plan text in the redirect url which I find a bit low security.  I guess it is in the user's own browser, so perhaps not that big a deal.

I thought I would try to use the approach of getting an authorization code and then use the /token endpoint to get the token.  However, I am unable to retrieve this successfully.

Initially I send the /authorize request

 

string authorizeUrl = "https://www.arcgis.com/sharing/rest/oauth2/authorize";
string clientId = "my-client-id";
string redirectUrl = "https://localhost:7109/counter";
string responseType = "code";


string codeChallenge = "12345";
string codeChallengeMethod = "plain";

Console.WriteLine($"Index Challenge: {codeChallenge}");

$"{authorizeUrl}?client_id={clientId}&redirect_uri={redirectUrl}&response_type={responseType}&code_challenge={codeChallenge}&code_challenge_method={codeChallengeMethod}";

UriBuilder builder = new UriBuilder(authorizeUrl)
{
	Query = $"client_id={clientId}&redirect_uri={redirectUrl}&response_type={responseType}&code_challenge={codeChallenge}&code_challenge_method={codeChallengeMethod}"
};

string oAuthUrl = builder.ToString();
NavigationManager.NavigateTo(oAuthUrl);

 

This works perfect and takes me to the login page and when I login I am then redirected correctly to redirect page and the code is attached.

So then a post request is made to get the /token endpoint

 

//this gets the returned code, I have validated that it matches when is in the redirect Url
string code =  Navigation.Uri.Substring(Navigation.Uri.IndexOf("=", StringComparison.Ordinal) + 1);

string tokenUrl = "https://www.arcgis.com/sharing/rest/oauth2/token";
string clientId = "my-client-id";
string redirectUrl = "https://localhost:7109/counter";
//string codeChallenge = CreateSHA256Challenge();
string codeChallenge = "12345";

var dictionary = new Dictionary<string, string>
{
	{"client_id", clientId},
	{"grant_type", "authorization_code"},
	{"code", code!},
	{"code_verifier", codeChallenge},
	{"redirect_uri", redirectUrl}
};

FormUrlEncodedContent content = new FormUrlEncodedContent(dictionary);

using HttpClient client = new HttpClient();
var response = await client.PostAsync(tokenUrl, content);
var json = await response.Content.ReadAsStringAsync();

 

However, this fails and returns 

 

{
	"error": {
		"code": 400,
		"error": "invalid_request",
		"error_description": "Invalid PKCE code_challenge_verifier",
		"message": "Invalid PKCE code_challenge_verifier",
		"details": []
	}
}

 

 

The example I am just using a very simple challenge in plain text to make this as easy as possible to validate.

Is there something I am missing in the /token request?  One thing I find odd is you do not need to specify if is plain or S256 in the /token request.  The /token post call is within milliseconds of the initial /authorize request so nothing could have expired.

Does anyone has thoughts on what I am missing?

Thanks - Joe

 

 

 

 

Thanks,
-Joe
Tags (1)
0 Kudos
1 Solution

Accepted Solutions
Raul_Jimenez
Esri Contributor

Sorry @JoeHershman I'm a little bit late, but I'm taking the opportunity that I'm preparing a session about authentication in ArcGIS for the DevSummit Europe next month and I have solved this using PKCE and SHA256.

This is the right way to generate the code_verifier and the code_challenge (Node.js code):

 

let crypto;
try {
  crypto = require("crypto");
} catch (err) {
  console.log('Run $npm install first!');
}

function base64URLEncode(str) {
  return str.toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
}
const verifier = base64URLEncode(crypto.randomBytes(32));
console.log("Code verifier: ", verifier);

function sha256(buffer) {
  return crypto.createHash('sha256').update(buffer).digest();
}
const challenge = base64URLEncode(sha256(verifier));
console.log("Code challenge: ", challenge);

 

An example running this would return something like this:

$ node index.js
Code verifier: CMBya5LFXGJktdlm5OL8bIIpVa4LtUAl4ihYCFalQNc
Code challenge: qaXuju2sX8lKLvErIKHfdrg0h7DLvSeLuErfsfMJFj4

This will work. I have created a GitHub repo plus a Postman collection with a flow example (+ sample responses to help anyone implement this workflow):

2022-10-25_11-36-38.png

 

I know this comes late to help you but hopefully it is not late for someone else or at least it is interesting to learn something new  (it has been for me!😁)

Best,
Raul

View solution in original post

0 Kudos
4 Replies
Raul_Jimenez
Esri Contributor

Hi @JoeHershman ,

This is the first time I try this, but I made it work. You can see the postman collection I used here

The first time I tried I got the same error as you got:

 

 

 

{
	"error": {
		"code": 400,
		"error": "invalid_request",
		"error_description": "Invalid PKCE code_challenge_verifier",
		"message": "Invalid PKCE code_challenge_verifier",
		"details": []
	}
}

 

 

 

It was because I was sending the in the "code_verifier" parameter "12345". Reading again the doc again I noticed:

The first step in the PKCE modified workflow is having the application create a code verifier. The code verifier is a cryptographical string using alphanumeric characters (A-Z, a-z, 0-9) and punctuation characters (hyphen, period, underscore, and tilde). A code verifier string should be between 43 to 128 characters long.

So I encoded 12345 with this online tool getting the following hash: "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"

Then I tried again and it worked:

Screenshot 2022-08-02 at 13.00.28.png

I prefer to use Postman to get familiar with any APIs before I start writing code. But I hope this helps.

Cheers!

0 Kudos
JoeHershman
MVP Regular Contributor

I'll check again, I had not noticed that.  The 12345 was just trying to simplify it to get working. 

My original attempts used an SHA256 string created by my app.  I'll look at your example and see if I see anything different from what I did

Thanks,
-Joe
Raul_Jimenez
Esri Contributor

Sorry @JoeHershman I'm a little bit late, but I'm taking the opportunity that I'm preparing a session about authentication in ArcGIS for the DevSummit Europe next month and I have solved this using PKCE and SHA256.

This is the right way to generate the code_verifier and the code_challenge (Node.js code):

 

let crypto;
try {
  crypto = require("crypto");
} catch (err) {
  console.log('Run $npm install first!');
}

function base64URLEncode(str) {
  return str.toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
}
const verifier = base64URLEncode(crypto.randomBytes(32));
console.log("Code verifier: ", verifier);

function sha256(buffer) {
  return crypto.createHash('sha256').update(buffer).digest();
}
const challenge = base64URLEncode(sha256(verifier));
console.log("Code challenge: ", challenge);

 

An example running this would return something like this:

$ node index.js
Code verifier: CMBya5LFXGJktdlm5OL8bIIpVa4LtUAl4ihYCFalQNc
Code challenge: qaXuju2sX8lKLvErIKHfdrg0h7DLvSeLuErfsfMJFj4

This will work. I have created a GitHub repo plus a Postman collection with a flow example (+ sample responses to help anyone implement this workflow):

2022-10-25_11-36-38.png

 

I know this comes late to help you but hopefully it is not late for someone else or at least it is interesting to learn something new  (it has been for me!😁)

Best,
Raul

0 Kudos
JoeHershman
MVP Regular Contributor

My issue was that I was putting S256 not plain when I was generating the authorization code.  I guess I misunderstand what one needs to do for using the S256 setting.  My initial challenge was created with an SH256 encryption (as yours was), but I guess am supposed to encrypt something being sent back when defining as S256

Thanks - Joe

Thanks,
-Joe