Skip to content

Security & Permissions

This guide covers Querri’s security architecture, including Fine-Grained Authorization (FGA), authentication via WorkOS, JWT token management, and data security.

Querri implements a multi-layered security model:

User Request
Traefik Reverse Proxy (JWT Validation)
OPA Policy Engine (Authorization)
Application Layer (Business Logic)
FGA (Fine-Grained Permissions)
Data Layer (MongoDB)

Querri uses WorkOS for enterprise-grade authentication.

  1. User visits application
  2. Redirected to WorkOS authentication endpoint
  3. User authenticates via SSO provider (Google, Microsoft, Okta, etc.)
  4. WorkOS returns authorization code
  5. Hub service exchanges code for JWT token
  6. JWT token stored in secure cookie
  7. Subsequent requests include JWT in header

Required environment variables:

Terminal window
# WorkOS API credentials
WORKOS_API_KEY=sk_live_xxxxxxxxxxxxx
WORKOS_CLIENT_ID=client_xxxxxxxxxxxxx
# JWKS endpoint for token verification
WORKOS_JWKS_ENDPOINT=https://auth.yourcompany.com/sso/jwks/client_xxxxxxxxxxxxx
# OAuth callback URL
WORKOS_REDIRECT_URI=https://app.yourcompany.com/hub/auth/callback
# Cookie encryption
WORKOS_COOKIE_PASSWORD=generate_random_32_character_string

WorkOS supports major identity providers:

  • Google Workspace - OAuth 2.0
  • Microsoft Azure AD - SAML 2.0 / OAuth 2.0
  • Okta - SAML 2.0
  • OneLogin - SAML 2.0
  • Auth0 - OAuth 2.0
  • Custom SAML - Any SAML 2.0 provider
  1. Create WorkOS Organization:

    • Log in to WorkOS Dashboard
    • Create organization for your company
    • Note organization ID
  2. Configure SSO Connection:

    • Select SSO provider type
    • Enter provider configuration (IdP URL, certificates, etc.)
    • Configure attribute mapping
  3. Update Environment:

    Terminal window
    WORKOS_PUBLIC_ORG=org_xxxxxxxxxxxxx
  4. Test SSO:

    Terminal window
    # Test authentication flow
    curl https://app.yourcompany.com/hub/auth/login

Querri uses JWT (JSON Web Tokens) for authentication:

{
"header": {
"alg": "RS256",
"typ": "JWT"
},
"payload": {
"sub": "user@company.com",
"email": "user@company.com",
"name": "John Doe",
"org": "org_xxxxxxxxxxxxx",
"is_admin": false,
"iat": 1640000000,
"exp": 1640086400
},
"signature": "..."
}

Tokens are generated by the hub service:

# Generate JWT token
import jwt
from datetime import datetime, timedelta
payload = {
'sub': user_email,
'email': user_email,
'name': user_name,
'org': organization_id,
'is_admin': is_admin,
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(hours=8)
}
token = jwt.encode(
payload,
private_key,
algorithm='RS256'
)

Traefik validates tokens using JWT plugin:

traefik/dynamic.yml:

http:
middlewares:
jwt-auth:
plugin:
jwt:
jwksUrl: ${WORKOS_JWKS_ENDPOINT}
required: true
forwardHeaders:
Authorization: "Bearer {token}"

Tokens expire after 8 hours. Refresh process:

  1. Client detects expired token (401 response)
  2. Redirect to /hub/auth/refresh
  3. Hub service validates refresh token
  4. New JWT token issued
  5. Client resumes with new token
  • Client-side: Secure HTTP-only cookie
  • Server-side: Redis for session management
Terminal window
# View active sessions in Redis
docker compose exec redis redis-cli
> KEYS session:*
> GET session:user@company.com

Querri uses OpenFGA for resource-level permissions.

FGA implements a tuple-based authorization model:

user:<email> <relation> <object_type>:<object_id>

Examples:

user:john@company.com owner project:abc123
user:jane@company.com editor project:abc123
user:bob@company.com viewer project:abc123
  • Full control over resource
  • Can delete resource
  • Can modify permissions
  • Can transfer ownership
  • Can view and modify resource
  • Can create child resources (e.g., steps in project)
  • Cannot delete resource
  • Cannot change permissions
  • Read-only access
  • Can view resource and data
  • Can export data
  • Cannot modify or delete

FGA queries determine access:

# Check if user can edit project
from openfga_sdk.client import OpenFgaClient
client = OpenFgaClient(configuration)
allowed = await client.check(
user="user:john@company.com",
relation="editor",
object="project:abc123"
)

Resources inherit permissions from parent:

