Skip to content

Authentication

Keryx uses cookie-based sessions stored in Redis. Authentication is handled through actions and middleware — there's no separate auth plugin or magic. You write a login action, apply SessionMiddleware to protected routes, and the framework handles the rest.

Sessions

When a user logs in, a session is created in Redis and a cookie is set on the response. The session stores arbitrary typed data (like userId) and has a configurable TTL (default: 24 hours).

ts
// Define your session data shape
export type SessionImpl = { userId?: number };

The session initializer manages the full lifecycle:

ts
// Create or update session data
await connection.updateSession({ userId: user.id });

// Access session data
const userId = connection.session?.data.userId;

// Destroy the session (logout)
await api.session.destroy(connection);

Sessions are typed via the generic Connection<T> parameter, giving you type-safe access to session data throughout your middleware and actions.

Login Action

A login action validates credentials and creates a session:

ts
import { SessionMiddleware } from "../middleware/session";
import type { SessionImpl } from "./session";

export class SessionCreate implements Action {
  name = "session:create";
  description = "Sign in with email and password";
  web = { route: "/session", method: HTTP_METHOD.PUT };
  mcp = { enabled: false, isLoginAction: true };
  middleware = [RateLimitMiddleware];
  inputs = z.object({
    email: z
      .string()
      .email()
      .transform((val) => val.toLowerCase()),
    password: secret(z.string().min(8)),
  });

  run = async (
    params: ActionParams<SessionCreate>,
    connection: Connection<SessionImpl>,
  ) => {
    const [user] = await api.db.db
      .select()
      .from(users)
      .where(eq(users.email, params.email));

    if (!user || !(await checkPassword(user, params.password))) {
      throw new TypedError({
        message: "Invalid email or password",
        type: ErrorType.CONNECTION_ACTION_RUN,
      });
    }

    await connection.updateSession({ userId: user.id });
    return { user: serializeUser(user), session: connection.session! };
  };
}

The mcp = { isLoginAction: true } marker tells the OAuth system to use this action during the MCP authorization flow. See MCP for details.

SessionMiddleware

Apply SessionMiddleware to any action that requires authentication:

ts
import { SessionMiddleware } from "../middleware/session";

export class UserEdit implements Action {
  name = "user:edit";
  middleware = [RateLimitMiddleware, SessionMiddleware];
  web = { route: "/user", method: HTTP_METHOD.POST };
  // ...
}

If no valid session exists, SessionMiddleware throws a TypedError with type CONNECTION_SESSION_NOT_FOUND (HTTP 401). The action's run() method is never called.

The middleware checks both that connection.session exists and that connection.session.data.userId is set — so even if a session cookie is present, the user must have actually logged in.

Logout Action

ts
export class SessionDestroy implements Action {
  name = "session:destroy";
  web = { route: "/session", method: HTTP_METHOD.DELETE };
  middleware = [RateLimitMiddleware, SessionMiddleware];

  async run(_params, connection: Connection<SessionImpl>) {
    await api.session.destroy(connection);
    return { success: true };
  }
}

OAuth for MCP Clients

MCP clients authenticate through OAuth 2.1 with PKCE. The OAuth flow renders a browser-based login/signup page that invokes your login and signup actions. Actions tagged with mcp.isLoginAction and mcp.isSignupAction must return an OAuthActionResponse:

ts
// Must return this shape from login/signup actions used in OAuth
type OAuthActionResponse = { user: { id: number } };

The OAuth page dynamically generates form fields from your action's Zod inputs schema — if you change the inputs on your login or signup action, the form updates automatically. Fields wrapped in secret() render as password inputs, and .describe() text is used for labels.

See the MCP guide for the full OAuth flow and endpoint documentation.

Session Configuration

KeyEnv VarDefaultDescription
cookieNameSESSION_COOKIE_NAME"session_id"Cookie name for session tracking
ttlSESSION_TTL86400Session TTL in seconds (1 day)

Cookie security settings (Secure, SameSite, HttpOnly) are configured in the web server config. See the Security guide for production recommendations.

Testing with Sessions

The pattern for testing authenticated endpoints:

ts
// 1. Create a user
await fetch(url + "/api/user", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Test User",
    email: "test@example.com",
    password: "password123",
  }),
});

// 2. Log in to get a session
const sessionRes = await fetch(url + "/api/session", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "test@example.com",
    password: "password123",
  }),
});
const { session } = await sessionRes.json();
const sessionId = session.id;

// 3. Use the session cookie on authenticated requests
const res = await fetch(url + "/api/user", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Cookie: `${config.session.cookieName}=${sessionId}`,
  },
  body: JSON.stringify({ name: "New Name" }),
});

See the Testing guide for more patterns.

Released under the MIT License.