Skip to content

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:

  1. The system checks for an unconfirmed profile with the same email address.
  2. If found: the unconfirmed profile is upgraded — its iss and sub are set to the real OIDC values, and confirmed is set to true.
  3. 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 iss value. Since unconfirmed users share iss = "-", 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 iss values). On first login, the system links the unconfirmed profile to the real identity.
  • Attempting to create a duplicate returns HTTP 409 Conflict with 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:

OIDC_CLAIM_UPSTREAM_ISSUER=original_issuer
OIDC_CLAIM_UPSTREAM_ID=original_sub

When configured, on each login the system:

  1. Reads the named claims from the ID token.
  2. Stores them in upstream_issuer and upstream_id.
  3. 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):

  1. User authenticates via the OIDC login flow.
  2. The backend creates a server-side session in Redis containing iss, sub, and tokens.
  3. A floh_sid httpOnly cookie is set.
  4. On subsequent requests, the session is loaded and the user is looked up by (iss, sub).

Bearer Tokens (API Clients)

  1. External client obtains an access token from the identity provider.
  2. Client sends Authorization: Bearer <token> with each request.
  3. Floh verifies the token signature, extracts iss (falling back to OIDC_ISSUER config) and sub.
  4. User is looked up by (iss, sub). If not found, a new user is auto-provisioned with the requestor role.

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.