Authentication & Authorization
Authentication Methods
flow8 supports three distinct authentication mechanisms, each suited for different client types and use cases.
1. Session & Cookie Authentication
Mechanism:
- User submits username and password to
/api/v1/auth/login - Server verifies password against bcrypt hash (cost=12)
- Server generates HTTP-only session cookie with random session ID
- Client includes cookie in subsequent requests
- Server validates session TTL and user status
Session configuration:
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:
SESSION_TTL_HOURS=2SESSION_COOKIE_SECURE=trueSESSION_COOKIE_HTTP_ONLY=trueSESSION_COOKIE_SAME_SITE=strictSecurity 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=36002. API Key Authentication (JWT)
Mechanism:
- Administrator creates API key via
/api/v1/admin/api-keys - Server generates JWT with claims (user_id, company_id, key_id, scope)
- JWT is signed with HS256 using server secret
- Client includes JWT in
Authorization: Bearer <token>header - Server validates signature and claims
API Key creation:
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:
| Scope | Grants | Restrictions |
|---|---|---|
api | Full REST API access | Read/write flows, plays, configs |
mcp | Model Context Protocol access | Limited to MCP tools (flow CRUD for AI agents) |
read | Read-only API access | GET endpoints only |
flow:execute | Execute flows | Cannot 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:
# Create keyAPI_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 APIcurl -H "Authorization: Bearer $API_KEY" \ http://localhost:4454/api/v1/flows3. OAuth2 Authentication (Microsoft/Office 365)
Mechanism:
- User clicks βLogin with Office 365β on sign-in page
- Browser redirects to Microsoft login
- User authenticates with their corporate identity
- Microsoft redirects back to flow8 with authorization code
- flow8 exchanges code for access token (backend)
- flow8 looks up or auto-creates user in database
- Session cookie is created
- User is logged in
Configuration:
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 anyEnvironment variables:
MICROSOFT_CLIENT_ID=550e8400-e29b-41d4-a716-446655440000MICROSOFT_CLIENT_SECRET=...MICROSOFT_REDIRECT_URI=https://app.flow8.io/auth/callbackOAUTH2_AUTO_CREATE_USERS=trueOAuth2 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/tokenctx := r.Context()companyID := ctx.Value("company_id")
// All queries include company filterflows, _ := 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:
POST /api/v1/auth/switch-company{ "company_id": "company_xyz"}Role-Based Access Control (RBAC)
Each user has a role per company:
| Role | Permissions | Use Case |
|---|---|---|
admin | Create users, assign roles, manage OAuth2, configure system | IT administrator |
flow_editor | Create, edit, deploy flows | Flow developer |
flow_executor | Execute flows, view results | End user, automation consumer |
audit_viewer | Read audit logs, view user activity | Compliance officer |
integration_manager | Manage OAuth2 links, API credentials | Integration specialist |
analytics_viewer | View flow metrics, performance analytics | Manager |
viewer | Read-only access to flows and results | Stakeholder |
none | No 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:
| Resource | View | Create | Edit | Delete | Execute |
|---|---|---|---|---|---|
| Flows | viewer+ | flow_editor+ | flow_editor+ | admin | flow_executor+ |
| Plays | viewer+ | N/A | N/A | admin | flow_executor (own) |
| Audit Logs | audit_viewer+ | N/A | N/A | N/A | N/A |
| OAuth2 Links | admin | integration_mgr+ | integration_mgr+ | admin | N/A |
| System Config | admin | N/A | admin | N/A | N/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:
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 β ALLOWif user is flow creator β ALLOWif DBAccess grant exists for user+flow β ALLOW if permissions matchotherwise β DENYMulti-Company User Support
A user can be assigned to multiple companies with different roles:
# Alice is admin in company A, flow_executor in company Bcurl -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 Bcurl -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 dataSession 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
| Scope | Grants | Restrictions |
|---|---|---|
api | Full REST API | All endpoints |
api:read | Read-only API | GET endpoints only |
api:flows | Flow management | POST /flows, PUT /flows/:id |
api:plays | Play execution | POST /plays, GET /plays |
mcp | MCP tools | Flow management for AI agents |
webhooks | Webhook management | POST /webhooks, PUT /webhooks/:id |
admin | Admin operations | User management, system config |
Example: Create read-only API key for metrics:
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 indexdb.sessions.createIndex({ "expires_at": 1 }, { expireAfterSeconds: 0 })Sessions are deleted automatically by MongoDB after expiry.
Token Revocation
API Key Revocation
DELETE /api/v1/admin/api-keys/:key_idListing revoked key IDs in a cache prevents their use:
type RevokedKeys struct { KeyID string RevokedAt time.Time}
// Check during validationif isInRevokedList(claims["key_id"]) { return errors.New("token revoked")}Session Logout
POST /auth/logoutDeletes the session from MongoDB:
db.sessions.deleteOne({ "session_id": sessionID })Cookie is also cleared from browser.
Best Practices
- Use OAuth2 for enterprise: Integrates with Azure AD, enforces MFA
- Rotate API keys quarterly: Limit exposure window
- Use short-lived tokens: Set
expires_atto 24 hours maximum - Monitor failed auth: Alert on multiple failed login attempts
- Use entity-level access for sensitive flows: Donβt rely on role alone
- Enable audit logging: Track all auth events for compliance
- Implement rate limiting: Prevent brute force attacks (e.g., max 5 login attempts per minute)