mcpField
E
encore-auth

encore-auth

github.com/encoredevSecurity30835 installsUpdated 2d ago
Repository

About encore-auth

Encore Authentication is a skill for implementing authentication in Encore.ts APIs. It covers auth handlers, endpoint protection, access to auth data, service-to-service propagation, and testing patterns.

Key features

  1. 01Auth handler setup
  2. 02Endpoint authentication controls
  3. 03Access to request auth data
  4. 04Propagation across service calls
  5. 05JWT, API key, and cookie patterns
  6. 06Vitest authentication testing

Use cases

  1. 01Requiring authentication for API endpoints
  2. 02Passing identity through internal service calls
  3. 03Validating JWTs, API keys, or cookies
  4. 04Testing authenticated endpoint behavior

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/encoredev/skills --skill encore-auth

After installing, type/encore-authin Claude Code to confirm it loaded.

Details

From the project README

Encore Authentication

Instructions

Encore.ts provides a built-in authentication system for identifying API callers and protecting endpoints.

1. Create an Auth Handler
// auth.ts
import { Header, Gateway } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";

// Define what the auth handler receives
interface AuthParams {
  authorization: Header<"Authorization">;
}

// Define what authenticated requests will have access to
interface AuthData {
  userID: string;
  email: string;
  role: "admin" | "user";
}

export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    // Validate the token (example with JWT)
    const token = params.authorization.replace("Bearer ", "");
    
    const payload = await verifyToken(token);
    if (!payload) {
      throw APIError.unauthenticated("invalid token");
    }
    
    return {
      userID: payload.sub,
      email: payload.email,
      role: payload.role,
    };
  }
);

// Register the auth handler with a Gateway
export const gateway = new Gateway({
  authHandler: auth,
});
2. Protect Endpoints
import { api } from "encore.dev/api";

// Protected endpoint - requires authentication
export const getProfile = api(
  { method: "GET", path: "/profile", expose: true, auth: true },
  async (): Promise<Profile> => {
    // Only authenticated users reach here
  }
);

// Public endpoint - no authentication required
export const healthCheck = api(
  { method: "GET", path: "/health", expose: true },
  async () => ({ status: "ok" })
);
3. Access Auth Data in Endpoints
import { api } from "encore.dev/api";
import { getAuthData } from "~encore/auth";

export const getProfile = api(
  { method: "GET", path: "/profile", expose: true, auth: true },
  async (): Promise<Profile> => {
    const auth = getAuthData()!;  // Non-null when auth: true
    
    return {
      userID: auth.userID,
      email: auth.email,
      role: auth.role,
    };
  }
);

Auth Handler Behavior

ScenarioHandler ReturnsResult
Valid credentialsAuthData objectRequest authenticated
Invalid credentialsThrows APIError.unauthenticated()Treated as no auth
Other errorThrows other errorRequest aborted

Auth with Endpoints

Endpoint ConfigRequest Has AuthResult
auth: trueYesProceeds with auth data
auth: trueNo401 Unauthenticated
auth: false or omittedYesProceeds (auth data available)
auth: false or omittedNoProceeds (no auth data)

Service-to-Service Auth Propagation

Auth data automatically propagates to internal service calls:

import { user } from "~encore/clients";
import { getAuthData } from "~encore/auth";

export const getOrderWithUser = api(
  { method: "GET", path: "/orders/:id", expose: true, auth: true },
  async ({ id }): Promise<OrderWithUser> => {
    const auth = getAuthData()!;

    // Auth is automatically propagated to this call
    const orderUser = await user.getProfile();

    return { order: await getOrder(id), user: orderUser };
  }
);
Overriding Auth Data

You can explicitly override auth data when making service-to-service calls:

import { user } from "~encore/clients";

// Override auth data for this specific call
const adminUser = await user.getProfile(
  {},
  { authData: { userID: "admin-123", email: "[email protected]", role: "admin" } }
);

Common Auth Patterns

JWT Token Validation
import { jwtVerify } from "jose";
import { secret } from "encore.dev/config";

const jwtSecret = secret("JWTSecret");

async function verifyToken(token: string): Promise<JWTPayload | null> {
  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(jwtSecret())
    );
    return payload;
  } catch {
    return null;
  }
}
API Key Authentication
export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    const apiKey = params.authorization;
    
    const user = await db.queryRow<User>`
      SELECT id, email, role FROM users WHERE api_key = ${apiKey}
    `;
    
    if (!user) {
      throw APIError.unauthenticated("invalid API key");
    }
    
    return {
      userID: user.id,
      email: user.email,
      role: user.role,
    };
  }
);
Cookie-Based Auth
interface AuthParams {
  cookie: Header<"Cookie">;
}

export const auth = authHandler<AuthParams, AuthData>(
  async (params) => {
    const sessionId = parseCookie(params.cookie, "session");
    
    if (!sessionId) {
      throw APIError.unauthenticated("no session");
    }
    
    const session = await getSession(sessionId);
    if (!session || session.expiresAt < new Date()) {
      throw APIError.unauthenticated("session expired");
    }
    
    return {
      userID: session.userID,
      email: session.email,
      role: session.role,
    };
  }
);

Testing with Auth

Mock authentication in tests using Vitest:

import { describe, it, expect, vi } from "vitest";
import * as auth from "~encore/auth";
import { getProfile } from "./api";

describe("authenticated endpoints", () => {
  it("returns profile for authenticated user", async () => {
    // Mock getAuthData to return test user
    const spy = vi.spyOn(auth, "getAuthData");
    spy.mockImplementation(() => ({
      userID: "test-user-123",
      email: "[email protected]",
      role: "user",
    }));

    const profile = await getProfile();
    expect(profile.email).toBe("[email protected]");

    spy.mockRestore();
  });
});

The mock does not reach calls made through ~encore/clients. To call an auth: true endpoint on another service from a test, pass { authData: ... } as the second argument (see "Overriding Auth Data" above).

Guidelines

  • Auth handlers must be registered with a Gateway
  • Use getAuthData() from ~encore/auth to access auth data
  • getAuthData() returns null in unauthenticated requests
  • Auth data propagates automatically in service-to-service calls
  • Throw APIError.unauthenticated() for invalid credentials
  • Keep auth handlers fast - they run on every authenticated request

Comments

Markdown supported