OpenWebUI Accounts Are Provisioned Over SCIM Before The First Chat Login
Context
A user created in Keycloak and given AIHubAccess could open the chat and find the model picker empty ("No results found") until an administrator happened to change something access-related.
OpenWebUI only shows a user the models their groups are granted, and OpenWebuiProvisioner puts a user into the aihub:{tenant}:{role} groups by matching their Keycloak email against the accounts OpenWebUI lists over SCIM. Before this decision OpenWebUI created that account itself, on the user's first chat login. Two syncs could place a new user:
- The sync that
AccessChangeHookdebounces two seconds after the user's first Admin UI request creates theirUserTenantRoleEntity. - The sync that
WebhookControllerran when OpenWebUI posted its signup webhook.
When the chat login came after sync 1 had listed the accounts — the user lands on another Admin UI page first, or the iframe's OAuth round-trip is simply slower than two seconds — only sync 2 could place them. OpenWebUI 0.11.3 silently removed that path, which was verified against the running image:
utils/webhook.py::post_webhooknow runsvalidate_url, which rejects every non-global address unlessENABLE_LOCAL_WEB_FETCHis set.WEBHOOK_URLpoints athttp://localhost:8000(dev) orhttp://api:8000(every other stage), so each event is dropped withWebhook skipped, URL invalid or not publicly resolvable.- The payload switched to an event schema (
event: user.created,actor,subject) with noactionfield, whichOpenWebuiWebhookPayloadrequires, so it would be rejected even if it arrived.
Reproduced on a fresh OpenWebUI 0.11.3 database: a user opening the chat ten seconds after their first Admin UI load ended up in no group; one opening it after half a second ended up in theirs.
Decision Drivers
- No timing dependency. Whether a user can use the chat must not depend on how fast their browser reaches it.
- Keep OpenWebUI's SSRF protection. Restoring the webhook needs
ENABLE_LOCAL_WEB_FETCH=true, which also lets RAG web fetches and per-user webhooks (ENABLE_USER_WEBHOOKS: True) reach internal services. - No OpenWebUI fork, and no change to how access is computed — only to when an account exists.
Decision
The provisioner creates the OpenWebUI account of every Keycloak user about to join an aihub:{tenant}:{role} group who has none yet, over SCIM, inside the group sync, and adds it to its groups in the same pass.
_sync_groups_lockedcomputes each group's members first — users holding the role in the tenant that is their active tenant — and_provision_missing_accountsonly creates accounts for them, skipping users without an email and disabled users. An account outside every group would see no model, and a user who gets a role or an active tenant later joins a group through the sync that change triggers. Accounts are matched by email, falling back to the SCIM external id once the email changed in Keycloak.OpenWebuiClient.create_usersends the KeycloaksubasexternalId. OpenWebUI's OAuth login links to that account instead of creating a second one (OAUTH_MERGE_ACCOUNTS_BY_EMAILstays on). If the login created the account first, OpenWebUI answers 409; the client then reuses the account it finds byexternalId, falling back to email. OpenWebUI matches theexternalIdfilter against the OAuth sub too, so the lookup also finds an account whose email changed in Keycloak after its first chat login — OpenWebUI never updates it (OAUTH_UPDATE_EMAIL_ON_LOGINis off), and an unresolved 409 aborts the group sync for every user.- OpenWebUI runs with
SCIM_AUTH_PROVIDER: oidc. Without it, 0.11.3 cannot record the external id: the create request fails with a 500, and the account never appears in the SCIM list, becauseget_scim_usersonly returns accounts linked to an OAuth or SCIM identity. sync_accessnow waits for a running access sync instead of skipping it, for the same reason: the running sync may have read its inputs before the change that triggered the waiting one.
Consequences
Positive
- A new user sees their models on their first chat visit, whenever it happens.
- Users already stuck are repaired by the startup
provision()of the release that ships this. - The ADR
2026_03_05limitation "email-based user mapping requires users to have logged into both systems" is gone.
Negative
- Every user who is a member of a group now has an OpenWebUI account, including users who only use the Admin UI.
- A provisioned account has role
useruntil its owner's first chat login, where OAuth role management appliesAIHubSysAdmin→adminas before. sync_accesscallers can queue behind one another; the debounce inAccessChangeHookkeeps bulk changes to one run.- The signup webhook (
WebhookController,WEBHOOK_URL,OPENWEBUI_WEBHOOK_SECRET) no longer does anything and is left for a separate removal.
Risks
- Like ADR
2026_09_04, this rests on OpenWebUI internals verified by reading and running the 0.11.3 image (routers/scim.py,models/users.py), not on a published contract. - An OpenWebUI account linked to neither an OAuth nor a SCIM identity (one added by hand in the admin panel) that shares a Keycloak user's email still fails the sync: the create conflicts on the email, but
get_scim_usershides the account from both lookups. It resolves once its owner logs into the chat, which links it by email.
