User Profiles¶
Overview¶
Floh identifies users by their OIDC identity — specifically the combination of issuer (iss) and subject (sub). This composite key is globally unique and allows Floh to support multiple identity providers simultaneously without collisions, even when two providers happen to issue the same sub value.
Identity Model¶
Every user row stores the following identity fields:
| Column | Type | Nullable | Description |
|---|---|---|---|
iss |
VARCHAR(500) |
No | OIDC issuer URL, or "-" for unconfirmed users |
sub |
VARCHAR(500) |
No | OIDC subject identifier, or the email address for unconfirmed users |
email |
VARCHAR(255) |
No | User's email address |
display_name |
VARCHAR(255) |
No | Display name shown in the UI |
confirmed |
BOOLEAN |
No | true after first OIDC login; false for pre-provisioned users |
upstream_issuer |
VARCHAR(500) |
Yes | Original issuer when using an identity proxy |
upstream_id |
VARCHAR(500) |
Yes | Original subject identifier from the upstream provider |
Unique Constraints¶
Two composite unique indexes enforce identity uniqueness at the database level:
(iss, sub)— no two users can share the same issuer + subject pair(iss, email)— no two users can share the same issuer + email pair
These indexes cover all user types, including unconfirmed profiles, without relying on partial indexes.
User Lifecycle¶
┌───────────────┐
│ Admin creates │
│ unconfirmed │
│ profile │
└──────┬────────┘
│
▼
┌───────────────────────┐
│ Unconfirmed User │
│ iss = "-" │
│ sub = email │
│ confirmed = false │
└──────────┬────────────┘
│
First OIDC login
(email matches)
│
▼
┌───────────────────────┐
│ Confirmed User │
│ iss = <real issuer> │
│ sub = <real sub> │
│ confirmed = true │
└───────────────────────┘
Alternatively, users who log in via OIDC without a pre-existing profile are auto-provisioned directly as confirmed users.
Auto-Provisioning on First Login¶
When a user authenticates via OIDC and no matching (iss, sub) record exists:
- The system checks for an unconfirmed profile with the same email address.
- If found: the unconfirmed profile is upgraded — its
issandsubare set to the real OIDC values, andconfirmedis set totrue. - If not found: a new confirmed user record is created with the OIDC identity.
On subsequent logins, the existing user's email and display_name are updated from the latest OIDC claims.
Unconfirmed User Profiles¶
Administrators can pre-provision user profiles before the user has ever logged in. This is useful for:
- Pre-assigning roles so users have the correct access on first login
- Assigning users to workflow tasks or approvals before they have an account
- Referencing users by email in notification templates
Creating Unconfirmed Users¶
Via API:
curl -X POST https://floh.example.com/api/users \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "displayName": "Alice"}'
Via Admin Panel: Click "Create User" in the user management section, enter an email and optional display name.
Constraints¶
- The email must be unique across all users with the same
issvalue. Since unconfirmed users shareiss = "-", no two unconfirmed users can have the same email. - If a confirmed user with the same email already exists under a different issuer, the unconfirmed profile can still be created (they have different
issvalues). On first login, the system links the unconfirmed profile to the real identity. - Attempting to create a duplicate returns HTTP
409 Conflictwith the message:A user with the email address '<email>' already exists.
Upstream Identity (Identity Proxies)¶
When Floh sits behind an identity proxy such as Authifi, the tokens it receives have the proxy's iss and sub — not the original provider's. Two optional fields capture the original identity for verification and future use:
| Field | Configured via | Description |
|---|---|---|
upstream_issuer |
OIDC_CLAIM_UPSTREAM_ISSUER |
JWT claim containing the original issuer URL |
upstream_id |
OIDC_CLAIM_UPSTREAM_ID |
JWT claim containing the original subject ID |
Configuration¶
Set environment variables to specify which JWT claims contain the upstream identity:
When configured, on each login the system:
- Reads the named claims from the ID token.
- Stores them in
upstream_issuerandupstream_id. - Logs a warning if the stored upstream values differ from the incoming ones (indicating a potential account linkage issue).
When not configured, these fields remain null and have no effect.
Authentication Paths¶
Floh supports two authentication methods, both resolving users via (iss, sub):
Browser Sessions (Cookie-Based)¶
- User authenticates via the OIDC login flow.
- The backend creates a server-side session in Redis containing
iss,sub, and tokens. - A
floh_sidhttpOnly cookie is set. - On subsequent requests, the session is loaded and the user is looked up by
(iss, sub).
Bearer Tokens (API Clients)¶
- External client obtains an access token from the identity provider.
- Client sends
Authorization: Bearer <token>with each request. - Floh verifies the token signature, extracts
iss(falling back toOIDC_ISSUERconfig) andsub. - User is looked up by
(iss, sub). If not found, a new user is auto-provisioned with therequestorrole.
Development Mode¶
When OIDC_ISSUER is not set, authentication is bypassed. All requests use a built-in dev user (iss = "dev", sub = "dev-user") with admin privileges.
Legacy Users¶
Users that existed before the identity model migration have iss = "legacy". These users continue to function normally — they are matched by their (iss, sub) pair on login. If their actual OIDC issuer is different from "legacy", they will be treated as new users on next login and a new profile will be created. To avoid this, an administrator can manually update the iss field to the correct issuer URL.
Database Schema¶
The user table is defined in packages/server/src/db/schema/auth-tables.ts and the migration that introduced the identity fields is packages/server/src/db/migrations/009_user_identity.ts.
Migration 009: User Identity¶
Added columns: iss, upstream_issuer, upstream_id, confirmed
Changed constraints:
- Dropped the old UNIQUE(sub) constraint
- Added UNIQUE(iss, sub) index
- Added UNIQUE(iss, email) index
Data backfill: All existing users have iss set to "legacy" to distinguish them from users created after the migration.
Rollback¶
The migration's down function reverses all changes: drops the new indexes, restores the UNIQUE(sub) constraint, and removes the added columns.