<!-- Markdown mirror of https://justscale.sh/docs/features/auth -->

# Authentication

User authentication with database sessions

The `@justscale/auth` package provides a complete authentication system with user registration, password hashing, session management, and route protection.

## Installation

Bash

```bash
pnpm add @justscale/auth
```

## Quick Start

The `AuthFeature` bundles all auth services together. Just provide repositories for `User` and `Session` models:

main.tsTypeScript

```typescript
import JustScale from '@justscale/core';
import { AuthFeature, User, Session } from '@justscale/auth';
import { createPgModel, createPgRepository, PostgresClient } from '@justscale/postgres';

// Create PostgreSQL models
const PgUser = createPgModel(User, { table: 'users' });
const PgSession = createPgModel(Session, { table: 'sessions' });

// Build the app
const app = JustScale()
  .add(PostgresClient)
  .add(createPgRepository(PgUser))
  .add(createPgRepository(PgSession))
  .add(AuthFeature)  // Provides UserService, SessionService, PasswordService
  .add(AuthController)
  .build();

await app.serve({ http: 3000 });
```

## Built-in Models

The auth package provides pre-defined `User` and `Session` models:

### User Model

models/user.tsTypeScript

```typescript
// Built-in User model fields:
{
  email: string,           // Unique email address
  passwordHash: string,    // Scrypt-hashed password
  name?: string,           // Optional display name
  emailVerifiedAt?: Date,  // When email was verified
  lastLoginAt?: Date,      // Last successful login
}
```

### Session Model

models/session.tsTypeScript

```typescript
// Built-in Session model fields:
{
  user: Reference<User>,   // Reference to the user
  token: string,           // Random 64-char hex token
  userAgent?: string,      // Browser/client info
  ipAddress?: string,      // Client IP address
  expiresAt: Date,         // Session expiration
  lastActiveAt: Date,      // Last activity timestamp
}
```

## Services

### UserService

Handles user registration, authentication, and profile updates:

controllers/auth.controller.tsTypeScript

```typescript
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
import { UserService, SessionService, UserExistsError } from '@justscale/auth';

export const AuthController = createController('/auth', {
  inject: {
    users: UserService,
    sessions: SessionService,
  },
  routes: ({ users, sessions }) => ({
    register: Post('/register')
      .handle(async ({ body, res }) => {
        try {
          const user = await users.register(body.email, body.password, body.name);
          const session = await sessions.create(user);
          res.json({ token: session.token, user: { email: user.email } });
        } catch (e) {
          if (e instanceof UserExistsError) {
            res.status(409).json({ error: 'Email already registered' });
          } else throw e;
        }
      }),

    login: Post('/login')
      .handle(async ({ body, res }) => {
        const user = await users.authenticate(body.email, body.password);
        if (!user) {
          res.status(401).json({ error: 'Invalid credentials' });
          return;
        }
        const session = await sessions.create(user);
        res.json({ token: session.token });
      }),
  }),
});
```

### UserService Methods

user-service-api.tsTypeScript

```typescript
// Registration
const user = await users.register(email, password, name?);

// Authentication (returns user or undefined)
const user = await users.authenticate(email, password);

// Lookup
const user = await users.get(User.ref`${id}`);
const user = await users.findByEmail(email);

// Updates
await users.updatePassword(user, newPassword);
await users.verifyEmail(userId);  // Sets emailVerifiedAt
```

### SessionService

Manages session tokens with automatic expiration:

session-service-api.tsTypeScript

```typescript
// Create session (default TTL: 7 days)
const session = await sessions.create(user, {
  userAgent: req.headers['user-agent'],
  ipAddress: req.ip,
  ttlMs: 30 * 24 * 60 * 60 * 1000,  // 30 days
});

// Lookup (returns null if expired)
const session = await sessions.findByToken(token);

// Update activity timestamp
await sessions.touch(sessionId);

// Logout
await sessions.revoke(sessionId);
await sessions.revokeAllForUser(userRef);  // Logout everywhere

// Cleanup
await sessions.revokeExpired();  // Remove expired sessions
```

### PasswordService

Secure password hashing using scrypt with timing-safe comparison:

password-service-api.tsTypeScript

```typescript
// Hash a password (scrypt with random salt)
const hash = await passwords.hash('user-password');
// Returns: "salt:derivedKey" (hex encoded)

// Verify password (timing-safe)
const valid = await passwords.verify('user-password', hash);
```

## Middleware

Protect routes with authentication middleware:

### Required Authentication

protected-routes.tsTypeScript

