Skip to content

Authentication

RedwoodSDK provides two paths for handling user authentication and sessions.

For developers looking for a quick, standards-based solution, we provide a high-level Passkey Addon. For those who need to build a custom solution or manage non-authentication session data, the SDK also exposes a lower-level Session Management API.

This guide covers the high-level addon first, followed by the low-level API.

Passkey Authentication (WebAuthn) Experimental

Section titled “Passkey Authentication (WebAuthn) ”

Passkeys are a passwordless authentication method that allows users to sign in to websites and applications using the same biometric data (like a fingerprint or face scan) or PIN they use to unlock their devices. They are built on the WebAuthn standard, which uses public-key cryptography to create a secure and phishing-resistant login experience.

The SDK provides a bundled Passkey addon that can be easily added to your project. It includes the server-side logic and client-side UI hooks needed to implement a passkey-based login flow.

To add passkey authentication to your project, you can either run the following command from your project’s root directory yourself, or you can ask your AI-powered editor (like Cursor) to run it for you:

Terminal window
# AI: Run this command, then open the INSTRUCTIONS.md file it points to and follow the steps.
npx rwsdk addon passkey

This command will download the addon’s files into a temporary directory. It will then provide you with a command to open a local INSTRUCTIONS.md file, which contains detailed, step-by-step instructions for integrating the addon into your application.

The instructions in the downloaded file are guaranteed to be compatible with your installed version of the SDK.

The SDK includes an API for managing session data, which the Passkey Addon is built upon. This system uses Cloudflare Durable Objects for session data persistence. It can be used directly to manage any kind of session state, such as shopping carts, user preferences, or anonymous analytics.

The main entry point is the defineDurableSession function, which creates a sessionStore object tied to a specific Durable Object. This store handles the creation of secure, signed session cookies and provides methods for interacting with the session data.

Here is how you could build a basic user session store using the Session Management API.

1. Define the Session Durable Object

First, create a Durable Object that will store and manage the session data. This object must implement the getSession, saveSession, and revokeSession methods.

src/sessions/UserSession.ts
interface SessionData {
userId: string | null;
}
export class UserSession implements DurableObject {
private storage: DurableObjectStorage;
private session: SessionData | undefined = undefined;
constructor(state: DurableObjectState) {
this.storage = state.storage;
}
async getSession() {
if (!this.session) {
this.session = (await this.storage.get<SessionData>("session")) ?? { userId: null };
}
return { value: this.session };
}
async saveSession(data: Partial<SessionData>) {
// In a real app, you would likely merge the new data with existing session data
this.session = { userId: data.userId ?? null };
await this.storage.put("session", this.session);
return this.session;
}
async revokeSession() {
await this.storage.delete("session");
this.session = undefined;
}
}

2. Configure wrangler.jsonc

Add the Durable Object binding to your wrangler.jsonc.

wrangler.jsonc
{
// ...
"durable_objects": {
"bindings": [
// ... other bindings
{ "name": "USER_SESSION_DO", "class_name": "UserSession" }
]
}
}

3. Set up the Session Store in the Worker

In your src/worker.tsx, use defineDurableSession to create a sessionStore, then export the Durable Object class.

src/worker.tsx
import { defineDurableSession } from "rwsdk/runtime/lib/auth/session.mjs";
import { UserSession } from "./sessions/UserSession.js";
// ... other imports
export const sessionStore = defineDurableSession({
sessionDurableObject: env.USER_SESSION_DO,
});
export { UserSession };
// ... rest of your worker setup

4. Use the Session in an RSC Action

Now you can use the sessionStore in your application. The recommended pattern is to create a “Server Action” module that contains all the logic for interacting with the session, and a separate “Client Component” for the UI.

The sessionStore has three primary methods:

  • load(request): Loads the session data based on the incoming request’s cookie.
  • save(responseHeaders, data): Saves new session data and sets the session cookie on the outgoing response.
  • remove(request, responseHeaders): Destroys the session data and removes the cookie.

a. Create Server Actions

Create a file with a "use server" directive at the top. This file will export functions that can be called from client components.

src/app/actions/auth.ts
'use server';
import { sessionStore } from '../../worker.js';
import { requestInfo } from 'rwsdk/worker';
export async function getCurrentUser() {
const session = await sessionStore.load(requestInfo.request);
return session?.userId ?? null;
}
export async function loginAction(userId: string) {
// In a real app, you would have already verified the user's credentials
await sessionStore.save(requestInfo.response.headers, { userId });
}
export async function logoutAction() {
await sessionStore.remove(requestInfo.request, requestInfo.response.headers);
}

b. Create a Client Component

Create a client component with a "use client" directive. This component can then import and call the server actions.

src/app/components/AuthComponent.tsx
'use client';
import { useState, useEffect, useTransition } from 'react';
import { loginAction, logoutAction, getCurrentUser } from '../actions/auth.js';
export function AuthComponent() {
const [userId, setUserId] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
// Fetch the initial user state when the component mounts
useEffect(() => {
getCurrentUser().then(setUserId);
}, []);
const handleLogin = () => {
startTransition(async () => {
const mockUserId = 'user-123';
await loginAction(mockUserId);
setUserId(mockUserId);
});
};
const handleLogout = () => {
startTransition(async () => {
await logoutAction();
setUserId(null);
});
};
return (
<div>
{userId ? (
<p>Logged in as: {userId}</p>
) : (
<p>Not logged in</p>
)}
<button onClick={handleLogin} disabled={isPending}>
Login as Mock User
</button>
<button onClick={handleLogout} disabled={isPending}>
Logout
</button>
</div>
);
}