Skip to main content

Auth

Overview

Lumio implements a multi-type authentication system with five distinct auth types, each serving a different use case. Authentication is handled by the lo-auth crate (token generation, validation, RBAC) and the API layer (OAuth flows, session management, middleware).

Auth Types

TypePrefixUse CaseRate Limit
System Keylm_sys_Internal service-to-service communicationUnlimited
User API Keylm_usr_External API access for users1200 req/min
JWTlm_ + eyJ...Session-based auth after OAuth login600 req/min
Popout Tokenlm_pop_Non-expiring overlay/OBS browser source access600 req/min
Anonymous(none)Unauthenticated public access120 req/min

Token type is automatically identified from the prefix via identify_token().

Architecture

Client Request
|
v
Auth Middleware (identify_token -> resolve AuthContext)
|
+-- lm_sys_* --> System Key lookup (config file)
+-- lm_usr_* --> User API Key lookup (DB, hash match)
+-- lm_eyJ* --> JWT validation (decode + verify)
+-- lm_pop_* --> Popout Token lookup (DB, hash match)
+-- (none) --> Anonymous
|
v
AuthContext (enum: System | ApiKey | User | PopoutToken | Anonymous)
|
v
Permission checks (PermissionGuard / require_permission)
|
v
Handler / Resolver

OAuth Login Flow

  1. User initiates login in the ID App (NextAuth-based).
  2. ID App handles OAuth with the provider (Twitch, YouTube/Google, Discord, Kick, Trovo).
  3. ID App calls the exchangeToken GraphQL mutation with:
    • Provider name and provider-specific user ID
    • OAuth access token
    • User profile (display_name, username, avatar_url, email)
  4. The API finds or creates the user via find_or_create_user_by_provider.
  5. A session is created in the database with a SHA-256 hash of the refresh token's jti.
  6. The session is cached in Redis.
  7. A JWT (short-lived) and refresh token (long-lived) are returned.

Token Refresh Flow

  1. Client sends expired JWT's refresh token to the refreshToken mutation.
  2. Refresh token is validated and the session is looked up by token hash.
  3. A new JWT is issued with the same session ID.
  4. The original refresh token is returned unchanged.

Logout Flow

  1. Client sends the refresh token to the logout mutation.
  2. Session is deleted from PostgreSQL and Redis.
  3. Returns success even if the token was already expired.

API

GraphQL Queries

QueryArgsReturnsGuard
me--MeResult!AuthGuard
myPermissions--[String!]!AuthGuard

MeResult includes:

  • User profile (id, displayName, email, avatarUrl, createdAt)
  • activeAccountId (nullable)
  • accounts: [AccountMembership!]! (role, plan, owner status, owner avatar)
  • permissions: [String!]! -- resolved account-scoped permissions
  • adminPermissions: [String!]! -- resolved admin-scope permissions
  • loginConnections: [LoginConnection!]!
  • enabledFeatures: [String!]!
  • token: String -- present only when updateMe switched the active account

GraphQL Mutations

MutationArgsReturnsGuard
exchangeTokeninput: ExchangeTokenInput!TokenResult!None (public)
refreshTokenrefreshToken: String!TokenResult!None (public)
logoutrefreshToken: String!LogoutResult!None (public)
logoutSession--LogoutResult!AuthGuard
disconnectLoginConnectionprovider: String!LogoutResult!AuthGuard
updateMeinput: UpdateMeInput!MeResult!AuthGuard

ExchangeTokenInput

input ExchangeTokenInput {
provider: String! # "twitch", "discord", "google"
providerId: String! # Provider-specific user ID
accessToken: String! # OAuth access token from provider
profile: ProviderProfileInput!
}

input ProviderProfileInput {
displayName: String!
username: String
avatarUrl: String
email: String
}

TokenResult

type TokenResult {
token: String! # Lumio JWT (short-lived, lm_ prefix)
refreshToken: String! # Refresh token (long-lived)
expiresAt: String! # JWT expiration (ISO 8601)
isNewUser: Boolean! # First login ever
hasAccount: Boolean! # User has at least one account
}

REST Endpoints

Auth REST endpoints are public (no permission guard) unless noted. They live under /v1/auth.

