Skip to main content

Overview

Openfront implements a comprehensive authentication system supporting multiple auth methods:
  • Session-based authentication - Cookie-based sessions for admin users and customers
  • OAuth 2.0 - Third-party app authorization with scopes
  • API keys - Programmatic access with scope-based permissions
  • Customer tokens - Special tokens for business account integrations
All authentication logic is centralized in features/keystone/index.ts within the custom statelessSessions function.

Authentication Methods

Session-Based Authentication

How It Works

Openfront uses Iron-sealed stateless sessions stored in HTTP-only cookies. Sessions don’t require database lookups - all data is encrypted in the cookie itself.

Session Configuration

features/keystone/index.ts
const sessionConfig = {
  maxAge: 60 * 60 * 24 * 360, // 360 days
  secret: process.env.SESSION_SECRET,
  cookieName: "keystonejs-session",
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
};
Security: The SESSION_SECRET must be at least 32 characters long and kept secure. Never commit it to version control.

Session Data Structure

When a user signs in, their session contains:
{
  itemId: "user_123",           // User ID
  listKey: "User",              // Model name
  data: {                       // User data
    id: "user_123",
    name: "John Doe",
    email: "[email protected]",
    role: {                     // Role with permissions
      canAccessDashboard: true,
      canManageProducts: true,
      canManageOrders: true,
      // ... more permissions
    }
  }
}

Authentication Flow

1

User submits credentials

User provides email and password via login form.
mutation SignIn {
  authenticateUserWithPassword(
    email: "[email protected]"
    password: "secure-password"
  ) {
    ... on UserAuthenticationWithPasswordSuccess {
      sessionToken
      item {
        id
        name
        email
      }
    }
    ... on UserAuthenticationWithPasswordFailure {
      message
    }
  }
}
2

Keystone validates credentials

Password is verified using bcrypt hash comparison.
3

Session created and sealed

Session data is encrypted using Iron and set as HTTP-only cookie.
const sealedData = await Iron.seal(sessionData, secret, {
  ttl: maxAge * 1000,
});

context.res.setHeader(
  "Set-Cookie",
  cookie.serialize(cookieName, sealedData, {
    maxAge,
    httpOnly: true,
    secure: true,
    sameSite: "lax",
  })
);
4

Subsequent requests include cookie

Browser automatically sends session cookie with each request.
5

Session unsealed and validated

Cookie is decrypted and user permissions are checked.
const cookies = cookie.parse(context.req.headers.cookie || "");
const token = cookies[cookieName];
const session = await Iron.unseal(token, secret, ironOptions);

Session Endpoints

Sign In
mutation {
  authenticateUserWithPassword(
    email: "[email protected]"
    password: "password123"
  ) {
    ... on UserAuthenticationWithPasswordSuccess {
      sessionToken
      item { id name email }
    }
  }
}
Sign Out
mutation {
  endSession
}
Current User
query {
  authenticatedItem {
    ... on User {
      id
      name
      email
      role {
        canAccessDashboard
        canManageProducts
      }
    }
  }
}

OAuth 2.0 Authentication

OAuth Apps

Third-party applications can integrate with Openfront using OAuth 2.0. Each app is registered with:
OAuthApp:
- name: "My Integration App"
- clientId: "app_abc123"
- clientSecret: "secret_xyz789" (hashed)
- redirectUris: ["https://app.com/callback"]
- scopes: ["read_orders", "write_products"]
- status: "active" | "inactive" | "suspended"

OAuth Flow

1

Authorization Request

App redirects user to Openfront authorization page.
GET /oauth/authorize?
  client_id=app_abc123&
  redirect_uri=https://app.com/callback&
  response_type=code&
  scope=read_orders+write_products&
  state=random_state_string
2

User Grants Permission

User reviews requested scopes and approves or denies.
3

Authorization Code Issued

Openfront redirects back with authorization code.
https://app.com/callback?
  code=auth_code_123&
  state=random_state_string
4

Token Exchange

App exchanges code for access token.
mutation ExchangeOAuthCode {
  exchangeOAuthCode(
    code: "auth_code_123"
    clientId: "app_abc123"
    clientSecret: "secret_xyz789"
  ) {
    accessToken
    refreshToken
    expiresIn
    tokenType
  }
}
5

API Access

