Skip to main content
ClerkAuth is the Auth component’s Clerk-backed strategy. It wraps the official @clerk/backend SDK to verify session tokens, resolve the current user, and manage accounts — banning, locking, metadata, sessions, and password sign-in — all behind the framework’s IAuth interface. A companion ClerkAuthMiddleware plugs Clerk into the request pipeline and maps a Clerk user onto the framework IUser, so your controllers read context.user and never depend on Clerk directly.

Why Clerk

  • Token verification built in. getCurrentUser(token) verifies the Clerk session token with your secret key and resolves the user in one call.
  • Drop-in middleware. ClerkAuthMiddleware extracts the bearer token, enforces route roles, and maps the Clerk user onto IUser.
  • Full account management. Ban, lock, update profiles, manage metadata, and revoke sessions through the Clerk Backend API.
  • Password sign-in. signIn() verifies credentials and mints a Clerk sign-in token.
  • Container-managed. Registered with @decorator.auth() and resolved from the container — no manual wiring.

Installation

ClerkAuth ships with @ooneex/auth and depends on the Clerk Backend SDK.
bun add @ooneex/auth @clerk/backend

Environment variables

VariableRequiredPurpose
CLERK_SECRET_KEYYesClerk Backend API secret key. Missing throws AuthException (API_KEY_REQUIRED).
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
The secret key is validated when ClerkAuth is constructed, so a missing key fails fast at startup rather than on the first request.

How it works

ClerkAuthMiddleware runs in the request pipeline. For each request it:
  1. Reads the token from the Authorization: Bearer <token> header (or a bearerToken query).
  2. Checks the route’s roles — if the route is guest-only (no roles or ROLE_GUEST), it lets the request through without a token.
  3. Otherwise calls ClerkAuth.getCurrentUser(token), which verifies the token and loads the Clerk user.
  4. Maps the Clerk user onto the framework IUser and sets it on context.user.
Request → Bearer token
  → ClerkAuthMiddleware: route requires roles?
      → guest-only  → continue, no token needed
      → protected   → verifyToken → getUser → context.user
  → Controller (context.user is available)
When mapping, the middleware pulls the user’s primary email, derives roles and the internal id from Clerk’s privateMetadata (falling back to ROLE_USER), and copies across name, phone, avatar, activity timestamps, and ban/lock flags.

Usage

Register the middleware (and ClerkAuth) with the container, then declare route roles — the middleware enforces them automatically.
import { container } from "@ooneex/container";
import { ClerkAuth } from "@ooneex/auth";

const auth = container.get(ClerkAuth);

// Resolve the current user from a session token
const user = await auth.getCurrentUser(token); // User | null
Protect a route by declaring the roles it requires. The middleware verifies the Clerk token only when roles are present:
import { Route } from "@ooneex/routing";

@Route.get("/api/profile", {
  name: "profile",
  version: 1,
  description: "Get the current user profile",
  roles: ["ROLE_USER"], // requires a valid Clerk token
})
export class ProfileController {
  public async index(context: ContextType<ProfileRouteType>) {
    const { user } = context; // mapped from Clerk by the middleware
    return context.response.json({
      id: user.id,
      email: user.email,
      roles: user.roles,
    });
  }
}

Password sign-in

signIn() looks up the user by email, verifies the password through Clerk, and returns the user plus a sign-in token (default TTL 30 days):
const { user, token } = await auth.signIn({
  email: "ada@example.com",
  password: "s3cret",
  ttl: 2_592_000, // optional, seconds
});

await auth.signOut(sessionId); // revokes the Clerk session

Account management

ClerkAuth exposes the Clerk user and session APIs directly:
MethodPurpose
getCurrentUser(token)Verify a token and resolve the user, or null.
getUser(userId)Fetch a user by id.
banUser(userId) / unbanUser(userId)Ban or unban a user.
isBanned(userId)Whether the user is banned.
lockUser(userId) / unlockUser(userId)Lock or unlock a user.
isLocked(userId)Whether the user is locked.
updateUser(userId, params)Update user attributes.
updateUserProfileImage(userId, { file })Replace the profile image.
updateUserMetadata(userId, params)Update public/private/unsafe metadata.
getUserMetadata(userId)Read public/private/unsafe metadata.
deleteUser(userId)Delete a user.
deleteUserProfileImage(userId)Remove the profile image.
getSession(sessionId)Fetch a session.
getCurrentUserSession(token)Resolve the session for a token, or null.
signIn({ email, password, ttl? })Verify credentials and mint a sign-in token.
signOut(sessionId)Revoke a session.

Use in the app

In an @ooneex/app application, Clerk plugs in as a request middleware. Add ClerkAuthMiddleware to the middlewares slot of your App config — importing it registers both ClerkAuthMiddleware and ClerkAuth with the container through @decorator.auth(), so there is nothing else to wire.
bun add @ooneex/app @ooneex/auth @clerk/backend
import { App } from "@ooneex/app";
import { ClerkAuthMiddleware } from "@ooneex/auth";
import { TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
  middlewares: [ClerkAuthMiddleware], // runs on every matched route
});

await app.run();
Once registered, the middleware runs on each matched route: it reads the bearer token, enforces the route’s roles, and maps the Clerk user onto context.user. Controllers then read context.user and declare the roles they require — see Protecting routes above. Set CLERK_SECRET_KEY in your .env.yml (or environment) so the strategy can verify tokens.

Exceptions

ClerkAuth and ClerkAuthMiddleware throw AuthException with a machine-readable key, so callers can branch on it.
KeyWhen
API_KEY_REQUIREDClerkAuth is constructed without CLERK_SECRET_KEY.
INVALID_CREDENTIALSsignIn() is given an unknown email or a wrong password.
MISSING_BEARER_TOKENA protected route is requested without a token.
INVALID_TOKENThe token verifies to no user.
NO_PRIMARY_EMAILThe Clerk user has no primary email address.
import { AuthException } from "@ooneex/auth";

try {
  const { user, token } = await auth.signIn({ email, password });
  return context.response.json({ user, token });
} catch (error) {
  if (error instanceof AuthException && error.key === "INVALID_CREDENTIALS") {
    return context.response
      .status(401)
      .json({ error: "Invalid email or password" });
  }
  throw error;
}

Best practices

  • Keep CLERK_SECRET_KEY in the environment. Never hard-code it; load it from .env so it stays out of source control.
  • Store roles and the internal id in Clerk metadata. The middleware reads privateMetadata.roles and privateMetadata.externalId — set them so users map onto IUser correctly.
  • Read context.user, not Clerk. Keep controllers provider-agnostic so the auth strategy can change without touching handlers.
  • Drive access from route roles. Declare roles on routes and let ClerkAuthMiddleware enforce them instead of re-checking tokens in controllers.
  • Branch on AuthException.key. Return 401 for INVALID_CREDENTIALS, MISSING_BEARER_TOKEN, and INVALID_TOKEN.
See the Auth component for the strategy interface and route-protection model.