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.
Adding the Passkey Addon
Section titled “Adding the Passkey Addon”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:
# 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.
Session Management
Section titled “Session Management”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.
Example: A Simple User Session
Section titled “Example: A Simple User Session”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.
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
.
{ // ... "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.
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.
'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.
'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> );}