Using Clerk as an OAuth Provider for ASP.NET Core

I’ve been using Clerk on the frontend for a few applications and recently came across the need to authenticate on the server side too. This guide will show you the necessary steps to configure Clerk and ASP.NET to enable authenticating your Clerk users.

Update Oct 2023: Removed open_id scope as it is not accepted anymore by Clerk and modified the sign-out workflow to show revoking the session with Clerk before signing out the user.

Update Jul 2024: Instead of revoking just the first session on Sign out, code now revokes all active sessions.

Step 1. Create the OAuth application in Clerk

The endpoint is described here: https://clerk.com/docs/reference/backend-api/tag/OAuth-Applications#operation/CreateOAuthApplication

The Bearer token required is the Client Secret of your Clerk instance (Dashboard > API Keys > Secret keys).

Note: Don’t try to use the built-in “Try it” function on that page as the client secret will be missing from the response.

The easiest way is to use cURL (if you’re on Windows and have Git installed, just start Git Bash and run the following command):

curl -X POST https://api.clerk.com/v1/oauth_applications -H "Authorization: Bearer <replace-this-with-client-secret>" -H "Content-Type: application/json" -d '{"callback_url":"https://localhost:7052/oauth/callback", "name": "aspnet"}'

Remember to replace your client secret and the callback url. The callback url’s path (/oauth/callback) will be set below, but make sure for your localhost environment you are setting the right HTTPS port. For production environment obviously change that to your public domain.

The response will be a JSON containing two important fields: client_id and client_secret.

Note: the client_secret for OAuth is not the same as the Clerk Backend Secret Key. For OAuth you must use the client_secret returned from this endpoint in the next step.

Step 2. Configure ASP.NET

First off, add three settings to your appsettings.json:

  1. ClerkOAuthClientId: Value from the JSON response in Step 1

  2. ClerkOAuthClientSecret: Value from the JSON response in Step 1

  3. ClerkDomain: check the json above for the domain part of one of the URLs (usually https://something.clerk.accounts.dev), add just the https and domain without a trailing slash.

In Program.cs (if you are using Startup.cs, change the references to Builder) in the Service configuration block add this code:

builder.Services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme =
      CookieAuthenticationDefaults.AuthenticationScheme;
  options.DefaultSignInScheme =
      CookieAuthenticationDefaults.AuthenticationScheme;

  // use Clerk as default to sign in
  options.DefaultChallengeScheme = "Clerk";
})
.AddCookie()
.AddOAuth("Clerk", options =>
{
  var clerkDomain =
      builder.Configuration.GetValue<string>("ClerkDomain");

  options.AuthorizationEndpoint =
      $"{clerkDomain}/oauth/authorize";

  options.Scope.Add("profile");
  options.Scope.Add("email");

  options.CallbackPath = new PathString("/oauth/callback");

  options.ClientId = builder.Configuration.GetValue<string>(
      "ClerkOAuthClientId");
  options.ClientSecret =
      builder.Configuration.GetValue<string>(
          "ClerkOAuthClientSecret");
  options.TokenEndpoint = $"{clerkDomain}/oauth/token";

  options.UserInformationEndpoint =
      $"{clerkDomain}/oauth/userinfo";

  options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier,
                                  "user_id");
  options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
  options.ClaimActions.MapJsonKey(ClaimTypes.Email,
                                  "email");

  options.Events = new OAuthEvents
  {

      OnCreatingTicket = async context => {

          var request = new HttpRequestMessage(
              HttpMethod.Get,
              context.Options.UserInformationEndpoint);

          request.Headers.Accept.Add(
              new MediaTypeWithQualityHeaderValue(
                  "application/json"));

          request.Headers.Authorization =
              new AuthenticationHeaderValue(
                  "Bearer",
                  context.AccessToken);

          var response = await context.Backchannel.SendAsync(
                     request,
                     HttpCompletionOption.ResponseHeadersRead,
                     context.HttpContext.RequestAborted);

          response.EnsureSuccessStatusCode();

          var json = await response.Content.ReadAsStringAsync();

          context.RunClaimActions(
                     JsonDocument.Parse(json).RootElement);
      }
  };
});

This will add Clerk as the default authentication and map the fields retrieved from the userinfo endpoint to the ASP.NET User object.

Then find the app.UseRouting() and beneath that add

app.UseAuthentication();
app.UseAuthorization();

Step 3. Add Sign In

You can use the standard ASP.NET Authorization on a controller.

Example:

[Authorize]
public class AuthController : Controller
{
    [Route("/login")]
    public IActionResult Index()
    {
        return Content(User.Identity.Name);
    }

}

And that’s it. Navigate to /login to sign-in, you will be redirected to Clerk and then presented with your name (if you have configured one, otherwise it will be empty, then just add a breakpoint and check the claims in the User object).

Note: if you get a bad request error message, double check you are using the correct client_id and client_secret from the Endpoint in Step1, and double-check you have configured the right callback_url in the initial request (it should be https://localhost:12345/oauth/callback, where 12345 is the port you are using). If you made a mistake, you can just issue a new request as in Step 1 and will receive a new client_id and client_secret.

Step 4. Add Sign Out

Signing-out is not as straight forward as the sign-in because you need to both:

  • Sign-out from ASP.NET and

  • Sign-out out from Clerk (revoke the user’s session).

Otherwise your users will simply click sign-out on your side, be redirected to Clerk, automatically sign-ed and redirected to your app, triggering the OAuth sign-in flow again and never actually be signed-out of your app.

Add a second route to the AuthController.

[Route("/logout")]
public async Task<IActionResult> LogoutAsync() {
  using(var c = new HttpClient()) {
    c.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(
            "Bearer", CLERK_SECRET_KEY);

    var clerkUserId = User.Claims.FirstOrDefault(
        x => x.Type == ClaimTypes.NameIdentifier)
        ?.Value;

    // Get the sessions for the current user
    var response = await c.GetAsync(
        "https://api.clerk.com/v1/sessions?" +
        "user_id=" + clerkUserId +
        "&status=active");

    if (response.IsSuccessStatusCode) {
      var responseString =
          await response.Content
              .ReadAsStringAsync();

      // Get the active sessions for the current
      // user
      var sessions =
          JsonConvert.DeserializeObject<
              Dictionary<string, object>[]>(
              responseString);

      // Revoke each sessions
      foreach (var session in sessions) {
        try {
          var sessionId = session ["id"]
                              .ToString();
          await c.PostAsJsonAsync(
          $"https://api.clerk.com/v1/sessions/{sessionId}/revoke",
          new {});

        } catch (Exception ex) {
          m_logger.LogError(
              ex, "Failed to revoke sesssion");
        }
      }
    }
  }

  await HttpContext.SignOutAsync();

  return Redirect(Url.Content("~/"));
}

Replace the CLERK_SECRET_KEY with the secret value from your Clerk dashboard.