Skip to main content
Bitrix has no OIDC client of its own, but it does have a stock social-authentication framework that Veriqa plugs into as an ordinary provider. The button appears in the standard sign-in form, and the platform itself handles finding, creating and linking accounts.
Verified on “Site Management: Business” with the Telegram and Email channels: sign-in, automatic user creation, linking in b_socialservices_user, and the button in the stock form.

What will not work

The built-in COpenIDClient is OpenID 2.0, not OpenID Connect: it has openid.ns and check_authentication, but no id_token, no JWKS and no PKCE. It is fundamentally incompatible with Veriqa. By default the sign-in form shows exactly the services of that era — Livejournal, Rambler, Blogger, Mail.Ru OpenID. The marketplace module anighr.openidconnect has not been tested: the marketplace requires a license key.

The path that works — a socialservices provider

The whole scheme fits into stock extension points:
  1. a handler for the OnAuthServicesBuildList event returns the provider descriptor (ID / CLASS / NAME / ICON) — after that the button shows up in the sign-in form by itself, with no template changes;
  2. the provider class returns the link to /connect/authorize and handles the return;
  3. CSocServAuth::AuthorizeUser() takes care of finding, creating and linking — the link lands in b_socialservices_user.
A ready-made OIDC example already ships with the platform: apple.php in the socialservices module.
Keep the callback in /local/, not in /bitrix/tools/oauth/ — the /bitrix/ directory is overwritten by updates. The public CSocServAuthManager::Authorize($id) is enough for this.

Two settings without which sign-in never completes

Without them AuthorizeUser returns SOCSERV_REGISTRATION_DENY, and from the outside that does not look like a configuration problem — it looks like a broken sign-in. On a production site mind the side effect: the first setting also enables Bitrix’s own registration.

The CUser::GetList trap — required reading

CUser::GetList does not understand the D7 '=FIELD' syntax: it silently drops filter keys it does not recognize and behaves as if there were no filter at all. Fetch() then returns the first row — the user with id 1, the administrator. Code that means “found an existing user and signed them in” turns into signing in as the administrator on every successful OIDC sign-in. There is no bug in Bitrix here: the platform writes those keys correctly. What breaks is mixing dialects in the integration code. The rule is simple: legacy keys with the legacy API, or D7 syntax with UserTable::getList() — never mixed. And look up “your” users through the links in b_socialservices_user, not through b_user fields.

Three more framework pitfalls

  • getUrl() is called twice per form render (GetFormHtml and GetOnClickJs) — memoize the state and PKCE generation per request, or the second call overwrites the first.
  • The core session is already open inside the provider, so you do not need one of your own. session_write_close() cuts off the Bitrix session in the middle of rendering the page.
  • session_name() changes for the whole process. Open your own session without restoring the name before prolog_before.php, and the core opens a session under the wrong cookie: the sign-in “disappears” on the next request.

Email

Bitrix requires an email only for a new user, and the requirement can be turned off with main.new_user_email_required. That is gentler than Drupal and Joomla, where the address is needed on every sign-in. When the address does not arrive as a claim (Telegram does not provide one), substitute a synthetic one using the same convention as on the other engines: {channel}-{id}@{channel}.veriqa.invalid. The .invalid domain is reserved by RFC 2606 — mail to such an address goes nowhere, and the account gets neither notifications nor password recovery. A deliberate trade-off, not a free workaround. On the Email channel the address arrives as a claim and no synthesis is needed.
The ownership marker is set by the platform: an account created through the framework gets b_user.EXTERNAL_AUTH_ID = socservices, put there by CSocServAuth::AuthorizeUser itself. Your own provider identifier never reaches that field, so searching by it is pointless.

A pleasant side effect

The stock checkOldUser recognizes “old scheme” accounts (EXTERNAL_AUTH_ID plus XML_ID directly in b_user) and moves them into the link table without creating duplicates — migrating from an older integration happens on its own.