Skip to content

Authentication & Authorization

Authentication Methods

flow8 supports three distinct authentication mechanisms, each suited for different client types and use cases.

Mechanism:

  1. User submits username and password to /api/v1/auth/login
  2. Server verifies password against bcrypt hash (cost=12)
  3. Server generates HTTP-only session cookie with random session ID
  4. Client includes cookie in subsequent requests
  5. Server validates session TTL and user status

Session configuration:

config/config.yml
session:
ttl_hours: 1 # Session lifetime
cookie_secure: true # HTTPS only
cookie_http_only: true # JavaScript cannot access
cookie_same_site: strict # CSRF protection
cookie_domain: "" # Auto-detect from Host header
cookie_path: "/"

Environment overrides:

Terminal window
SESSION_TTL_HOURS=2
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTP_ONLY=true
SESSION_COOKIE_SAME_SITE=strict

Security properties:

  • bcrypt cost=12: ~100-200ms to hash (prevents brute force)
  • HTTP-only flag: Prevents XSS attacks from stealing session
  • Secure flag: Prevents transmission over unencrypted HTTP
  • SameSite=Strict: Prevents CSRF attacks
  • Random session ID: 32-byte cryptographically random

Flow:

User Server
β”‚ β”‚
│─── POST /auth/login ───────→│
β”‚ (username, password) β”‚
β”‚ β”œβ”€ Verify username exists
β”‚ β”œβ”€ Verify bcrypt(password)
β”‚ β”œβ”€ Generate session ID
β”‚ β”œβ”€ Store session with TTL
│←── Set-Cookie: sid=... ────│
β”‚ β”‚
│─── GET /api/flows ────────→│ (Include Cookie: sid=...)
β”‚ β”œβ”€ Lookup session by ID
β”‚ β”œβ”€ Check TTL not expired
β”‚ β”œβ”€ Return flows for user's company
│←── Flows ──────────────────│

Cookie header example:

Set-Cookie: sid=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6;
Path=/;
Domain=.flow8.local;
HttpOnly;
Secure;
SameSite=Strict;
Max-Age=3600

2. API Key Authentication (JWT)

Mechanism:

  1. Administrator creates API key via /api/v1/admin/api-keys
  2. Server generates JWT with claims (user_id, company_id, key_id, scope)
  3. JWT is signed with HS256 using server secret
  4. Client includes JWT in Authorization: Bearer <token> header
  5. Server validates signature and claims

API Key creation:

Terminal window
POST /api/v1/admin/api-keys
{
"name": "deployment-automation",
"user_id": "...",
"company_id": "...",
"scope": "api,mcp", # Comma-separated scopes
"expires_at": "2027-04-04T10:00:00Z"
}
Response:
{
"id": "key_123...",
"token": "eyJhbGciOiJIUzI1NiIs..." # JWT (returned once only)
}

JWT structure (HS256):

Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "user_123", # User ID
"iss": "flow8", # Issuer
"aud": ["api"], # Audience
"company_id": "company_456", # Multi-tenancy
"key_id": "key_123", # For key rotation/revocation
"scope": ["api", "mcp"], # Requested scopes
"iat": 1712244000, # Issued at
"exp": 1743780000 # Expiration
}
Signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)

Scope system:

ScopeGrantsRestrictions
apiFull REST API accessRead/write flows, plays, configs
mcpModel Context Protocol accessLimited to MCP tools (flow CRUD for AI agents)
readRead-only API accessGET endpoints only
flow:executeExecute flowsCannot create/edit flows

Security properties:

  • HS256: Symmetric signature (only server can mint tokens)
  • Key ID in claim: Enables key rotation without breaking all tokens
  • Per-scope restrictions: Limits blast radius of compromised key
  • TTL enforced: Tokens expire and must be refreshed
  • Revocation: Listing key as revoked invalidates all tokens with that key_id

Usage example:

