Bitween supports four ways to sign in.
POST /api/accounts/login with { "Username": "...", "Password": "..." }.
- Five wrong passwords from one address lock that address out of the account for 15 minutes, and 20 from anywhere lock the account itself for 15 minutes. An administrator can unlock it early.
- Disabled accounts cannot sign in.
- Changing your own password checks the old one, and that check is limited as sign-in is: five wrong ones from one address lock the change out for 15 minutes, so a session left open can't be used to guess the password.
- Passwords need at least 8 characters, including an upper-case letter, a lower-case letter, a digit and a symbol. When an administrator sets someone else's password, only the 8-character minimum applies.
- Passwords are hashed with PBKDF2-HMAC-SHA256, 210,000 iterations and a random salt, and compared in constant time. Hashes stored before September 2026 use PBKDF2-SHA1 with 10,000 iterations. They still verify, and each is replaced with the current hash the next time that person signs in with their password.
- With a mail server and a public address, the sign-in page offers Forgot password?: see Invites and password resets. Without them, an administrator sets a new password instead.
Turning on the Microsoft sign-in only setting disables this method, for Microsoft and OpenID Connect sign-in alike. New accounts are then created without a password.
Account emails go to one person, through the mail server set in Settings, Mail server (the one Email channels use), never through a notification channel.
- Invites. A member can be added with an invite instead of a password (
POST /api/accountswithinvite: true, thenPOST /api/accounts/{id}/invite). They choose their own password from a link that works once, for 48 hours. With a mail server the link is emailed; without one it is handed back once to whoever invited, to pass on. Sending another invite stops the earlier link working. Inviting needsusers.create, and, as setting a password does, every permission the member holds. On an instance with Microsoft sign-in only an invite is an email saying where to sign in, and needs a mail server. - Resets. Forgot password? emails a link that works once, for one hour. It needs a mail server and
Bitween:PublicUrl: a link built on the address a request came in on could be pointed at another site with a forgedHostheader. The answer is the same whether the address has an account or not, and the email is sent after the answer, so neither says who has one. A member gets at most one reset email a minute. - Using a link sets the password, clears a lockout, and ends every session the member had. Every other link they had stops working.
- Notices. A member is emailed when their password is changed (by them, by an administrator, or with a reset link) and when their account is locked after 20 wrong passwords.
- Only a SHA-256 hash of each link is stored, so the database can't be used to sign in.
POST /api/accounts/forgotPassword,passwordLinkandusePasswordLinkneed no session, and share the sign-in rate limit.
The sign-in page offers Microsoft once the Azure AD client id, tenant id and redirect URI settings are all set.
- The browser signs in through an MSAL popup and gets an ID token.
- It posts
{ "MsToken": "<id token>" }toPOST /api/accounts/login. - Bitween validates the token's signature and lifetime against Microsoft's published signing keys, and checks that it was issued for this app's client id by the token's own tenant, pinned to the configured tenant when that is a tenant id.
- It signs in the account bound to the token's Microsoft identity (object id and tenant). An account with none bound yet is found by the token's address, and the identity is bound to it on that first sign-in; after that, another Microsoft identity with the same address is refused.
Accounts are never created automatically. Add each person on the Team page first.
The redirect URI must be /blank.html on the Bitween host, such as https://bitween.example.com/blank.html, and must be registered on the Azure AD app. Other pages send a cross-origin opener policy that breaks the popup. Bitween logs a warning at startup when the redirect URI does not end in /blank.html.
The sign-in page offers any OpenID Connect provider, such as Keycloak, Okta, Entra ID, Google or ADFS, once its Provider address (the issuer) and Client ID are set under Settings, Single sign-on (OpenID Connect). The button reads "Continue with" the Button label, or "single sign-on". It can be offered beside Microsoft and passwords, or alone.
POST /api/accounts/oidcstartreturns the provider's sign-in address, read from its/.well-known/openid-configuration, for the authorization code flow with PKCE. The state, nonce and PKCE verifier go in an HttpOnly cookie encrypted with a key derived fromToken:Key, valid for 10 minutes, so any node can finish a sign-in another started.- The provider sends the browser back to
/oidc-callback, which posts{ "OidcCode": "...", "OidcState": "..." }toPOST /api/accounts/login. - Bitween checks the state against the cookie, exchanges the code at the provider's token endpoint itself, with the verifier and, for a confidential client, the Client secret, and validates the ID token: its signature against the provider's keys, its issuer, its audience (the client id), its lifetime and its nonce.
- It signs in the account bound to the token's subject and issuer. An account with none bound is found by the token's
email, unless the provider says the address isn't verified (email_verifiedfalse), and the identity is bound on that first sign-in. After that another identity with the same address is refused.
Register https://<bitween host>/oidc-callback as the redirect URI at the provider, or set Redirect URI when the public address isn't the one requests arrive on. The scopes asked are openid email profile unless Scopes says otherwise. Accounts are never created automatically.
POST /api/login signed in against Bitween:AdminCredentials and returned a token with every
permission, without touching the database and with no lockout. It shipped with a working default -
admin:1234512345, published in this repository - and neither the current nor the legacy UI ever
called it. A penetration test signed in with it. The endpoint and the setting are gone.
There is now no sign-in that bypasses the database. If the last account holding Administrator loses the role, no request can restore it and the database has to be repaired directly.
- Sign-in returns
{ "jwt": "..." }. Send it asAuthorization: Bearer <jwt>. - The JWT is signed with
Token:Keyand carriesToken:IssuerandToken:Audience. It lasts for the Sign-in session length setting, 60 minutes by default. - Sign-in also sets a
refresh_tokencookie that isHttpOnly,SecureandSameSite=Lax, valid for 30 days. Posting to the login endpoint with that cookie and no credentials returns a new JWT and replaces the refresh token. A client without cookies, such as the bitween CLI, sends the token asrefreshTokenin the body instead. Each renewal issues a new refresh token, so the 30 days are a limit on inactivity. POST /api/accounts/logoutdeletes the refresh token and tells the browser to clear site data. The JWT stays valid until it expires. A client without cookies sends the refresh token to end in the body;bitween logoutdoes.- The bitween CLI signs in through the browser: the admin UI's
/cli-loginpage asks the signed-in member to confirm, then hands the CLI a one-time code on its loopback (127.0.0.1), or shows it to paste. The code is signed with a key derived fromToken:Key, so any node can redeem it and it can't be used as an access token; it lasts two minutes and is redeemed only with the verifier the CLI kept (PKCE S256). It works for every account, Microsoft-only ones included. An account that must change its password can't confirm until it has. - The admin UI signs out after 30 minutes without activity in any tab.
- Permissions are not in the token. They are read from the database on every request, so removing a role takes effect at once.
A permission is written area.action. A member holds the union of their roles' permissions.
| Group | Area | Actions |
|---|---|---|
| Operate | exchanges |
view, operate |
| Operate | monitoring |
view |
| Operate | dashboard |
view |
| Subscriptions | subscriptions |
view, create, edit, delete, operate |
| Subscriptions | partners |
view, create, edit, delete |
| Subscriptions | documents |
view, create, edit, delete |
| Subscriptions | global-values |
view, create, edit, delete |
| Subscriptions | notifiers |
view, create, edit, delete. Covers notification channels and legacy notifiers. |
| Subscriptions | api-gateways |
view, create, edit, delete |
| Subscriptions | bus-gateways |
view, create, edit, delete |
| Configuration | data-sources |
view, create, edit, delete, operate |
| Configuration | data-source-statements |
view, create, edit, delete |
| Configuration | workgroups |
view, create, edit, delete |
| Configuration | retry-policies |
view, create, edit, delete |
| Administration | users |
view, create, edit, delete |
| Administration | roles |
view, create, edit, delete |
| Administration | settings |
view, edit |
| Administration | audit |
view |
| Administration | adapter-source |
view, edit, operate |
| Administration | mirror |
view, operate |
exchanges.operatecovers retrying and resubmitting exchanges, including running a scheduled retry now.subscriptions.operatecovers pause, resume, receive now, roll up now and resetting a retry budget.data-sources.operatecovers testing a connection.data-source-statementslets people write SQL for a database without the right to change its credentials.documentsis the information types area.- The audit trail has only
view, because nothing can change it. mirroris labelled "Prod mirror".viewcovers comparing a staging with the prod it mirrors;operatecovers bringing prod's items onto it and testing them with prod's traffic. See Prod mirror.adapter-sourceis labelled "Adapter code".viewcovers reading the source a published adapter version carries and comparing versions.editcovers the adapter editor's drafts: starting, editing, deleting, building, checking and trying them, which runs their code on the Bitween server.operatecovers publishing a version, from the editor or as a package through the API, making a version current and withdrawing one. Listing an adapter's versions needs onlysubscriptions.view.
| Role | Permissions |
|---|---|
| Administrator | Every permission, including ones added in later versions |
| Member | Every action in the Operate, Subscriptions and Configuration groups |
| Viewer | Only the view actions in those groups |
Built-in roles cannot be edited or deleted. Custom roles can hold any combination, and cannot be deleted while members hold them.
Bitween refuses to remove, disable or demote the last enabled Administrator. Members cannot disable or remove themselves.
A signed-in request without the needed permission gets HTTP 403. A request that is not signed in, or whose token has expired, gets 401.
GET /api/permissions returns the full catalogue with labels and descriptions. GET /api/accounts/profile returns the signed-in member's permissions.
Partners authenticate with an API key, sent in the key header (partnerkey unless renamed in Settings or on the gateway, and partnerkey is always accepted too), as Authorization: Bearer <key>, or as Basic auth with the key's name as username and the key as password. API gateways and the legacy exchange endpoints use it. An API gateway can take tokens from a login server instead. See Entry points.
- Generate a key on the partner page. It is shown once. Afterwards the API returns only its first five characters.
- Keys must be unique and are stored in plain text.
- Removing a key revokes it immediately.
- Basic auth's password is always the generated key, never one someone chose.
The seeded SYSTEM partner has a key named default whose value is fixed in the source code. That key can post documents of any information type to POST /api/xchanges/{informationType}, which feeds the filter. Replace or remove the SYSTEM partner's key on every instance.
- Adapter properties marked secret are masked as
__private__in every API response and redacted from the audit trail. Their defaults are withheld from the adapter descriptions the UI reads. A published adapter that doesn't mark its passwords and keys as private has them returned in plain text, so check custom adapters before uploading them. Names are matched ignoring case, as adapters read them, sopasswordis masked likePassword. Sending__private__back keeps the stored value only while the adapter and where it connects stay the same, and never when a request names one setting twice in different case. - A connection's client secret, password, refresh token and access token are encrypted the same way, and are never returned by the API or written to the audit trail. An access token is put into an adapter's settings only as the adapter starts, so none is stored on an exchange.
- The secret settings are the Rebex license key, the OpenID Connect client secret and the mail server password. They are encrypted with AES-256-GCM, using a key derived from
Bitween:SettingsEncryptionKey. Without that passphrase the value can only come from configuration, and changing the passphrase makes the stored value unreadable. - Teams and Slack webhook addresses are secret, because anyone who has one can post to the channel.
- A bus gateway's endpoint settings are masked as its data source's settings are: those the data source names as secret, and any with a credential's name (password, token, key and so on). Sending
__private__back keeps the stored value. - Partner properties, global values, adapter properties, data source settings, notification channel, notifier and retry alert properties, including passwords, are encrypted in the database with AES-256-GCM when
Bitween:SettingsEncryptionKeyis set, and stored in plain text when it isn't. Values stored before the key was set are encrypted a few minutes after the next start.Bitween:PreviousSettingsEncryptionKeystill decrypts values while the key is rotated. - Bitween won't start when it can't read these with the key it has: a key that isn't set, or isn't the one they were encrypted with. A Helm deployment still has the key in an earlier release:
helm history <release>lists them andhelm get values <release> --revision <n>shows one's values. A database backup doesn't help without it, as it is encrypted with the same key. When the key is lost for good,Bitween:SettingsEncryptionKeyLost=truelets Bitween start: every value the lost key encrypted shows as empty until it is entered again, and stays stored until then, so it can still be read if the key turns up. Remove the setting once everything is re-entered.
Exchange files are always written as private objects, so a storage URL doesn't open them. Files written by earlier versions keep the public access they were written with until the bucket deletes them.
GET /api/bitweendocs?documentKey=..., which the UI uses to show file content, needsexchanges.view, and only reads a key that is exactly one of an exchange's files.- Readers without a login use links Bitween serves at
/api/files/{seal}/{storage key}: aggregation roll-ups, a partner's exchange result, the URLs in the exchange list. Anyone holding a link can read that one file, as with the public URL it replaces, until the bucket deletes the file. The seal can't be forged or moved to another file withoutToken:Key, and changing that key invalidates every link. - File links have their own rate limit per client address,
Bitween:RateLimits:FileLinksPerMinute, 60,000 by default, because an aggregation's handler downloads every link in a roll-up. - On Azure, where the container decides privacy for every file in it, Bitween switches its container to private at startup, keeping its stored access policies. Storage URLs handed out before that stop opening.
- The Settings page checks, every few minutes, whether a file Bitween writes opens straight from the bucket without credentials, and warns when it does, on every provider.
- A roll-up made before Bitween served its own links lists storage URLs. Retrying it swaps each URL of an exchange file for a Bitween link, so the handler still gets every file.
Data sources open connections to customers' brokers and databases, so they only run on nodes with Bitween__BusProvidersEnabled set.
- Their settings are masked in responses, and encrypted at rest as other credentials are (see Secrets).
- Anyone with
data-sources.viewcan browse a database's catalogue and see the login's privileges. - Saving or testing an Oracle statement runs
DBMS_SQL.PARSE, which executes DDL. Connect Oracle sources with a login that has no DDL rights. - SQL only comes from named statements, and every value is bound as a parameter, unless a data source turns on
AllowAdHocSql.
Every response carries these headers.
| Header | Value |
|---|---|
X-Frame-Options |
DENY |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
X-Permitted-Cross-Domain-Policies |
none |
Cross-Origin-Opener-Policy |
same-origin-allow-popups, or unsafe-none on /blank.html so the Microsoft popup works |
Content-Security-Policy |
Scripts from the same origin only, inline styles allowed, Microsoft sign-in allowed for connections, frames and forms, and no framing by other sites. Not sent under /swagger. |
- JSON responses carry
Cache-Control: no-store. Hashed UI assets are cached for a year, andindex.htmlis always revalidated. - CORS is closed unless
Bitween:CorsOriginslists origins, which are then allowed with credentials. - Request bodies are limited to 50 MB.
- Responses are gzip-compressed, including over HTTPS.
Bitween records every change to configuration: subscriptions and their schedules, categories, partners and API keys, information types, gateways and routes, work groups, retry policies and alert overrides, notification channels, notifiers, global value sets, settings, accounts, roles and role assignments.
Each entry holds the time, the member, the entity and its key, whether it was added, modified or deleted, and the old and new value of each changed property. Entries from one save share a correlation id. Adapter properties, partner properties, global values, API key values, passwords and secret settings are redacted. Exchanges and other runtime records are not audited. Reading an adapter's source is: each file read is an AdapterSourceAccess entry with the adapter, version and file. So are the adapter editor's drafts, each save recorded by a hash of its files rather than the code, and every version published, made current or withdrawn from Bitween (AdapterRelease). Operations that change what Bitween does without changing its configuration are recorded as OperatorAction entries naming what was done to what: retrying an exchange, a bulk retry, exporting exchange files, running a scheduled retry now, requeueing dead letters and deleting unattended queues. A sign-in with a password, Microsoft, OpenID Connect or the CLI sets the member's LastSignInOn, which is in the trail as a change to the account; a session renewing itself doesn't.
The trail is written in the same transaction as the change, and no API edits or deletes it. Browse it on the Audit trail page or with GET /api/audit.
- Set
Bitween__InitialAdminPasswordwhen installing; a new installation refuses to start without it (details). On an existing one still using the published default password, set it too, or change that password yourself: until then, whoever signs in with it first chooses the new one. - Replace or remove the SYSTEM partner's API key.
- Set
Token__Keyto a long random secret, and choose your ownToken__IssuerandToken__Audience. - Set
Bitween__SettingsEncryptionKey, so credentials are encrypted at rest, and before saving a Rebex license key or an OpenID Connect client secret. - Give the chart's Ingress a TLS entry for its host, so file links are built on an https address, and treat roll-ups and exchange results as holding the files themselves: a link opens its file without a login. Over plain http a link and its file can be read on the way.
- Serve Bitween over HTTPS. The refresh cookie is always marked
Secure. - Connect data sources with least-privilege logins, and never with DDL rights on Oracle.
- Keep the chart's read-only root file system (
securityContext.readOnlyRootFilesystem, on by default), and connect to MySQL over TLS (required by default). - Review Known limitations.