Skip to content

Simulate a browser doing authorization code flow with PKCE using C#

Simulate a browser doing authorization code flow with PKCE using C#

Let’s say you need to get an access token from an OAuth 2.0 server using the authorization code flow with PKCE (Proof Key for Code Exchange). A very common scenario when you’re building a SPA. But it becomes a bit less common when you want to do this from a C# back-end application.

You might think, why would you ever want to do this, just use the client credentials flow and be done with it. Well, sometimes a product or service does not have an API for the outside world, but you do want to integrate with it. I’ve recently found myself in this situation where I wanted to automatically download invoices every month to make my life a bit more comfortable.

Unfortunately the service did not expose an API for external developers, but it does have a web site. And the website is a SPA that uses an API which requires an access token to interact with it. This gives us an integration opportunity so let’s hack together a solution.

The API uses Auth0 as an identity provider, so we’ll use their SPA quick start to test on.

Authorization code flow with PKCE

First we need to take a look at the OAuth 2.0 flow that the SPA uses: the authorization code flow with PKCE. This flow is designed for public clients that cannot keep secrets. It looks like this:
Sequence diagram authorization code flow with PKCE (Image shamelessly taken from the Auth0 docs)

Since we don’t have a user we can skip the first step and start by generating the code verifier and code challenge:

var randomGen = RandomNumberGenerator.Create();

var bytes = new byte[32];
var codeVerifier = Base64UrlEncode(Convert.ToBase64String(bytes));

var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = Base64UrlEncode(Convert.ToBase64String(challengeBytes));

private static string Base64UrlEncode(string input) => input
  .Replace('+', '-')
  .Replace('/', '_')
  .Replace("=", "");

Now that we have the code verifier and code challenge we can initiate the authorization code flow:

const string REDIRECT_URL = "http://localhost:4200/";
const string AUTHORIZE_URL = "";

const string SCOPE = "openid+profile+email";

using var httpClient = new HttpClient();

var clientId = "<our SPA client id>";

var authnUri = AUTHORIZE_URL +
  $"?client_id={clientId}" +
  $"&code_challenge={codeChallenge}" +
  "&code_challenge_method=S256" +
  $"&redirect_uri={Uri.EscapeDataString(REDIRECT_URL)}" +
  "&response_type=code" +
  $"&state={Guid.NewGuid()}" +

var authnResponse = await httpClient.GetAsync(authnUri);
var responseString = await authnResponse.Content.ReadAsStringAsync();

// extract state input value which is used to prevent CSRF
var pattern = @"<input\s+type=""hidden""\s+name=""state""\s+value=""(.*?)""\s*/>";
var match = Regex.Match(responseString, pattern);
if (!match.Success)
    throw new Exception("Hidden input for state not found in response html.");

var state = match.Groups[1].Value;

The authorization code is sent to the Auth0 tenant authorize endpoint using a get request. The server responds with a login page that has a form with a username and password field: Auth0 login form We’re not interested in most of the page, we’re just going to post to the same endpoint as the login form does. However, in order to do this we need to extract a hidden input containing state from the form. This is used to prevent CSRF attacks and needs to be sent back to the server in the login post request:

const string LOGIN_URL = "";

var username = "our username";
var password = "our password";

var loginUrl = LOGIN_URL + $"?state={state}";
var authnPayload = new FormUrlEncodedContent(new Dictionary<string, string>
    ["state"] = state,
    ["username"] = username,
    ["password"] = password,
    ["action"] = "default"

var postResponse = await httpClient.PostAsync(loginUrl, authnPayload);
// HttpClient by default follows redirects, but in this example
// it's redirected from HTTPS to HTTP (our localhost SPA) and in that case redirects are not followed.
if (postResponse.StatusCode != HttpStatusCode.Redirect)
    throw new Exception("Unable to get redirect URL: response status code was not a 302.");

var code = ExtractCodeFromUriQueryParameter(postResponse.Headers.Location);

private static string ExtractCodeFromUriQueryParameter(Uri? location)
    var codeParameter = location?.Query
        .FirstOrDefault(param => param.StartsWith("code="));

    return codeParameter?.Remove(0, "code=".Length)
        ?? throw new Exception("Response did not return a code.");

We assume here that the account does not use any social account to login but just a username and password, without 2FA. If it would, we’d need to write some more code to handle that, but for now that’s out of scope.

If the login attempt was successful Auth0 will redirect the HttpClient to our redirect URL. In the URL there will be a query parameter with the code we can use to get the access token later.
In the demo here we’re redirected from HTTPS to HTTP and in that case HttpClient does not follow the redirect automatically. This is convenient because now we can fetch the code from the location URL in the headers. In a real world scenario we’d probably be redirected to a different HTTPS URL and we’d need to disable auto redirects. To do this, we just need to set the AllowAutoRedirect property of the HttpClientHandler to false:

ar httpClientHandler = new HttpClientHandler
    AllowAutoRedirect = false,

using var httpClient = new HttpClient(httpClientHandler);

So now that we have the code we can send it, together with the code_challenge we generated earlier, to the token endpoint to get the access token:

const string TOKEN_URL = "";

var tokenPayload = new StringContent(
    $"client_id={clientId}" +
    $"&redirect_uri={Uri.EscapeDataString(REDIRECT_URL)}" +
    "&grant_type=authorization_code" +
    $"&code_verifier={codeVerifier}" +

var tokenResponse = await httpClient.PostAsync(TOKEN_URL, tokenPayload);

var tokenResponseDeserialized = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>()
    ?? throw new Exception("Unable to get token: invalid response");

public record TokenResponse(
    [property: JsonPropertyName("token_type")] string TokenType,
    [property: JsonPropertyName("expires_in")] int ExpiresIn,
    [property: JsonPropertyName("access_token")] string AccessToken,
    [property: JsonPropertyName("id_token")] string IdToken,
    [property: JsonPropertyName("scope")] string Scope);

And if all is well we’ve now received our access token that we can use to call the API. That would just be a matter of checking out the requests the SPA makes to the API and replicating them in our code.


The full code can be found here.