11

I cannot get IdentityServer4 PKCE authorization to work using Postman.

Using online tools I create the necessary parts:

Choose a random string:

1234567890

Get its SHA-256 hash:

c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646

Base64 encode the hash to get the code challenge:

Yzc3NWU3Yjc1N2VkZTYzMGNkMGFhMTExM2JkMTAyNjYxYWIzODgyOWNhNTJhNjQyMmFiNzgyODYyZjI2ODY0Ng==

In the browser I navigate to the following URL, fill in my credentials and retrieve the code from the fragmented redirect URL.

GET https://localhost:5000/connect/authorize
?client_id=pkceclient
&scope=openid
&response_type=code
&redirect_uri=https://jwt.ms
&state=abc
&nonce=xyz  
&code_challenge=Yzc3NWU3Yjc1N2VkZTYzMGNkMGFhMTExM2JkMTAyNjYxYWIzODgyOWNhNTJhNjQyMmFiNzgyODYyZjI2ODY0Ng==
&code_challenge_method=S256

When redeeming the code for a token I pass the code_verifier (SHA-256 hash) but my IdentityServer logs the following error:

"Transformed code verifier does not match code challenge".

POST https://localhost:5000/connect/token
client_id=pkceclient
grant_type=authorization_code
code:-CesrmjPYjdLdDd5AviOZpR6GdjjkZia_ZapoJdGUZI
redirect_uri=https://jwt.ms
code_verifier=c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646

In his blog post, the author uses the following code to generate the parts.

var verifier = CryptoRandom.CreateRandomKeyString(64);
var challenge = verifier.ToCodeChallenge();

but I cannot find the code in the repositories for the ToCodeChallenge method.

Why doesn't my manually generated challenge match the one used in the verification process, what am I missing?

jhhwilliams
  • 2,297
  • 1
  • 22
  • 26

4 Answers4

21

While putting this question together I came across the specification document for PKCE and found the following line:

code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

It turns out the ASCII part is not carried out by the online tools that I used.

Implementing the steps in code I get the following which, when substituting the values from before, passes the verification in the second step of the process.

var codeVerifier = "c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646";
var codeVerifierBytes = Encoding.ASCII.GetBytes(codeVerifier);
var hashedBytes = codeVerifierBytes.Sha256();
var transformedCodeVerifier = Base64Url.Encode(hashedBytes);

code_challenge: 51FaJvQFsiNdiFWIq2EMWUKeAqD47dqU_cHzJpfHl-Q

code_verifier: c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646

Community
  • 1
  • 1
jhhwilliams
  • 2,297
  • 1
  • 22
  • 26
  • 1
    Thank you for updating your own question with a solution. Saved me a ton of time. – datoml Dec 10 '19 at 12:10
  • @jhhwilliams Can you elaborate when you say **"the online tools that I used"**? As I understand it, the code_challenge is created by the client. Are you saying that IdentityServer4 on the server was not correctly transforming the code_verifier? – Ross Brasseaux Aug 16 '21 at 20:36
  • @Lopsided, while developing our Identity feature I used Postman to figure out and test the IdentityServer4 flows. To generate the code_challenge, I used an _"online-tool"_ to hash and base64 encode my `1234567890` string and this is where the ASCII conversion was missing. – jhhwilliams Aug 17 '21 at 05:42
1

Following links helps to achieve the PKCE-AuthZ-Code flow.

https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce

https://github.com/gilbert-fernandes/S256Code/blob/master/src/S256Code.java

Zeigeist
  • 3,755
  • 3
  • 20
  • 22
1

Here is a slight improvement for the selected answer, without the need for the Sha256() extension method (credit).

code_verifier random generator (for the /connect/token endpoint):

private string GenerateCodeVerifier()
{
    var rng = RandomNumberGenerator.Create();

    var bytes = new byte[32];
    rng.GetBytes(bytes);

    // It is recommended to use a URL-safe string as code_verifier.
    // See section 4 of RFC 7636 for more details.
    var code_verifier = Convert.ToBase64String(bytes)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');

    return code_verifier;
}

code_challenge generator based on a code_verifier (for the /connect/authorize endpoint):

private string GenerateCodeChallenge(string code_verifier)
{
    var code_challenge = string.Empty;
    using (var sha256 = SHA256.Create())
    {
        var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(code_verifier));
        code_challenge = Convert.ToBase64String(challengeBytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

        return code_challenge;
    }
}

Usage:

using System;
using System.Security.Cryptography;
using System.Text;

[TestMethod]
public void CodesTest()
{
    string code_verifier = GenerateCodeVerifier();
    Console.WriteLine("code_verifier:");
    Console.WriteLine(code_verifier);

    string code_challenge = GenerateCodeChallenge(code_verifier);
    Console.WriteLine("code_challenge:");
    Console.WriteLine(code_challenge);
}

Will output:

code_verifier:
3t1_Ve6NezEoLtj-7GKAWuXOOEUXe0z9Bd-uKoZeBnE
code_challenge:
cmcJe_eAcSGnEema7PXUEDZZOSofeaUDhKJC5P--uOY

Additional info in my article

Shahar Shokrani
  • 7,598
  • 9
  • 48
  • 91
1

This had me stumped for a while, because i thought the rfc mentioned base64 encoding. It is easy to overlook, but it actually says BASE64URL-ENCODE and not just BASE64 ;)

BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge

The PKCE rfc document even contains a c# code sample to do the encoding:

https://www.rfc-editor.org/rfc/rfc7636#appendix-A

static string base64urlencode(byte [] arg)
 {
   string s = Convert.ToBase64String(arg); // Regular base64 encoder
   s = s.Split('=')[0]; // Remove any trailing '='s
   s = s.Replace('+', '-'); // 62nd char of encoding
   s = s.Replace('/', '_'); // 63rd char of encoding
   return s;
 }
Davy
  • 6,295
  • 5
  • 27
  • 38