Skip to content

Migrating Plugins to Session-Based Auth ​

This is part 2 of the core 0.1.x β†’ 0.2.x upgrade (session auth). It covers migrating existing racletteJS plugins from the legacy JWT-based authentication (req.user) to the new session-based system (req.authUser).

Related

Part 1: App upgrade (0.1.x β†’ 0.2.x) β€” packages, raclette.config, env, landing compositions. Finish that first, then return here for plugin route handlers.

When This Is Required ​

This migration is required for all plugins that:

  • Access req.user in route handlers
  • Use fastify.jwt.sign or fastify.jwt.verify for HTTP authentication
  • Import from modules/authentication

Breaking Changes ​

BeforeAfter
req.user._idreq.authUser!.userId
req.user.isAdminreq.authUser!.isAdmin
req.user.accountFetch manually if needed
req.user.emailFetch manually if needed
fastify.jwt.sign(payload)Not available for HTTP (use session)
import { authenticate } from "@m/authentication"Use fastify.authenticate preHandler

Migration Steps ​

1. Replace req.user._id with req.authUser!.userId ​

The most common usage in plugins is accessing the user's ID for audit fields:

typescript
// Before
const handler = async (req, reply) => {
  const result = await service.create(req.body, req.user._id)
  return result
}

// After
const handler = async (req, reply) => {
  const result = await service.create(req.body, req.authUser!.userId)
  return result
}

TIP

The ! non-null assertion is safe in routes protected by fastify.authenticate, which guarantees req.authUser is set before the handler runs.

2. Replace req.user.isAdmin with req.authUser!.isAdmin ​

typescript
// Before
if (!req.user.isAdmin) {
  throw new ForbiddenError("Admin only")
}

// After
if (!req.authUser!.isAdmin) {
  throw new ForbiddenError("Admin only")
}

Or use the new permissions API:

typescript
// Even better -- use req.can()
const handler = async (req, reply) => {
  if (!(await req.can("user.isAdmin"))) {
    return reply.status(403).send({ message: "Admin only" })
  }
  // ...
}

3. Fetching Full User Data (When Needed) ​

If your route needs more than just the userId and isAdmin flag (e.g., user's email, name, avatar):

typescript
import mongoose from "mongoose"

const handler = async (req, reply) => {
  const user = await mongoose
    .model("raclette__core-user")
    .findById(req.authUser!.userId)
    .select("-password -resetToken")
    .lean()

  if (!user) {
    return reply.status(404).send({ message: "User not found" })
  }

  // Now use user.email, user.name, etc.
}

WARNING

Most routes only need userId for audit fields (owner, updatedBy). Avoid fetching the full user document unless you actually need the data β€” it adds an unnecessary database query.

4. Update Route Configuration ​

Route preHandlers remain the same:

typescript
// This still works β€” no changes needed
export default (fastify) => {
  return {
    handler,
    onRequest: [fastify.authenticate],
    // or
    onRequest: [fastify.authenticate, fastify.checkPermissions("user.isAdmin")],
  }
}

5. Work Sessions ​

Work session handling is unchanged:

typescript
// Still works the same way
export default (fastify) => {
  return {
    handler,
    onRequest: [fastify.authenticatedOrWorksession("your-plugin-key")],
  }
}

req.workSession and req.isWorkSession are still set by the middleware when a valid work session header is present.

6. Remove JWT Imports ​

If your plugin imported from the old authentication module:

typescript
// Before β€” remove this
import { authenticate } from "@m/authentication"

// After β€” use the Fastify decorator instead
// No import needed, just use fastify.authenticate in route config

7. Asset Downloads ​

If you previously minted JWT-backed download URLs, switch to session asset tokens.

  1. Register a resolver with fastify.auth.registerAssetResolver (authorize against tokenData.sessionUserId).
  2. Mint with mintAssetToken / mintSessionAssetToken, or from any widget via raclette__core $data.assetLink.createSingle / createSession (useStore: false).
  3. Download via GET /auth/asset/:token (single-use) or GET /auth/session-asset/:token (multi-use until the session ends).

Full developer guide (resolver, minting, frontend assetLink, stacks): Asset Downloads.

TypeScript Types ​

req.authUser is typed as AuthUser | null:

typescript
interface AuthUser {
  userId: string
  isAdmin: boolean
  provider: string // "local", "github", etc.
}

req.user is now typed as UserDoc | undefined (optional, deprecated). TypeScript will flag all usages that need updating.

Permissions API ​

The new permissions system provides:

typescript
// In route handlers
const allowed = await req.can("custom.permission")

// As a preHandler
onRequest: [fastify.requirePermission("custom.permission")]

// Register custom resolvers at plugin boot
fastify.permissions.register({
  name: "my-plugin-resolver",
  resolve: async (userId, permission, context) => {
    if (permission === "myPlugin.canEdit") {
      return checkIfUserCanEdit(userId, context)
    }
    return null // null = "I don't handle this, ask next resolver"
  },
})

Work sessions do not use JWT. A guest proves access by presenting a valid work session UUID:

ChannelMechanism
HTTPx-work-session-id header (set by the client from the URL)
WebSocketauth.sessionId on the socket handshake

The server validates each request: document exists, isActive, not past expiryDate, and (for plugin routes) pluginData includes the plugin key.

Routes that should allow guest read access must use fastify.authenticatedOrWorksession(pluginKey) instead of fastify.authenticate alone. Work session callers receive 403 on POST/PATCH/PUT/DELETE β€” read-only by default.

Initial composition data is delivered via socket joinConfirmation after validation; no separate JWT is issued.

Checklist ​

  • [ ] Replace all req.user._id β†’ req.authUser!.userId
  • [ ] Replace all req.user.isAdmin β†’ req.authUser!.isAdmin
  • [ ] Remove any import { authenticate } from "@m/authentication" (module removed)
  • [ ] Remove any authToken cookie / Bearer JWT usage on the frontend
  • [ ] Use authenticatedOrWorksession for routes that must work with work session guests (read-only)
  • [ ] Add explicit user data fetching where the full document is needed
  • [ ] Test login/logout flow end-to-end
  • [ ] Verify work session links still load via x-work-session-id