MethodPathDescription
POST/v1/auth/tokenIssue a JWT for a dashboard login (ID App -> API handshake).
POST/v1/auth/refreshExchange a refresh token for a new JWT.
POST/v1/auth/logoutInvalidate the current refresh token/session.
POST/v1/auth/authorizeOAuth 2.0 authorize handshake for downstream clients.
POST/v1/auth/token/exchangeExchange a provider OAuth token for a Lumio JWT.
POST/v1/auth/linkLink an additional provider identity to the authenticated user (JWT required).

Bodies are application/json with snake_case fields. Response shapes match the GraphQL TokenResult / LogoutResult types.

User / Session Endpoints

These live under /v1/users/me and are covered in detail in Sessions and the Users resource.

MethodPathPermissionDescription
GET/v1/users/meAuthCurrent user profile + permissions
PATCH/v1/users/meAuthUpdate display name / avatar
GET/v1/users/me/login-connectionsAuthList provider identities
GET/v1/users/me/sessionsAuthList active sessions
DELETE/v1/users/me/sessions/{id}AuthRevoke one session (ownership enforced in handler)
DELETE/v1/users/me/sessionsAuthRevoke all other sessions for the current user

AuthContext

The AuthContext enum is the core of the auth system, resolved by middleware and available in every handler/resolver:

enum AuthContext {
System { name, permissions },
ApiKey { user_id, account_id, label, permissions, rate_tier },
User { user_id, account_id, session_id, global_permissions, account_permissions },
PopoutToken { account_id, user_id, label, permissions },
Anonymous,
}

Permission Resolution

  • System -- Checked against the system key's configured permissions.
  • API Key -- Checked against the key's assigned permissions.
  • User (JWT) -- admin:* in global permissions grants everything. Otherwise, global permissions and account permissions are checked in order.
  • Popout Token -- Checked against the token's custom permission list.
  • Anonymous -- Always denied.

Wildcard support: events:* matches events:read, events:create, etc.

JWT Structure

Claims

FieldTypeDescription
subUUIDUser ID
account_idUUID?Active account (can be switched)
session_idUUID?References sessions table (JWT auth only)
iati64Issued at (Unix timestamp)
expi64Expiration (Unix timestamp)
jtiUUIDUnique token identifier (UUID v7)

All JWTs are prefixed with lm_ for type identification.

API Keys

System Keys (lm_sys_)

  • Generated via generate_system_api_key()
  • Configured in TOML config files
  • Used by internal services and bots
  • 32 random bytes, hex-encoded
  • Stored as SHA-256 hash for verification

User API Keys (lm_usr_)

  • Generated via generate_user_api_key()
  • Created by users in the dashboard
  • Bound to a user + account
  • Display prefix shows first 2 chars of the random part (e.g., lm_usr_ab)
  • Full key shown once, only hash stored in DB

Popout Tokens (lm_pop_)

  • Generated via generate_popout_token()
  • Non-expiring, bound to user + account
  • Used for OBS browser sources and dashboard popout windows
  • Limited, configurable permissions
  • 32 random bytes, hex-encoded, SHA-256 hashed for storage

Login Connections

Login connections link OAuth provider identities to Lumio users. Each connection stores:

  • Provider name and provider account ID
  • Username, display name, avatar URL
  • Encrypted OAuth tokens (access_token, refresh_token)
  • Scopes and token expiry

Supported providers: Twitch, YouTube (via Google), Discord, Kick, Trovo.

Key Files

FilePurpose
crates/lo-auth/src/lib.rsPublic API re-exports
crates/lo-auth/src/jwt.rsJWT creation, validation, refresh token generation
crates/lo-auth/src/context.rsAuthContext enum with permission checks
crates/lo-auth/src/api_key.rsAPI key generation, hashing, type identification
crates/lo-auth/src/popout_token.rsPopout token generation and validation
crates/lo-auth/src/rbac.rsPermission constants, default roles, plan limits
crates/lo-auth/src/error.rsAuth error types
apps/api/src/graphql/auth.rsGraphQL mutations (exchange, refresh, logout) and me query
apps/api/src/db/auth.rsUser, session, and login connection DB operations