Organization
↓ (all members)
Project
↓ (inherited)
Steps
↓ (inherited)
Results

Example:

  • User is organization member → Access to org resources
  • User is project owner → Owner of all project steps
  • Project shared with user → User can access all steps

Open Policy Agent enforces authorization policies.

opa/policies/authz.rego - General authorization:

package querri.authz
default allow = false
# Allow if user is admin
allow {
input.user.is_admin == true
}
# Allow if user owns resource
allow {
input.user.email == input.resource.owner
}
# Allow if user has permission via FGA
allow {
fga_check(input.user.email, input.permission, input.resource.id)
}

opa/policies/adminz.rego - Admin authorization:

package querri.admin
default allow_admin = false
# Allow if user email is @querri.com
allow_admin {
endswith(input.user.email, "@querri.com")
}
# Allow if user has is_admin flag
allow_admin {
input.user.is_admin == true
}

Test OPA policies locally:

Terminal window
# Test authorization policy
docker compose exec opa opa eval \
-d /policies/authz.rego \
-i '{"user": {"email": "john@company.com", "is_admin": false}, "resource": {"owner": "john@company.com"}}' \
'data.querri.authz.allow'

All API endpoints require authentication:

Terminal window
# API request with JWT
curl -X GET "https://app.yourcompany.com/api/projects" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

For programmatic access, users can generate API keys:

Terminal window
# Generate API key
curl -X POST "https://app.yourcompany.com/api/keys" \
-H "Authorization: Bearer JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Production API Key", "expires_in_days": 90}'

API key storage:

{
_id: "key_xxxxxxxxxxxxx",
user_email: "user@company.com",
name: "Production API Key",
key_hash: "sha256_hash_of_key",
created_at: "2024-01-15T10:00:00Z",
expires_at: "2024-04-15T10:00:00Z",
last_used: "2024-01-20T15:30:00Z"
}

Projects and dashboards can be shared via public links.

// Generate share link
db.share_links.insertOne({
_id: "share_xxxxxxxxxxxxx",
resource_type: "project",
resource_id: "project_uuid",
created_by: "user@company.com",
created_at: new Date(),
expires_at: new Date(Date.now() + 30*24*60*60*1000), // 30 days
access_level: "viewer",
password_hash: "optional_bcrypt_hash",
access_count: 0,
max_access_count: -1 // -1 = unlimited
})
https://app.yourcompany.com/share/share_xxxxxxxxxxxxx
  1. Expiration - Links expire after set period
  2. Password protection - Optional password requirement
  3. Access limits - Maximum number of views
  4. Revocation - Can be disabled anytime
  5. Audit logging - Track who accessed shared resource

Set organization-wide share link policies:

db.organizations.updateOne(
{_id: "org_xxxxxxxxxxxxx"},
{
$set: {
"settings.sharing": {
allow_public_sharing: true,
require_password: false,
default_expiration_days: 30,
max_expiration_days: 90,
allow_download: true
}
}
}
)
// Revoke specific share link
db.share_links.updateOne(
{_id: "share_xxxxxxxxxxxxx"},
{
$set: {
status: "revoked",
revoked_at: new Date(),
revoked_by: "admin@company.com"
}
}
)
// Revoke all share links for resource
db.share_links.updateMany(
{resource_id: "project_uuid"},
{
$set: {
status: "revoked",
revoked_at: new Date()
}
}
)

MongoDB supports encryption at rest:

docker-compose.yml:

mongo:
command:
- mongod
- --enableEncryption
- --encryptionKeyFile=/data/encryption.key
volumes:
- ./encryption.key:/data/encryption.key:ro

Generate encryption key:

Terminal window
# Generate 32-byte encryption key
openssl rand -base64 32 > encryption.key
chmod 400 encryption.key

For S3 storage, enable server-side encryption:

Terminal window
# S3 encryption configuration
AWS_S3_ENCRYPTION=AES256
AWS_S3_KMS_KEY_ID=arn:aws:kms:region:account:key/key-id

All communication encrypted via TLS:

  1. Client ↔ Traefik: HTTPS with TLS 1.2+
  2. Traefik ↔ Services: Internal HTTP (within Docker network)
  3. External APIs: HTTPS required

traefik/traefik.yml:

tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

Log all authentication events:

// Login event
db.audit_log.insertOne({
event_type: "authentication",
action: "login",
user_email: "user@company.com",
timestamp: new Date(),
ip_address: "203.0.113.42",
user_agent: "Mozilla/5.0...",
success: true
})

Log permission checks:

// Permission check
db.audit_log.insertOne({
event_type: "authorization",
action: "check_permission",
user_email: "user@company.com",
resource_type: "project",
resource_id: "project_uuid",
permission: "editor",
granted: true,
timestamp: new Date()
})

