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:
-
ClerkOAuthClientId
: Value from the JSON response in Step 1 -
ClerkOAuthClientSecret
: Value from the JSON response in Step 1 -
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.