App uses access token in Authorization header.
curl -H "Authorization: Bearer access_token_xyz" \
  https://api.openfront.com/graphql

OAuth Token Validation

When a request includes an OAuth token, Openfront validates it:
features/keystone/index.ts (lines 192-236)
const authHeader = context.req.headers.authorization;

if (authHeader?.startsWith("Bearer ")) {
  const accessToken = authHeader.replace("Bearer ", "");
  
  // Find OAuth token in database
  const oauthToken = await context.sudo().query.OAuthToken.findOne({
    where: { token: accessToken },
    query: `id clientId scopes expiresAt tokenType isRevoked user { id }`
  });
  
  // Validate token
  if (oauthToken) {
    if (oauthToken.tokenType !== "access_token") return;
    if (oauthToken.isRevoked === "true") return;
    if (new Date() > new Date(oauthToken.expiresAt)) return;
    
    // Check if app is active
    const oauthApp = await context.sudo().query.OAuthApp.findOne({
      where: { clientId: oauthToken.clientId },
      query: `id status`
    });
    
    if (!oauthApp || oauthApp.status !== 'active') return;
    
    // Return session with OAuth scopes
    return { 
      itemId: oauthToken.user.id, 
      listKey: "User",
      oauthScopes: oauthToken.scopes
    };
  }
}

OAuth Scopes

Openfront supports fine-grained permission scopes:
  • read_products - View products and inventory
  • write_products - Manage products and inventory
  • read_orders - View orders and customer information
  • write_orders - Manage orders and fulfillments
  • read_customers - View customer information
  • write_customers - Manage customer accounts
  • read_fulfillments - View fulfillment information
  • write_fulfillments - Manage fulfillments and shipping
  • read_payments - View payment information
  • write_payments - Process payments and refunds
  • read_discounts - View discount codes
  • write_discounts - Manage discount codes
  • read_gift_cards, write_gift_cards
  • read_returns, write_returns
  • read_sales_channels, write_sales_channels
  • read_webhooks, write_webhooks
  • read_apps, write_apps

API Key Authentication

Creating API Keys

API keys provide programmatic access with scope-based permissions. They’re ideal for server-to-server integrations.
mutation CreateApiKey {
  createApiKey(
    data: {
      name: "Production Bot"
      scopes: ["read_orders", "write_orders"]
      status: "active"
      expiresAt: "2025-12-31T23:59:59Z"
      restrictedToIPs: ["192.168.1.100", "10.0.0.50"]
    }
  ) {
    id
    name
    tokenPreview
    # Full token only shown once at creation
  }
}
Important: The full API key token (e.g., of_1234567890abcdef) is only shown once during creation. Store it securely - it cannot be retrieved later.

API Key Format

Openfront API keys follow this format:
of_[random_string]
Example: of_sk_test_51A2B3C4D5E6F7G8H9I0J

API Key Validation

When a request includes an API key, Openfront performs comprehensive validation:
features/keystone/index.ts (lines 76-189)
if (accessToken.startsWith("of_")) {
  // Get all active API keys
  const apiKeys = await context.sudo().query.ApiKey.findMany({
    where: { status: { equals: 'active' } },
    query: `
      id name scopes status expiresAt usageCount
      restrictedToIPs tokenSecret { isSet }
      user { id }
    `,
  });
  
  // Test token against each API key using bcrypt
  let matchingApiKey = null;
  for (const apiKey of apiKeys) {
    if (!apiKey.tokenSecret?.isSet) continue;
    
    const fullApiKey = await context.sudo().db.ApiKey.findOne({
      where: { id: apiKey.id },
    });
    
    // Verify using bcryptjs (same as Keystone password field)
    const isValid = await bcryptjs.compare(
      accessToken, 
      fullApiKey.tokenSecret
    );
    
    if (isValid) {
      matchingApiKey = apiKey;
      break;
    }
  }
  
  if (!matchingApiKey) return; // Invalid key
  
  // Check IP restrictions
  if (matchingApiKey.restrictedToIPs?.length > 0) {
    const clientIP = context.req.headers['x-forwarded-for'] || 
                     context.req.connection?.remoteAddress;
    
    if (!matchingApiKey.restrictedToIPs.includes(clientIP)) {
      return; // IP not allowed
    }
  }
  
  // Check expiration
  if (matchingApiKey.expiresAt && 
      new Date() > new Date(matchingApiKey.expiresAt)) {
    // Auto-revoke expired keys
    await context.sudo().query.ApiKey.updateOne({
      where: { id: matchingApiKey.id },
      data: { status: 'revoked' },
    });
    return;
  }
  
  // Update usage statistics
  const today = new Date().toISOString().split('T')[0];
  const usage = matchingApiKey.usageCount || { total: 0, daily: {} };
  usage.total = (usage.total || 0) + 1;
  usage.daily[today] = (usage.daily[today] || 0) + 1;
  
  context.sudo().query.ApiKey.updateOne({
    where: { id: matchingApiKey.id },
    data: {
      lastUsedAt: new Date(),
      usageCount: usage,
    },
  }).catch(console.error);
  
  // Return session with API key scopes
  return { 
    itemId: matchingApiKey.user.id, 
    listKey: "User",
    apiKeyScopes: matchingApiKey.scopes
  };
}