Log sensitive data access:

// Data access
db.audit_log.insertOne({
event_type: "data_access",
action: "view_project",
user_email: "user@company.com",
resource_id: "project_uuid",
ip_address: "203.0.113.42",
timestamp: new Date()
})
Terminal window
# View recent authentication events
docker compose exec mongo mongosh -u querri -p
> use querri
> db.audit_log.find({event_type: "authentication"}).sort({timestamp: -1}).limit(10)
# View failed login attempts
> db.audit_log.find({
event_type: "authentication",
action: "login",
success: false
}).sort({timestamp: -1})
# View user's activity
> db.audit_log.find({user_email: "user@company.com"}).sort({timestamp: -1})

For WorkOS-managed authentication:

  1. Minimum length: 12 characters
  2. Complexity: Upper, lower, number, special character
  3. Expiration: 90 days (optional)
  4. History: Prevent reuse of last 5 passwords
  5. Lockout: 5 failed attempts = 30 minute lockout

Configure in WorkOS Dashboard > Organization > Security Settings

Enable 2FA for admin users:

  1. Navigate to WorkOS Dashboard
  2. Select organization
  3. Enable 2FA requirement
  4. Users configure 2FA on next login (TOTP app or SMS)

Configure session timeout:

Terminal window
# Session expires after 8 hours of inactivity
SESSION_TIMEOUT_HOURS=8
db.organizations.updateOne(
{_id: "org_xxxxxxxxxxxxx"},
{
$set: {
"settings.security.session_timeout_minutes": 480
}
}
)

Force logout all sessions for a user:

Terminal window
# Clear user sessions from Redis
docker compose exec redis redis-cli
> DEL session:user@company.com

Limit concurrent sessions per user:

db.organizations.updateOne(
{_id: "org_xxxxxxxxxxxxx"},
{
$set: {
"settings.security.max_concurrent_sessions": 3
}
}
)

Restrict access to specific IP ranges:

db.organizations.updateOne(
{_id: "org_xxxxxxxxxxxxx"},
{
$set: {
"settings.security.allowed_ip_ranges": [
"203.0.113.0/24", // Office network
"198.51.100.0/24" // VPN network
]
}
}
)

Implement in Traefik:

traefik/dynamic.yml:

http:
middlewares:
ip-whitelist:
ipWhiteList:
sourceRange:
- "203.0.113.0/24"
- "198.51.100.0/24"
  • Review failed login attempts
  • Check for unusual access patterns
  • Monitor API usage spikes
  • Review user permissions
  • Audit admin user list
  • Review share links
  • Check for inactive users
  • Rotate JWT private keys
  • Update API credentials
  • Review and update security policies
  • Security audit of custom integrations
  • Full security audit
  • Penetration testing
  • Update SSL certificates (if not auto-renewed)
  • Review disaster recovery procedures
  1. Immediately disable account:

    db.users.updateOne(
    {user_email: "compromised@company.com"},
    {$set: {is_suspended: true}}
    )
  2. Revoke all sessions:

    Terminal window
    docker compose exec redis redis-cli DEL session:compromised@company.com
  3. Revoke API keys:

    db.api_keys.updateMany(
    {user_email: "compromised@company.com"},
    {$set: {revoked: true}}
    )
  4. Audit account activity:

    db.audit_log.find({
    user_email: "compromised@company.com",
    timestamp: {$gte: new Date(Date.now() - 7*24*60*60*1000)}
    }).sort({timestamp: -1})
  5. Reset credentials via WorkOS

  6. Document incident for security review

  1. Identify scope of breach
  2. Contain breach - Disable affected services
  3. Notify stakeholders per compliance requirements (GDPR, etc.)
  4. Forensic analysis - Review audit logs
  5. Remediation - Patch vulnerabilities
  6. Post-incident review - Update security policies
  • Data portability: Export user data on request
  • Right to deletion: Permanently delete user data
  • Consent management: Track data processing consent
  • Audit logging: Maintain access logs
  • Access controls: Role-based permissions
  • Encryption: Data at rest and in transit
  • Audit logging: Comprehensive activity logs
  • Incident response: Documented procedures

Issue: Users cannot log in

Check:

  1. WorkOS service status
  2. JWT validation configuration
  3. Certificate expiration
  4. Session timeout settings

Issue: Users cannot access resources they should have access to

Check:

  1. User organization membership
  2. FGA permission tuples
  3. OPA policy evaluation
  4. Resource ownership

Issue: Public share link returns error

Check:

  1. Link expiration date
  2. Link revocation status
  3. Password requirement (if set)
  4. Access count limits