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
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
}
}
}
Keystone validates credentials
Password is verified using bcrypt hash comparison.
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" ,
})
);
Subsequent requests include cookie
Browser automatically sends session cookie with each request.
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
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
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
User Grants Permission
User reviews requested scopes and approves or denies.
Authorization Code Issued
Openfront redirects back with authorization code. https://app.com/callback?
code=auth_code_123&
state=random_state_string
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
}
}
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.
Openfront API keys follow this format:
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.
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