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
| Type | Prefix | Use Case | Rate Limit |
|---|---|---|---|
| System Key | lm_sys_ | Internal service-to-service communication | Unlimited |
| User API Key | lm_usr_ | External API access for users | 1200 req/min |
| JWT | lm_ + eyJ... | Session-based auth after OAuth login | 600 req/min |
| Popout Token | lm_pop_ | Non-expiring overlay/OBS browser source access | 600 req/min |
| Anonymous | (none) | Unauthenticated public access | 120 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
- User initiates login in the ID App (NextAuth-based).
- ID App handles OAuth with the provider (Twitch, YouTube/Google, Discord, Kick, Trovo).
- ID App calls the
exchangeTokenGraphQL mutation with:- Provider name and provider-specific user ID
- OAuth access token
- User profile (display_name, username, avatar_url, email)
- The API finds or creates the user via
find_or_create_user_by_provider. - A session is created in the database with a SHA-256 hash of the refresh token's
jti. - The session is cached in Redis.
- A JWT (short-lived) and refresh token (long-lived) are returned.
Token Refresh Flow
- Client sends expired JWT's refresh token to the
refreshTokenmutation. - Refresh token is validated and the session is looked up by token hash.
- A new JWT is issued with the same session ID.
- A new refresh token is issued with the remaining session lifetime (refresh token rotation). The session's
token_hashis updated atomically, invalidating the old refresh token.
Proxy-Level Auto-Refresh
All three Next.js apps (web, admin, ID) run a proxy.ts that intercepts every request and proactively refreshes the JWT when it is within 60 seconds of expiry. The proxy calls the GraphQL refreshToken mutation directly and sets updated cookies on the response. The user experiences no interruption.
If the API is unreachable during a refresh attempt, the proxy passes the request through without logging out — the page renders and shows a service-unavailable error instead.
Client-Side Refresh Timer
The web and admin apps run a useTokenRefresh() hook that proactively refreshes the JWT for long-lived pages (popouts, chat, dashboards left open overnight). After a successful refresh, it schedules the next refresh 5 minutes before the new token expires. The timer only fires when the browser tab is visible.
Logout Flow
- Client sends the refresh token to the
logoutmutation. - Session is deleted from PostgreSQL and Redis.
- Returns success even if the token was already expired.
API
GraphQL Queries
| Query | Args | Returns | Guard |
|---|---|---|---|
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 permissionsadminPermissions: [String!]!-- resolved admin-scope permissionsloginConnections: [LoginConnection!]!enabledFeatures: [String!]!token: String-- present only whenupdateMeswitched the active account
GraphQL Mutations
| Mutation | Args | Returns | Guard |
|---|---|---|---|
exchangeToken | input: ExchangeTokenInput! | TokenResult! | None (public) |
refreshToken | refreshToken: String! | TokenResult! | None (public) |
logout | refreshToken: String! | LogoutResult! | None (public) |
logoutSession | -- | LogoutResult! | AuthGuard |
disconnectLoginConnection | provider: String! | LogoutResult! | AuthGuard |
updateMe | input: 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.
| Method | Path | Description |
|---|---|---|
POST | /v1/auth/token | Issue a JWT for a dashboard login (ID App -> API handshake). |
POST | /v1/auth/refresh | Exchange a refresh token for a new JWT. |
POST | /v1/auth/logout | Invalidate the current refresh token/session. |
POST | /v1/auth/authorize | OAuth 2.0 authorize handshake for downstream clients. |
POST | /v1/auth/token/exchange | Exchange a provider OAuth token for a Lumio JWT. |
POST | /v1/auth/link | Link 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.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/users/me | Auth | Current user profile + permissions |
PATCH | /v1/users/me | Auth | Update display name / avatar |
GET | /v1/users/me/login-connections | Auth | List provider identities |
GET | /v1/users/me/sessions | Auth | List active sessions |
DELETE | /v1/users/me/sessions/{id} | Auth | Revoke one session (ownership enforced in handler) |
DELETE | /v1/users/me/sessions | Auth | Revoke 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
| Field | Type | Description |
|---|---|---|
sub | UUID | User ID |
account_id | UUID? | Active account (can be switched) |
session_id | UUID? | References sessions table (JWT auth only) |
iat | i64 | Issued at (Unix timestamp) |
exp | i64 | Expiration (Unix timestamp) |
jti | UUID | Unique 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
| File | Purpose |
|---|---|
crates/lo-auth/src/lib.rs | Public API re-exports |
crates/lo-auth/src/jwt.rs | JWT creation, validation, refresh token generation |
crates/lo-auth/src/context.rs | AuthContext enum with permission checks |
crates/lo-auth/src/api_key.rs | API key generation, hashing, type identification |
crates/lo-auth/src/popout_token.rs | Popout token generation and validation |
crates/lo-auth/src/rbac.rs | Permission constants, default roles, plan limits |
crates/lo-auth/src/error.rs | Auth error types |
apps/api/src/graphql/auth.rs | GraphQL mutations (exchange, refresh, logout) and me query |
apps/api/src/db/auth.rs | User, session, and login connection DB operations |