Skip to main content

Session Authentication

@zeltjs/auth-session provides cookie-based session management for server-rendered applications.

Installation

pnpm add @zeltjs/auth-session @zeltjs/kv

Quick Start

1. Set the Secret

Set the SESSION_SECRET environment variable:

# .env
SESSION_SECRET=your-secret-key-at-least-32-characters

2. Configure Session Store

Sessions are stored in a KV store. By default SessionConfig uses the in-memory adaptor under the session: namespace. To customize the namespace (or other options), extend SessionConfig:

@Config
class MySessionConfig extends SessionConfig {
  override readonly kvStoreNamespace = 'sessions:';
}

3. Register Middleware

const app = createApp([http({
    controllers: [AuthController, UserController],
    middlewares: [SessionMiddleware],
  })], { configs: [MySessionConfig] });

4. Manage Sessions

Use session functions in your handlers:

@Controller('/auth')
class AuthController {
  @Post('/login')
  async login(body = validated(LoginSchema)) {
    const user = await validateCredentials(body.email, body.password);
    if (!user) {
      throw new HTTPException(401, { message: 'Invalid credentials' });
    }
    
    setSession({ userId: user.id, name: user.name });
    return { success: true };
  }

  @Get('/me')
  me() {
    const session = getSession();
    if (!session) {
      throw new HTTPException(401, { message: 'Not logged in' });
    }
    return session;
  }

  @Post('/logout')
  logout() {
    destroySession();
    return { success: true };
  }
}

Session API

FunctionDescription
getSession()Get current session data (undefined if not logged in)
setSession(data)Set session data (replaces existing)
updateSession(fn)Update session data with a function
destroySession()Destroy session and clear cookie
isNewSession()Check if this is a newly created session
getSessionId()Get the current session ID

setSession

Create or replace the session:

setSession({
  userId: '123',
  name: 'Alice',
  cart: [{ productId: 'abc', qty: 2 }],
});

updateSession

Partially update the session:

updateSession((session) => ({
  ...session,
  lastActivity: Date.now(),
}));

destroySession

Clear the session and cookie (for logout):

destroySession();

Type-Safe Sessions

Extend SessionSchema for type-safe session access:

declare module '@zeltjs/auth-session' {
  interface SessionSchema {
    userId?: string;
    name?: string;
    email?: string;
    cart?: CartItem[];
  }
}

Now all session functions are typed:

const session = getSession();
// TypeScript knows: session?.userId, session?.name, session?.cart

setSession({ userId: '123', name: 'Alice' });
// Type-checked against SessionSchema

Configuration

Extend SessionConfig to customize behavior:

@Config
class MySessionConfig extends SessionConfig {
  override readonly kvStoreNamespace = 'sessions:';

  override get cookieName(): string {
    return 'my_session';  // default: 'session'
  }

  override get ttlSec(): number {
    return 86400 * 7;  // 7 days (default: 1 day)
  }

  override get cookieOptions() {
    return {
      httpOnly: true,
      secure: true,
      sameSite: 'Strict' as const,
      path: '/',
    };
  }
}

Configuration Options

OptionTypeDefaultDescription
kvKVAdaptorMemoryKVKV adaptor (constructor arg 2) backing session storage
kvStoreNamespacestring'session:'Namespace prefix for session keys
secretstringenv.getString('SESSION_SECRET')Secret for signing session IDs
cookieNamestring'session'Cookie name
ttlSecnumber86400 (1 day)Session TTL in seconds
cookieOptionsobjectSee belowCookie configuration
  override get cookieOptions() {
    return {
      httpOnly: true,
      secure: this.env.getString('NODE_ENV', '') === 'production',
      sameSite: 'Lax' as const,
      path: '/',
    };
  }
}

Storage Backends

Memory (Development)

@Config
class MySessionConfig extends SessionConfig {
  constructor(kv = inject(MemoryKV)) {
    super(undefined, kv);
  }
}

Redis (Production)

SessionConfig takes the KV adaptor as its second constructor argument. Pass a RedisKVAdaptor to super() to store sessions in Redis (leave the first argument as undefined to keep the default Env injection):

@Config
class MySessionConfig extends SessionConfig {
  constructor(kv = inject(RedisKVAdaptor)) {
    super(undefined, kv);
  }

  override readonly kvStoreNamespace = 'sessions:';
}

Using Redis requires registering RedisConfig (from @zeltjs/redis) so the adaptor can resolve its connection.

Integration with User Context

Sessions don't automatically set the user context. Add middleware to bridge them:

@Middleware
export class SessionAuthMiddleware {
  constructor(private userRepo = inject(UserRepository)) {}

  async use(c: RequestContext, next: Next): Promise<Response | undefined> {
    const session = getSession() as { userId?: string } | undefined;

    if (session?.userId) {
      const user = await this.userRepo.findById(session.userId);
      setUser(
        { id: user.id, name: user.name, email: user.email },
        user.roles
      );
    }

    await next();
    return undefined;
  }
}

Register after SessionMiddleware:

const app = createApp([http({
    controllers: [UserController],
    middlewares: [SessionMiddleware, SessionAuthMiddleware],
  })], { configs: [MySessionConfig] });

Security Considerations

CSRF Protection

Session-based authentication requires CSRF protection. Consider using:

  • SameSite=Strict cookies (strongest, may affect UX)
  • SameSite=Lax cookies with CSRF tokens for mutations
  • Double-submit cookie pattern

Session Fixation

Always regenerate the session ID after login:

@Controller('/auth')
class AuthController {
  @Post('/login')
  async login(body = validated(LoginSchema)) {
    const user = await validateCredentials(body.email, body.password);
    
    destroySession();  // Clear old session
    setSession({ userId: user.id, name: user.name });  // Creates new ID
    
    return { success: true };
  }
}

Secure Cookies

In production, always use secure cookies:

const cookieOptions = {
  httpOnly: true,
  secure: true,  // HTTPS only
  sameSite: 'Strict' as const,
  path: '/',
};