Authentication & Authorization System
Overview
Heimdall uses a comprehensive authentication and authorization system supporting:
- OAuth Users - Multiple OAuth providers (Twitch, Discord, GitHub, YouTube/Google, Kick, Steam, Trovo). Seeded platforms:
email,twitch,discord,github,google(YouTube),kick,steam,trovo—github,kick,steam, andtrovoship disabled (enabled = false) - Email/Password - Traditional authentication with email verification
- Two-Factor Authentication - TOTP-based 2FA with backup codes
- API Keys - Programmatic access with scopes and permissions
- RBAC - Role-Based Access Control with granular permissions
- GDPR Compliance - Data export, account deletion, privacy controls
Architecture
Entities
User
├── Sessions (NextAuth managed)
├── API Keys (user-owned)
├── User Roles (many-to-many)
├── Two-Factor Auth (TOTP secret, backup codes)
├── Platform Accounts (OAuth + email/password credentials)
├── Connected Apps (OAuth consents)
└── Data Export Logs
API Key
├── Owner (User, optional for system keys)
├── Scopes (array of scope strings)
└── API Key Roles (many-to-many)
Role
├── Name (e.g., "Admin", "Viewer", "GPS Manager")
├── Description
├── Requires 2FA (boolean)
└── Permissions (many-to-many)
Permission
├── Resource (e.g., "gps", "users", "settings")
├── Action (e.g., "read", "write", "delete")
├── Description
Authentication Flow
1. User Authentication (OAuth)
Frontend (Next.js) → NextAuth → OAuth Provider (Twitch/Discord/YouTube/GitHub)
↓
Session Created
↓
JWT Token (NextAuth)
↓
API Request → Bearer Token → Validate Session → Check 2FA → User Context
2. User Authentication (Email/Password)
Frontend (Next.js) → Login Form → API Validate Credentials
↓
Check 2FA Required
↓
Verify TOTP/Backup Code
↓
Session Created
3. API Key Authentication
API Request → Bearer Token → Lookup API Key → Check Active
↓
User Context (if owned)
↓
API Key Context
4. Two-Factor Authentication Flow
Login Success → Check 2FA Enabled
↓
┌──────┴──────┐
│ │
2FA Enabled 2FA Disabled
│ │
▼ ▼
Show 2FA Form Grant Access
│
▼
Verify Code
(TOTP or Backup)
│
▼
Grant Access
Authorization Flow
Request → Auth Context (User/API Key)
↓
Get Roles
↓
Get Permissions
↓
Check Permission (resource:action)
↓
Allow/Deny
Database Schema
Users
The User table is a clean identity record. Credentials (passwords, OAuth tokens) live in PlatformAccount, and 2FA data lives in UserTwoFactor / TwoFactorBackupCode.
CREATE TABLE "User" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
primary_platform_account_id UUID, -- FK to PlatformAccount
privacy_mode BOOLEAN NOT NULL DEFAULT false,
preferred_locale VARCHAR(5) DEFAULT 'en',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMPTZ,
last_login_platform_account_id UUID, -- FK to PlatformAccount
deleted_at TIMESTAMPTZ -- soft delete (NULL = active)
);
Platform Accounts (OAuth + Email/Password Credentials)
All user credentials and linked accounts — both OAuth accounts and email/password accounts — live here. There is no separate LinkedAccount table.
CREATE TABLE "PlatformAccount" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform_id TEXT NOT NULL REFERENCES "Platform"(id) ON DELETE CASCADE,
platform_user_id TEXT NOT NULL, -- OAuth: external ID; email: the email address
username TEXT NOT NULL, -- display name
email TEXT,
avatar_url TEXT,
password_hash TEXT, -- Argon2 (email platform only, NULL for OAuth)
email_verified_at TIMESTAMPTZ, -- email platform only
user_id UUID NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(platform_id, platform_user_id)
);
Sessions (OAuth login flow)
CREATE TABLE "Session" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
session_token TEXT UNIQUE NOT NULL,
expires TIMESTAMPTZ NOT NULL,
ip_address TEXT,
user_agent TEXT,
last_activity_at TIMESTAMPTZ DEFAULT NOW(),
provider TEXT, -- auth provider slug (twitch, discord, email, ...)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
API Keys
Keys are hashed with SHA-256; only the hash and a short display prefix are stored.
CREATE TABLE "ApiKey" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key_hash TEXT UNIQUE NOT NULL, -- SHA-256 hash of the API key
key_prefix TEXT NOT NULL, -- first 11 chars for display (e.g. "hm_abc12345")
name TEXT NOT NULL,
description TEXT,
user_id UUID REFERENCES "User"(id) ON DELETE CASCADE,
scopes TEXT[] NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT true,
is_system BOOLEAN NOT NULL DEFAULT false, -- system keys (device/service auth) cannot be deleted via API
expires_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Roles
CREATE TABLE "Role" (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
is_system BOOLEAN NOT NULL DEFAULT false,
requires_two_factor BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Permissions
CREATE TABLE "Permission" (
id TEXT PRIMARY KEY,
resource TEXT NOT NULL,
action TEXT NOT NULL,
description TEXT,
admin_only BOOLEAN NOT NULL DEFAULT false, -- hide from non-admins in the UI
display_order INTEGER NOT NULL DEFAULT 0, -- sort order within a resource group
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(resource, action)
);
Junction Tables
CREATE TABLE "UserRole" (
user_id UUID NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
role_id TEXT NOT NULL REFERENCES "Role"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE "ApiKeyRole" (
api_key_id UUID NOT NULL REFERENCES "ApiKey"(id) ON DELETE CASCADE,
role_id TEXT NOT NULL REFERENCES "Role"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (api_key_id, role_id)
);
CREATE TABLE "RolePermission" (
role_id TEXT NOT NULL REFERENCES "Role"(id) ON DELETE CASCADE,
permission_id TEXT NOT NULL REFERENCES "Permission"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (role_id, permission_id)
);
OAuth Consents (Connected Apps)
CREATE TABLE "OAuthConsent" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
client_id UUID NOT NULL REFERENCES "OAuthClient"(id) ON DELETE CASCADE,
scopes TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, client_id)
);
Data Export Logs
CREATE TABLE "DataExportLog" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
exported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
export_format VARCHAR(50) NOT NULL DEFAULT 'zip',
file_size_bytes BIGINT,
locale VARCHAR(10),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Default Roles
Role names are case-sensitive. Always use the exact capitalization shown below when checking roles programmatically (e.g., "Admin" not "admin").
Best Practice: Use role_ids instead of roles (names) for programmatic role checks. Role IDs are:
- Immutable - They never change, even if role names are renamed
- Case-insensitive - No capitalization issues
- Stable - Safe for long-term use in code
The API returns both roles (names for display) and role_ids (IDs for programmatic use).
System Roles
| Role Name | ID | Description | Permissions | Requires 2FA |
|---|---|---|---|---|
| Super Admin | role_super_admin | Full system access, bypasses all permission checks | *:* (wildcard) | Yes |
| Admin | role_admin | Full administrative access | *:* (wildcard) | Yes |
| Moderator | role_moderator | Content moderation and GPS management | gps:read, gps:write, gps:delete, users:read, users:ban, stats:read, backend:access, storage:read, storage:write, storage:edit, storage:download, storage:delete, discord:read, discord:guild.read, discord:guild.sync, discord:guild.warn, discord:guild.timeout, discord:guild.kick, discord:guild.ban | Yes |
| User | role_user | Standard user access | (none — users always manage their own 2FA/account) | No |
| Developer | role_developer | OAuth and API key management (external developers) | oauth_clients:read, oauth_clients:create, oauth_clients:update, oauth_clients:delete, api_keys:read, api_keys:write, api_keys:delete, backend:access | Yes |
API Roles
| Role Name | ID | Description | Permissions |
|---|---|---|---|
| API Full Access | role_api_full_access | Full API access | *:* (wildcard) |
| API Read Only | role_api_read_only | Read-only API access | gps:read, users:read, roles:read, permissions:read, api_keys:read, settings:read, stats:read, oauth_clients:read, two_factor:read, storage:read, storage:download, devices:read, trips:read, trip_categories:read, locations:read, geofences:read, pegel:read |
Super Admin
A user becomes a super admin if any of these conditions are true:
- Has the
role_super_adminrole - TheSuper Adminrole with IDrole_super_admin - Has a role named
super_admin(case-sensitive) - Legacy support - Has the
*:*permission - The wildcard permission granting access to all resources
Super admin users:
- Have
is_super_admin: truein their permissions response - Bypass all permission checks
- Can access all resources and actions
- Can modify system roles
Default Permissions
GPS Resource
gps:read- View GPS datagps:write- Create/update GPS datagps:delete- Delete GPS data
Users Resource
users:read- View usersusers:write- Create/update usersusers:delete- Delete users
Roles Resource
roles:read- View rolesroles:write- Create/update rolesroles:delete- Delete roles
Permissions Resource
permissions:read- View permissionspermissions:write- Create/update permissionspermissions:delete- Delete permissions
API Keys Resource
api_keys:read- View API keysapi_keys:write- Create/update API keysapi_keys:delete- Delete API keys
Settings Resource
settings:read- View settingssettings:write- Create/update settingssettings:delete- Delete settings
Stats Resource
stats:read- View statistics
Storage Resource
storage:read- View storage files and metadatastorage:write- Upload files to storagestorage:edit- Edit file metadata (rename, move, update metadata)storage:download- Download files from storagestorage:delete- Delete files from storage
Two-Factor Auth Resource
two_factor:read- View 2FA status for other users (users can always view their own)two_factor:write- Modify 2FA for other users (users can always manage their own)two_factor:manage- Manage 2FA for any user (admin only)
OAuth Clients Resource
oauth_clients:read- View OAuth clientsoauth_clients:create- Create OAuth clientsoauth_clients:update- Update OAuth clientsoauth_clients:delete- Delete OAuth clients
Audit Resource
audit:read- Read all audit logs (admin view)audit:write- Write audit events (internal apps)
Backend Resource
backend:access- Access the backend application
Discord Resource
discord:read- Main Discord access (sidebar, guilds list, bot settings)discord:edit- Edit Discord bot settingsdiscord:delete- Delete Discord guilds from the databasediscord:bot.admin- Access Discord bot admin commandsdiscord:guild.read- View guild data, members, channels, rolesdiscord:guild.edit- Edit guild data and member informationdiscord:guild.sync- Resync a guild (channels, roles, members)discord:guild.warn- Warn Discord membersdiscord:guild.timeout- Timeout Discord membersdiscord:guild.kick- Kick Discord membersdiscord:guild.ban- Ban Discord members
Integrations Resource
integrations:read- View platform integrationsintegrations:manage- Connect/disconnect platform integrations
Devices Resource
devices:read- View devicesdevices:write- Create/update devicesdevices:delete- Delete devices
Trips Resource
trips:read- View tripstrips:write- Create/update tripstrips:delete- Delete trips
Trip Categories Resource
trip_categories:read- View trip categoriestrip_categories:write- Create/update trip categoriestrip_categories:delete- Delete trip categories
Locations Resource
locations:read- View locationslocations:write- Create/update locationslocations:delete- Delete locations
Geofences Resource
geofences:read- View geofencesgeofences:write- Create/update geofencesgeofences:delete- Delete geofences
Pegel Resource
pegel:read- View water level (PegelOnline) data
API Key Scopes
Scopes provide additional restrictions beyond permissions:
gps:read- Read GPS datagps:write- Write GPS datausers:read- Read usersusers:write- Write userssettings:read- Read settingssettings:write- Write settings*- All scopes (admin keys)
Scopes act as an additional layer: an API key needs both the scope AND the permission through roles.
Auth Context
Auth logic lives in the heimdall-auth crate (crates/heimdall-auth/), which provides OAuth 2.0 / OpenID Connect flows (PKCE, access/refresh/ID tokens), RBAC permission checking, TOTP/2FA, and cryptographic utilities. The AuthContext type itself is defined in heimdall-db::models.
Auth middleware lives in heimdall-rest::middleware and OAuth HTTP endpoints live in heimdall-rest::oauth.
// crates/heimdall-db/src/models/auth.rs
pub struct AuthContext {
pub user: Option<User>,
pub api_key: Option<ApiKey>,
pub session_id: Option<Uuid>, // DB session ID (session-based auth)
pub roles: Vec<String>, // role names (for display)
pub role_ids: Vec<String>, // role IDs (for programmatic checks)
pub permissions: Vec<String>, // ["gps:read", "gps:write"]
pub scopes: Vec<String>, // API key / OAuth scopes
pub is_super_admin: bool,
}
impl AuthContext {
pub fn has_permission(&self, resource: &str, action: &str) -> bool;
pub fn has_scope(&self, scope: &str) -> bool;
pub fn can(&self, resource: &str, action: &str) -> bool;
}
Endpoints
Authentication
POST /v1/auth/session- Validate next-auth sessionPOST /v1/auth/refresh- Refresh session
Users
GET /v1/users- List users (users:read)GET /v1/users/:id- Get user (users:read)PATCH /v1/users/:id- Update user (users:write)DELETE /v1/users/:id- Delete user (users:delete)GET /v1/users/:id/roles- Get user roles (users:read)POST /v1/users/:id/roles- Add role to user (users:write)DELETE /v1/users/:id/roles/:roleId- Remove role (users:write)
API Keys
GET /v1/api-keys- List API keys (api_keys:read)POST /v1/api-keys- Create API key (api_keys:write)GET /v1/api-keys/:id- Get API key (api_keys:read)PATCH /v1/api-keys/:id- Update API key (api_keys:write)DELETE /v1/api-keys/:id- Delete API key (api_keys:delete)POST /v1/api-keys/:id/rotate- Rotate key (api_keys:write)
Roles
GET /v1/roles- List roles (roles:read)POST /v1/roles- Create role (roles:write)GET /v1/roles/:id- Get role (roles:read)PATCH /v1/roles/:id- Update role (roles:write)DELETE /v1/roles/:id- Delete role (roles:delete)GET /v1/roles/:id/permissions- Get role permissions (roles:read)POST /v1/roles/:id/permissions- Add permission (roles:write)DELETE /v1/roles/:id/permissions/:permId- Remove permission (roles:write)
Permissions
GET /v1/permissions- List permissions (permissions:read)POST /v1/permissions- Create permission (permissions:write)GET /v1/permissions/:id- Get permission (permissions:read)DELETE /v1/permissions/:id- Delete permission (permissions:delete)
Two-Factor Authentication
GET /v1/2fa/status- Get 2FA statusPOST /v1/2fa/setup- Generate TOTP secret and QR codePOST /v1/2fa/enable- Enable 2FA with verification codePOST /v1/2fa/disable- Disable 2FAPOST /v1/2fa/verify- Verify TOTP codeGET /v1/2fa/backup-codes- Get backup codesPOST /v1/2fa/backup-codes/regenerate- Regenerate backup codes
GDPR / Data Management
GET /v1/user/export- Export user data (GDPR)GET /v1/user/export/logs- Get export historyPOST /v1/user/delete- Request account deletionDELETE /v1/user/delete- Cancel account deletionGET /v1/user/deletion-status- Get deletion status
Connected Apps (OAuth Consents)
GET /v1/user/connected-apps- List connected appsDELETE /v1/user/connected-apps/:id- Revoke app accessDELETE /v1/user/connected-apps- Revoke all app access
Security Considerations
-
API Keys
- Generate using cryptographically secure random
- Store hashed version (SHA-256)
- Prefix with
hm_for identification - Support expiration dates
- Track last used timestamp
-
Sessions
- Managed by NextAuth
- JWT tokens with short expiration
- Secure httpOnly cookies
- App-specific cookie names to prevent session sharing:
- ID App:
id.session-token - Backend:
backend.session-token - Policies:
policies.session-token
- ID App:
-
Two-Factor Authentication
- TOTP-based (RFC 6238)
- 30-second time window
- Compatible with Google Authenticator, Authy, 1Password
- 10 single-use backup codes
- Role-based 2FA requirements
-
Permissions
- Cached per request to avoid N+1 queries
- Wildcard support (
*:*,gps:*) - Hierarchical checking (most specific first)
-
Rate Limiting
- Per user/API key
- Different limits for different roles
- Stricter limits for write operations
-
GDPR Compliance
- Data export in structured format (JSON)
- Account deletion with 30-day grace period
- User anonymization on deletion
- Privacy mode to hide sensitive data
- Audit logging of data exports