Terminal window
# Create key
API_KEY=$(curl -X POST http://localhost:4454/api/v1/admin/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"name":"ci-cd","scope":"api"}' | jq -r .token)
# Use key to call API
curl -H "Authorization: Bearer $API_KEY" \
http://localhost:4454/api/v1/flows

3. OAuth2 Authentication (Microsoft/Office 365)

Mechanism:

  1. User clicks β€œLogin with Office 365” on sign-in page
  2. Browser redirects to Microsoft login
  3. User authenticates with their corporate identity
  4. Microsoft redirects back to flow8 with authorization code
  5. flow8 exchanges code for access token (backend)
  6. flow8 looks up or auto-creates user in database
  7. Session cookie is created
  8. User is logged in

Configuration:

config/config.yml
oauth2:
microsoft:
client_id: "550e8400-e29b-41d4-a716-446655440000"
client_secret: "[encrypted]" # Encrypted in config/secrets
redirect_uri: "https://app.flow8.io/auth/callback"
scopes: ["openid", "profile", "email"]
auto_create_users: true # Create user on first login
auto_assign_company: "company_123" # Assign to company if not in any

Environment variables:

Terminal window
MICROSOFT_CLIENT_ID=550e8400-e29b-41d4-a716-446655440000
MICROSOFT_CLIENT_SECRET=...
MICROSOFT_REDIRECT_URI=https://app.flow8.io/auth/callback
OAUTH2_AUTO_CREATE_USERS=true

OAuth2 flow diagram:

User Browser flow8 Microsoft
β”‚ β”‚ β”‚ β”‚
│─── Click Login ──────→│ β”‚ β”‚
β”‚ │─ Redirect ─────────→│ β”‚
β”‚ β”‚ /authorize? β”‚ β”‚
β”‚ β”‚ client_id=... β”‚ β”‚
β”‚ β”‚ redirect_uri=... │─ Redirect ────────→│
β”‚ β”‚ β”‚ /authorize?... β”‚
│◄─────────────────────────────────────────────────────────────────│
β”‚ (Microsoft login page)
│─── Submit creds ─────→│ (Microsoft handles)
β”‚ β”‚
│◄──────────────────────── Redirect back ────│
β”‚ with code=... β”‚ /callback?code=..←│
β”‚ β”‚
β”‚ │──Post /token ─────→│
β”‚ β”‚ (client_id, β”‚
β”‚ β”‚ client_secret, β”‚
β”‚ β”‚ code) β”‚
β”‚ β”‚ β”‚
β”‚ │←─ Access token ────│
β”‚ β”‚
β”‚ │──Get /userinfo───→ β”‚
β”‚ β”‚ (access_token) β”‚
β”‚ β”‚ β”‚
β”‚ │←─ User info ───────│
β”‚ β”‚ (email, name) β”‚
β”‚ β”‚
β”‚ │◄─ Set-Cookie ─────│
│◄──────────────────────────────────────────│
β”‚ (session created)

Access token storage (DBLink):

type DBLink struct {
ID primitive.ObjectID
UserID primitive.ObjectID
CompanyID primitive.ObjectID
IntegrationType string // "microsoft", "google", "slack", etc.
AccessToken string // [encrypted]
RefreshToken string // [encrypted]
TokenExpiry time.Time
Scopes []string // Requested scopes
ProviderUserID string // Unique ID from provider
ProviderUserEmail string // Email from provider
CreatedAt time.Time
UpdatedAt time.Time
}

Security properties:

  • Code exchange: Server-side (token never exposed to browser)
  • Token encryption: Stored encrypted in MongoDB
  • Token refresh: Automatic refresh before expiry
  • Scopes: Minimal scopes requested (openid, profile, email)
  • PKCE: Optional (recommended for high-security deployments)

Authorization & RBAC

Company-Level Isolation

Every flow8 entity is scoped to a company_id:

type DBFlow struct {
CompanyID primitive.ObjectID
// ...
}
type DBUser struct {
CompanyID primitive.ObjectID // User can belong to multiple companies
// ...
}
type DBAuditLog struct {
CompanyID primitive.ObjectID
// ...
}

All database queries are automatically filtered:

// Middleware attaches company context from session/token
ctx := r.Context()
companyID := ctx.Value("company_id")
// All queries include company filter
flows, _ := s.db.Collection("flows").Find(ctx, bson.M{
"company_id": companyID, // Automatic filter
})

A user in multiple companies must specify which company they’re accessing:

Terminal window
POST /api/v1/auth/switch-company
{
"company_id": "company_xyz"
}

Role-Based Access Control (RBAC)

Each user has a role per company:

RolePermissionsUse Case
adminCreate users, assign roles, manage OAuth2, configure systemIT administrator
flow_editorCreate, edit, deploy flowsFlow developer
flow_executorExecute flows, view resultsEnd user, automation consumer
audit_viewerRead audit logs, view user activityCompliance officer
integration_managerManage OAuth2 links, API credentialsIntegration specialist
analytics_viewerView flow metrics, performance analyticsManager
viewerRead-only access to flows and resultsStakeholder
noneNo access (default)Placeholder

Example user roles:

{
"_id": "user_123",
"email": "alice@company.com",
"company_roles": [
{
"company_id": "company_a",
"role": "admin"
},
{
"company_id": "company_b",
"role": "flow_editor"
}
]
}

Permission matrix:

ResourceViewCreateEditDeleteExecute
Flowsviewer+flow_editor+flow_editor+adminflow_executor+
Playsviewer+N/AN/Aadminflow_executor (own)
Audit Logsaudit_viewer+N/AN/AN/AN/A
OAuth2 Linksadminintegration_mgr+integration_mgr+adminN/A
System ConfigadminN/AadminN/AN/A

Entity-Level Access Control (DBAccess)

For fine-grained control beyond roles, use the access_grants collection:

type DBAccess struct {
ID primitive.ObjectID
CompanyID primitive.ObjectID
ResourceType string // "flow", "flow_group", "integration"
ResourceID primitive.ObjectID // Flow ID, group ID, etc.
GrantType string // "user" or "group"
GranteeID primitive.ObjectID // User ID or group ID
Permissions []string // ["read", "execute", "edit", "delete"]
CreatedBy primitive.ObjectID // Creator user ID
CreatedAt time.Time
}

Example: Grant editor access to a specific flow:

Terminal window
POST /api/v1/access/grant
{
"resource_type": "flow",
"resource_id": "flow_456",
"grantee_id": "user_789",
"grant_type": "user",
"permissions": ["read", "execute", "edit"]
}

Access evaluation:

if user has admin role β†’ ALLOW
if user is flow creator β†’ ALLOW
if DBAccess grant exists for user+flow β†’ ALLOW if permissions match
otherwise β†’ DENY

Multi-Company User Support

A user can be assigned to multiple companies with different roles:

Terminal window
# Alice is admin in company A, flow_executor in company B
curl -X POST http://localhost:4454/api/v1/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"email": "alice@company.com",
"roles": [
{"company_id": "company_a", "role": "admin"},
{"company_id": "company_b", "role": "flow_executor"}
]
}'
# Later, Alice switches to company B
curl -X POST http://localhost:4454/api/v1/auth/switch-company \
-H "Authorization: Bearer $SESSION_COOKIE" \
-d '{"company_id": "company_b"}'
# Now Alice can only access company B data