Using API Keys

Include the API key in the Authorization header:
curl -X POST https://api.openfront.com/graphql \
  -H "Authorization: Bearer of_sk_test_51A2B3C4D5E6F7G8H9I0J" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ products { id title } }"}'

IP Restrictions

Restrict API keys to specific IP addresses for enhanced security:
mutation UpdateApiKey {
  updateApiKey(
    where: { id: "..." }
    data: {
      restrictedToIPs: [
        "192.168.1.100",  # Office IP
        "203.0.113.42"    # Server IP
      ]
    }
  ) {
    id
    restrictedToIPs
  }
}
Use IP restrictions when API keys are used from known server locations. This prevents unauthorized use if a key is compromised.

Usage Tracking

Openfront automatically tracks API key usage:
{
  "total": 15847,
  "daily": {
    "2024-02-27": 523,
    "2024-02-28": 891,
    "2024-02-29": 412
  }
}

Customer Token Authentication

Overview

Customer tokens (ctok_*) are special authentication tokens for business account integrations, particularly for invoice management and order placement systems like Openship.

Token Format

ctok_[random_string]
Example: ctok_a1b2c3d4e5f6g7h8i9j0

Token Validation

features/keystone/index.ts (lines 241-281)
if (accessToken.startsWith('ctok_')) {
  // Find user by customer token
  const users = await context.sudo().query.User.findMany({
    where: { customerToken: { equals: accessToken } },
    take: 1,
    query: `
      id email name
      accounts(
        where: { 
          status: { equals: "active" }, 
          accountType: { equals: "business" } 
        }
      ) {
        id status availableCredit
      }
    `
  });
  
  const user = users[0];
  if (!user) return; // Invalid token
  
  // Require active business account
  const activeAccount = user.accounts?.[0];
  if (!activeAccount) return;
  
  // Return session with customer token flag
  return { 
    itemId: user.id, 
    listKey: "User",
    customerToken: true,
    activeAccountId: activeAccount.id
  };
}

Use Cases

Invoice Payments

Allow customers to pay invoices without full account login

Order Integration

Third-party systems (Openship) can place orders on behalf of customers

Self-Service Portal

Customers access their account via magic link

Webhook Receivers

Receive order updates at customer-specified endpoints

Generating Customer Tokens

mutation GenerateCustomerToken {
  generateCustomerToken(
    userId: "user_123"
  ) {
    token
    user {
      id
      email
      customerToken
    }
  }
}

Role-Based Access Control (RBAC)

Permission Model

Openfront uses a comprehensive role-based permission system with 30+ granular permissions:
Role {
  // Dashboard access
  canAccessDashboard: boolean
  
  // Product permissions
  canReadProducts: boolean
  canManageProducts: boolean
  
  // Order permissions
  canReadOrders: boolean
  canManageOrders: boolean
  
  // Customer permissions
  canReadUsers: boolean
  canManageUsers: boolean
  
  // Fulfillment permissions
  canReadFulfillments: boolean
  canManageFulfillments: boolean
  
  // ... and 20+ more permissions
}

Permission Checks