```typescript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';

export const ProfileController = createController('/profile', {
  inject: {},
  routes: () => ({
    // Requires valid Authorization header
    me: Get('/')
      .use(auth)  // Adds session + user to context
      .handle(({ user, res }) => {
        res.json({
          email: user.email,
          name: user.name,
        });
      }),
  }),
});
```

The `auth` middleware extracts the Bearer token from the Authorization header, validates it, and adds `session` and `user` to the context. Throws `AuthenticationError` if invalid.

### Optional Authentication

optional-auth.tsTypeScript

```typescript
import { Get } from '@justscale/http';
import { optionalAuth } from '@justscale/auth';

// Works with or without authentication
feed: Get('/feed')
  .use(optionalAuth)  // session/user may be null
  .handle(({ user, res }) => {
    if (user) {
      // Personalized feed for logged-in users
      res.json({ feed: getPersonalizedFeed(user) });
    } else {
      // Generic feed for anonymous users
      res.json({ feed: getPublicFeed() });
    }
  }),
```

## Guards

Add authorization checks after authentication:

Files

guards-example.tsticket.model.tsprincipals.ts

guards-example.tsticket.model.tsprincipals.ts

guards-example.tsTypeScript

```typescript
import { Get, Patch, Post } from '@justscale/http';
import { auth, requireAuth, requireVerifiedEmail } from '@justscale/auth';
import { Ticket } from '../models/ticket';

// Require authenticated user
adminPanel: Get('/admin')
  .use(auth)
  .guard(requireAuth)
  .handle(...),

// Require verified email
sendMessage: Post('/messages')
  .use(auth)
  .guard(requireVerifiedEmail)
  .handle(...),

// Require ownership via a MODEL permission — Ticket.can.close is declared
// on the Ticket model as permit(Customer).when(customer). Under the hood
// that rule is queryable: toCondition(principal) yields an ORM condition
// the repository can push into a WHERE clause (ticket.customer_id = :id).
// The same rule runs at guard time against the resolved :ticket param.
closeTicket: Post('/tickets/:ticket/close')
  .types({ Ticket })
  .use(auth)
  .guard(Ticket.can.close)
  .handle(({ params, res }) => {
    // Guard passed — the authenticated Customer owns params.ticket
    res.status(204).end();
  }),
```

## Error Handling

The auth package exports specific error classes for handling auth failures:

error-handling.tsTypeScript

```typescript
import {
  AuthenticationError,
  UserExistsError,
  InvalidCredentialsError,
} from '@justscale/auth';

// In your error handler
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (e) {
    if (e instanceof AuthenticationError) {
      ctx.res.status(401).json({ error: e.message });
    } else if (e instanceof UserExistsError) {
      ctx.res.status(409).json({ error: 'Email already registered' });
    } else if (e instanceof InvalidCredentialsError) {
      ctx.res.status(401).json({ error: 'Invalid email or password' });
    } else throw e;
  }
});
```

## Database Schema

Create the required tables for User and Session models:

migrations/001_auth.sqlTypeScript

```typescript
-- Users table
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  name VARCHAR(100),
  email_verified_at TIMESTAMPTZ,
  last_login_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

-- Sessions table
CREATE TABLE sessions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token VARCHAR(255) NOT NULL UNIQUE,
  user_agent VARCHAR(500),
  ip_address VARCHAR(45),
  expires_at TIMESTAMPTZ NOT NULL,
  last_active_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
```

## Complete Example

src/main.tsTypeScript

```typescript
import JustScale from '@justscale/core';
import { listen } from '@justscale/http';
import { AuthFeature, User, Session } from '@justscale/auth';
import { createPgModel, createPgRepository, PostgresClient } from '@justscale/postgres';
import { AuthController } from './controllers/auth';
import { ProfileController } from './controllers/profile';

const PgUser = createPgModel(User, { table: 'users' });
const PgSession = createPgModel(Session, { table: 'sessions' });

const app = JustScale()
  .add(PostgresClient)
  .add(createPgRepository(PgUser))
  .add(createPgRepository(PgSession))
  .add(AuthFeature)
  .add(AuthController)
  .add(ProfileController)
  .build();

await app.serve({ http: 3000 });
```

## Best Practices

- Use HTTPS in production - Tokens are sent in headers, so always use TLS to protect them in transit
- Set appropriate session TTL - Balance security (shorter) vs UX (longer) based on your app's needs
- Clean up expired sessions - Run sessions.revokeExpired()periodically to remove stale sessions
- Store tokens securely on client - Use httpOnly cookies or secure storage, never localStorage for sensitive apps
- Implement rate limiting - Protect login/register endpoints from brute force attacks

## Next Steps

- Middleware
- Guards
- PostgreSQL Repositories