Session context (stored in cookie/JWT) tracks current company:

type SessionContext struct {
UserID primitive.ObjectID
CompanyID primitive.ObjectID // Current company
Roles []string // Roles in current company
Scopes []string // (for JWT)
}

API Key Scopes

API keys can be restricted to specific operations via scopes:

Scope Definitions

ScopeGrantsRestrictions
apiFull REST APIAll endpoints
api:readRead-only APIGET endpoints only
api:flowsFlow managementPOST /flows, PUT /flows/:id
api:playsPlay executionPOST /plays, GET /plays
mcpMCP toolsFlow management for AI agents
webhooksWebhook managementPOST /webhooks, PUT /webhooks/:id
adminAdmin operationsUser management, system config

Example: Create read-only API key for metrics:

Terminal window
POST /api/v1/admin/api-keys
{
"name": "metrics-export",
"scope": "api:read",
"expires_at": "2027-04-04T00:00:00Z"
}

Scope enforcement in middleware:

func (m *AuthMiddleware) Validate(token *jwt.Token) error {
claims := token.Claims.(jwt.MapClaims)
requiredScope := m.getRequiredScope(r.URL.Path) // e.g., "api:flows" for POST /flows
grantedScopes := parseScopes(claims["scope"])
if !contains(grantedScopes, requiredScope) {
return errors.New("insufficient scope")
}
}

Session Management

Session Lifecycle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ SESSION CREATION β”‚
β”‚ POST /auth/login β†’ verify password β†’ issue cookie β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ TTL (1 hour default)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ACTIVE SESSION β”‚
β”‚ User makes requests with cookie, TTL refreshed β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ TTL expires
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ SESSION EXPIRED β”‚
β”‚ Cookie invalid, user redirected to login β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Session Storage

Sessions stored in MongoDB with TTL index:

type DBSession struct {
ID primitive.ObjectID
UserID primitive.ObjectID
CompanyID primitive.ObjectID
SessionID string // Random
CreatedAt time.Time
ExpiresAt time.Time // TTL index
LastActivity time.Time // For activity tracking
}
// MongoDB TTL index
db.sessions.createIndex({ "expires_at": 1 }, { expireAfterSeconds: 0 })

Sessions are deleted automatically by MongoDB after expiry.

Token Revocation

API Key Revocation

Terminal window
DELETE /api/v1/admin/api-keys/:key_id

Listing revoked key IDs in a cache prevents their use:

type RevokedKeys struct {
KeyID string
RevokedAt time.Time
}
// Check during validation
if isInRevokedList(claims["key_id"]) {
return errors.New("token revoked")
}

Session Logout

Terminal window
POST /auth/logout

Deletes the session from MongoDB:

db.sessions.deleteOne({ "session_id": sessionID })

Cookie is also cleared from browser.

Best Practices

  1. Use OAuth2 for enterprise: Integrates with Azure AD, enforces MFA
  2. Rotate API keys quarterly: Limit exposure window
  3. Use short-lived tokens: Set expires_at to 24 hours maximum
  4. Monitor failed auth: Alert on multiple failed login attempts
  5. Use entity-level access for sensitive flows: Don’t rely on role alone
  6. Enable audit logging: Track all auth events for compliance
  7. Implement rate limiting: Prevent brute force attacks (e.g., max 5 login attempts per minute)