Permissions are enforced at multiple levels: 1. Model-Level Access Control
features/keystone/models/Product.ts
export const Product = list({
  access: {
    operation: {
      query: () => true, // Public can query
      create: permissions.canManageProducts,
      update: permissions.canManageProducts,
      delete: permissions.canManageProducts,
    },
    filter: {
      query: ({ session }) => {
        // Only show published products to non-admins
        if (!permissions.canManageProducts({ session })) {
          return { status: { equals: "published" } };
        }
        return true; // Admins see all
      },
    },
  },
  // ... fields
});
2. Field-Level Access Control
price: float({
  access: {
    read: () => true,
    create: permissions.canManageProducts,
    update: permissions.canManageProducts,
  },
})
3. Custom Mutation Guards
addToCart: graphql.field({
  resolve: async (source, args, context) => {
    // Check if user is authenticated
    if (!context.session?.itemId) {
      throw new Error('Must be signed in');
    }
    
    // Check specific permission
    if (!permissions.canManageOrders({ session: context.session })) {
      throw new Error('Insufficient permissions');
    }
    
    // Execute mutation
  },
})

Permission Helper Functions

features/keystone/access.ts
export const permissions = {
  canAccessDashboard: ({ session }) => {
    return !!session?.data?.role?.canAccessDashboard;
  },
  
  canManageProducts: ({ session }) => {
    return !!session?.data?.role?.canManageProducts;
  },
  
  canManageOrders: ({ session }) => {
    return !!session?.data?.role?.canManageOrders;
  },
  
  // ... more permission checks
};

// Filter rules for query restrictions
export const rules = {
  canManageUsers: ({ session }) => {
    if (!session) return false;
    
    // Admins can manage all users
    if (permissions.canManageUsers({ session })) {
      return true;
    }
    
    // Users can only manage themselves
    return { id: { equals: session.itemId } };
  },
};

Security Best Practices

  • Use HTTPS in production (secure: true cookie flag)
  • Set httpOnly: true to prevent XSS attacks
  • Use sameSite: "lax" to prevent CSRF
  • Rotate SESSION_SECRET periodically
  • Set appropriate session expiration (360 days default)
  • Store API keys in environment variables, never in code
  • Use IP restrictions when possible
  • Set expiration dates on API keys
  • Regularly audit and rotate keys
  • Use minimum required scopes (principle of least privilege)
  • Monitor usage for anomalies
  • Validate redirect_uri against registered URIs
  • Use state parameter to prevent CSRF
  • Implement PKCE for public clients
  • Short access token expiration (1 hour)
  • Use refresh tokens for long-lived access
  • Revoke tokens when app is deauthorized
  • Enforce minimum password length (10+ characters)
  • Use bcrypt for password hashing (handled by Keystone)
  • Implement rate limiting on login attempts
  • Support password reset flow
  • Reject common passwords

Testing Authentication

Testing with cURL

Session Auth (Cookie)
# Login and save cookie
curl -c cookies.txt -X POST https://api.openfront.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "mutation { authenticateUserWithPassword(email: \"[email protected]\", password: \"password123\") { sessionToken } }" }'

# Use cookie for subsequent requests
curl -b cookies.txt -X POST https://api.openfront.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ authenticatedItem { ... on User { id name } } }"}'
API Key Auth
curl -X POST https://api.openfront.com/graphql \
  -H "Authorization: Bearer of_sk_test_51A2B3C4D5E6F7G8H9I0J" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ products { id title } }"}'
OAuth Token Auth
curl -X POST https://api.openfront.com/graphql \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"query": "{ orders { id displayId } }"}'

Troubleshooting

Possible causes:
  • Expired session/token
  • Invalid credentials
  • Revoked API key
  • IP address not whitelisted
  • OAuth app suspended
Solutions:
  • Check token expiration
  • Verify API key status
  • Confirm IP restrictions
  • Re-authenticate and get new token
Possible causes:
  • Insufficient permissions/scopes
  • User role doesn’t have required permission
  • Attempting to access another user’s data
Solutions:
  • Check user role permissions
  • Verify OAuth/API key scopes
  • Use admin account for testing
  • Review access control rules in model definition
Possible causes:
  • Cookie not being set (check HTTPS/secure flag)
  • Cookie being blocked by browser
  • Domain mismatch
  • CORS issues
Solutions:
  • Check cookie settings in DevTools
  • Verify sameSite attribute
  • Ensure frontend and backend domains match
  • Configure CORS properly

Next Steps

User Management

Manage users and roles in the dashboard

API Keys Guide

Create and manage API keys

OAuth Apps

Register and configure OAuth applications

GraphQL API

Explore authenticated API endpoints

Build docs developers (and LLMs) love