| name | using-clerk |
| description | Clerk authentication with official C# SDK, React integration, webhook verification via Svix, and organization multi-tenancy. Use when implementing auth with Clerk in .NET or React applications. |
Clerk Authentication Skill
Version: 1.0.0 | Target: <500 lines | Purpose: Fast reference for Clerk auth integration
When to Use
Load this skill when:
CLERK_SECRET_KEY or CLERK_PUBLISHABLE_KEY environment variable present
@clerk/clerk-react or @clerk/nextjs in package.json dependencies
Clerk.BackendAPI or Clerk.Net in .csproj PackageReference
- User mentions "Clerk", "Clerk auth", or Clerk-based authentication
SDK Overview
Two .NET SDK options exist for Clerk. Choose based on your use case.
Official SDK: Clerk.BackendAPI
- NuGet:
Clerk.BackendAPI (Jan 2025, beta, auto-generated from OpenAPI spec)
- Best for: Direct Clerk API calls (user management, organization CRUD, invitations)
- Limitation: No built-in ASP.NET Core middleware for JWT validation
var sdk = new ClerkBackendApi(bearerAuth: Environment.GetEnvironmentVariable("CLERK_SECRET_KEY"));
var response = await sdk.Users.GetAsync("user_2abc123");
Community SDK: Clerk.Net (Hawxy)
- NuGet:
Clerk.Net (community-maintained, mature)
- Best for: ASP.NET Core middleware, JWT authentication, request validation
When to Choose Which
| Use Case | Recommended SDK |
|---|
ASP.NET Core JWT middleware / [Authorize] | Clerk.Net |
| Creating/updating users or orgs via API | Clerk.BackendAPI |
| Both middleware + API calls | Both (they coexist) |
.NET Backend Integration
Use Clerk.Net for request authentication middleware in ASP.NET Core.
Service Registration
using Clerk.Net.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddClerkApiClient(options =>
{
options.SecretKey = builder.Configuration["Clerk:SecretKey"]!;
});
builder.Services.AddClerkJwtAuthentication();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Protecting Endpoints
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProfileController : ControllerBase
{
[HttpGet]
public IActionResult GetProfile()
{
var userId = User.FindFirst("sub")?.Value;
if (userId is null) return Unauthorized();
return Ok(new { UserId = userId });
}
}
JWT Claims Reference
| Claim | Description | Example |
|---|
sub | Clerk user ID | user_2abc123 |
azp | Authorized party (app origin) | https://myapp.com |
org_id | Active organization ID (if set) | org_2def456 |
org_role | Role in active organization | org:admin |
org_slug | Organization slug | acme-corp |
public static class ClerkClaimsExtensions
{
public static string? GetClerkUserId(this ClaimsPrincipal user)
=> user.FindFirst("sub")?.Value;
public static string? GetActiveOrgId(this ClaimsPrincipal user)
=> user.FindFirst("org_id")?.Value;
public static string? GetOrgRole(this ClaimsPrincipal user)
=> user.FindFirst("org_role")?.Value;
}
Environment Variables
CLERK_SECRET_KEY=sk_live_xxxxx
CLERK_PUBLISHABLE_KEY=pk_live_xxxxx
Webhook Handling
CRITICAL: Use the Svix library for webhook verification. Do NOT implement custom HMAC. Clerk uses Svix infrastructure and the Svix.Webhook class handles signature validation, timestamp tolerance, and replay prevention.
<PackageReference Include="Svix" Version="1.*" />
Webhook Endpoint
[ApiController]
[Route("api/webhooks")]
public class ClerkWebhookController : ControllerBase
{
private readonly string _webhookSecret;
public ClerkWebhookController(IConfiguration config)
{
_webhookSecret = config["Clerk:WebhookSecret"]
?? throw new InvalidOperationException("Clerk:WebhookSecret is required");
}
[HttpPost("clerk")]
[AllowAnonymous]
public async Task<IActionResult> HandleWebhook()
{
var body = await new StreamReader(Request.Body).ReadToEndAsync();
var headers = new Dictionary<string, string>
{
["svix-id"] = Request.Headers["svix-id"].ToString(),
["svix-timestamp"] = Request.Headers["svix-timestamp"].ToString(),
["svix-signature"] = Request.Headers["svix-signature"].ToString(),
};
try
{
new Webhook(_webhookSecret).Verify(body, headers);
}
catch (WebhookVerificationException)
{
return Unauthorized("Invalid webhook signature");
}
var payload = JsonSerializer.Deserialize<JsonElement>(body);
var eventType = payload.GetProperty("type").GetString();
switch (eventType)
{
case "user.created":
await HandleUserCreated(payload.GetProperty("data"));
break;
case "user.updated":
await HandleUserUpdated(payload.GetProperty("data"));
break;
case "user.deleted":
await HandleUserDeleted(payload.GetProperty("data"));
break;
case "organization.created":
await HandleOrgCreated(payload.GetProperty("data"));
break;
}
return Ok();
}
}
Common Webhook Event Types
| Event | Trigger |
|---|
user.created | New user signs up |
user.updated | Profile or metadata changes |
user.deleted | Account deleted |
session.created | New session started |
session.ended | Session terminated |
organization.created | New organization created |
organizationMembership.created | User added to organization |
organizationMembership.deleted | User removed from organization |
Idempotency
Use the svix-id header to deduplicate webhook deliveries:
var svixId = Request.Headers["svix-id"].ToString();
if (await _eventStore.ExistsAsync(svixId))
return Ok();
await _eventStore.MarkProcessedAsync(svixId);
React Frontend Integration
Provider Setup
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en"><body>{children}</body></html>
</ClerkProvider>
);
}
import { ClerkProvider } from "@clerk/clerk-react";
function App() {
return (
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
<Router />
</ClerkProvider>
);
}
Pre-Built Components and Protected Routes
import { SignedIn, SignedOut, UserButton, RedirectToSignIn } from "@clerk/clerk-react";
function Header() {
return (
<header>
<SignedIn><UserButton afterSignOutUrl="/" /></SignedIn>
<SignedOut><a href="/sign-in">Sign In</a></SignedOut>
</header>
);
}
function ProtectedPage() {
return (
<>
<SignedIn><Dashboard /></SignedIn>
<SignedOut><RedirectToSignIn /></SignedOut>
</>
);
}
Hooks
import { useUser, useAuth } from "@clerk/clerk-react";
function Profile() {
const { user, isLoaded, isSignedIn } = useUser();
const { getToken } = useAuth();
if (!isLoaded) return <div>Loading...</div>;
if (!isSignedIn) return <div>Not signed in</div>;
const callApi = async () => {
const token = await getToken();
return fetch("/api/profile", {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
};
return (
<div>
<p>Hello, {user.firstName}</p>
<button onClick={callApi}>Fetch Profile</button>
</div>
);
}
Organization Multi-Tenancy
Frontend: Organization Switcher
import { OrganizationSwitcher, useOrganization } from "@clerk/clerk-react";
function OrgSwitcher() {
return <OrganizationSwitcher hidePersonal={false} afterSelectOrganizationUrl="/dashboard" />;
}
function OrgDashboard() {
const { organization, membership, isLoaded } = useOrganization();
if (!isLoaded) return <div>Loading...</div>;
if (!organization) return <div>Select an organization</div>;
return (
<div>
<h1>{organization.name}</h1>
<p>Your role: {membership?.role}</p>
</div>
);
}
Passing org_id in API Calls
function OrgApiCaller() {
const { getToken, orgId } = useAuth();
const fetchOrgData = async () => {
const token = await getToken();
return fetch(`/api/orgs/${orgId}/data`, {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
};
return <button onClick={fetchOrgData}>Load Org Data</button>;
}
Backend: Tenant Resolution from JWT
[ApiController]
[Route("api/orgs/{orgId}/data")]
[Authorize]
public class OrgDataController : ControllerBase
{
[HttpGet]
public IActionResult GetOrgData(string orgId)
{
var tokenOrgId = User.FindFirst("org_id")?.Value;
if (tokenOrgId != orgId) return Forbid();
var orgRole = User.FindFirst("org_role")?.Value;
return Ok(new { OrgId = orgId, Role = orgRole });
}
}
Session Management
CRITICAL: Clerk session tokens (JWTs) have a default expiry of 60 seconds. The frontend SDK handles automatic refresh transparently. Do not cache tokens beyond a single API call.
| Token Type | Lifetime | Use Case |
|---|
| Session JWT | 60 seconds (default) | API calls from frontend |
| Long-lived API key | Configurable | Server-to-server communication |
CLERK_SECRET_KEY | Permanent (until rotated) | Backend SDK authentication |
async function fetchData() {
const token = await getToken();
return fetch("/api/data", { headers: { Authorization: `Bearer ${token}` } });
}
const [token] = useState(await getToken());
Next.js Middleware
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)",
]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) await auth.protect();
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};
Quick Reference Card
Key Packages
| Package | Platform | Purpose |
|---|
Clerk.Net | NuGet | ASP.NET Core JWT middleware |
Clerk.BackendAPI | NuGet | Clerk API client (official, beta) |
Svix | NuGet | Webhook signature verification |
@clerk/clerk-react | npm | React hooks and components |
@clerk/nextjs | npm | Next.js integration (middleware, SSR) |
Environment Variables
| Variable | Where | Purpose |
|---|
CLERK_SECRET_KEY | Backend | Authenticate SDK requests |
CLERK_PUBLISHABLE_KEY | Frontend | Initialize ClerkProvider |
CLERK_WEBHOOK_SECRET | Backend | Verify webhook signatures |
Minimal Setup Snippets
builder.Services.AddClerkApiClient(o => o.SecretKey = "sk_...");
builder.Services.AddClerkJwtAuthentication();
app.UseAuthentication();
app.UseAuthorization();
<ClerkProvider publishableKey="pk_...">
<SignedIn><App /></SignedIn>
<SignedOut><RedirectToSignIn /></SignedOut>
</ClerkProvider>
Key Base-Model Corrections
These facts correct common base-model hallucinations about Clerk:
- The official
Clerk.BackendAPI NuGet package exists (released Jan 2025, beta). It is auto-generated from the OpenAPI spec, not hand-crafted.
- The community
Clerk.Net (by Hawxy) is more mature and provides ASP.NET Core middleware that Clerk.BackendAPI does not include.
- Webhook verification must use the
Svix library. Do not implement custom HMAC -- Clerk uses Svix infrastructure and the signature format is Svix-specific.
- Default JWT expiry is 60 seconds with automatic refresh on the frontend. Do not cache tokens across time-separated calls.