using-clerk
Implement Clerk authentication — C# SDK and React integration, Svix webhook verification, org multi-tenancy.
SKILL.mdpackages/skills/using-clerk/SKILL.md
GitHubClerk 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_KEYorCLERK_PUBLISHABLE_KEYenvironment variable present@clerk/clerk-reactor@clerk/nextjsinpackage.jsondependenciesClerk.BackendAPIorClerk.Netin.csprojPackageReference- 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
// Program.cs
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] // Requires valid Clerk JWT
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 |
// Extension methods for common claim extraction
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 # Backend secret key
CLERK_PUBLISHABLE_KEY=pk_live_xxxxx # Frontend publishable key
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] // Verified via signature, not JWT
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(); // Already processed
// ... process event ...
await _eventStore.MarkProcessedAsync(svixId);
React Frontend Integration
Provider Setup
// Next.js App Router
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en"><body>{children}</body></html>
</ClerkProvider>
);
}
// Non-Next.js React (Vite, CRA, etc.)
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(); // Short-lived JWT, auto-refreshed
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)
{
// Verify active org in JWT matches the requested org
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 |
// CORRECT: Call getToken() before each API request
async function fetchData() {
const token = await getToken(); // Returns cached or refreshed token
return fetch("/api/data", { headers: { Authorization: `Bearer ${token}` } });
}
// WRONG: Storing token for reuse across time-separated calls
const [token] = useState(await getToken()); // Stale in 60s
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
// .NET
builder.Services.AddClerkApiClient(o => o.SecretKey = "sk_...");
builder.Services.AddClerkJwtAuthentication();
app.UseAuthentication();
app.UseAuthorization();
// React
<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.BackendAPINuGet 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 thatClerk.BackendAPIdoes not include. - Webhook verification must use the
Svixlibrary. 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.