Authentication
Turn on Hubzoid sign-in, create the first administrator, set up two-step sign-in and connect Google, Microsoft or an OpenID Connect provider.
At the end of this guide sign-in is on, you are the first administrator with two-step sign-in set up, and you can add teammates. Hubzoid signs people in with its own accounts. One account works for the web app, Settings, the hosted MCP server and every agent of a deployment.
Sign-in is off by default, so hubzoid run starts in local mode on your own machine. Turn it on before anyone else can reach the Hub. An account is not access: what each person may use is granted per agent in Settings, as described in access administration.
What you need
- A Hub you can start with
hubzoid run, on a server or in a container. See single server or Docker. - The HTTPS address people will open, such as
https://kestrel.example.com. - An authenticator app on your phone, such as Google Authenticator, 1Password or Microsoft Authenticator.
Turn sign-in on
Set sign-in and the public address
Add these lines to the Hub's .env. For several Hubs in one hubzoid.yaml, set sign_in: true and public_url there instead.
HUBZOID_AUTH=true
HUBZOID_PUBLIC_URL=https://kestrel.example.comHUBZOID_PUBLIC_URL is needed when people reach Hubzoid through a proxy or a name other than localhost, and for Google, Microsoft and OpenID Connect sign-in.
The first administrator
Create the first administrator in one of two ways.
With the admin command, on any Hub, including one you already ran in local mode:
hubzoid admin create sam@example.com ./my-hub --owner--owner makes you an Administrator with the owner's access on every Hub of the deployment. The command prints a one-time sign-in link. Open it, choose a password, and you are signed in. --password asks for a password at a hidden prompt instead.
From the environment, for a deployment that has never run, such as a new server or container:
HUBZOID_ADMIN_EMAIL=sam@example.com
HUBZOID_ADMIN_PASSWORD=<at least 8 characters>When sign-in is on and no account exists yet, the first start creates this administrator with the owner's access. Remove both lines after that start.
When the environment route gives no access
The owner's access is given once per Hub. A Hub you already ran in local mode has the local owner as its owner, so the environment route creates an Administrator with no access there. Use hubzoid admin create --owner instead, or keep your laptop conversations with hubzoid auth bootstrap.
Restart the Hub
Restart hubzoid run, or its service. The ready line now says sign-in is on:
✓ Hubzoid is ready: https://kestrel.example.com (sign-in on)Sign in and set up two-step sign-in
Open the address and sign in. Administrators must use two-step sign-in by default, so right after your password Hubzoid shows Set up two-step sign-in:
- Scan the QR code with your authenticator app, or type the key it shows.
- Enter the six-digit code the app shows.
- Save the ten recovery codes. Each one works once, and they are how you get back in without your phone.
Add your teammates
Open Settings → People and choose Add user. Enter a name, an email and the agents they may use. Hubzoid shows a one-time sign-in link once. Nothing is emailed, so share the link yourself.


Two-step sign-in
After the password, or Google, Microsoft or OpenID Connect, a person enters a six-digit code from an authenticator app. Anyone can turn it on for themselves under Settings → Account → Security.
An administrator chooses who must use it under Settings → Organization → Sign-in:


| Choice | Setting value | Who must use it |
|---|---|---|
| Optional for everyone | none | Nobody. Each person may turn it on. |
| Required for administrators | admins (the default) | Administrators |
| Required for everyone | all | Every account |
Code rules, lost phones and changing the rule from a shell
Before anyone picks a rule, HUBZOID_MFA_REQUIRED gives the default. From a shell signed in to the deployment, hubzoid auth mfa policy shows the rule and hubzoid auth mfa policy all --server https://kestrel.example.com changes it. Someone who must use it and has not set it up is taken through set-up right after their password, so nobody is locked out.
- Codes have six digits and a 30-second step, with one step of tolerance either side. Each code works once. Five wrong codes end the sign-in attempt.
- Lost phone. Sign in with a recovery code, then set up a new authenticator under Security. Or an administrator opens People, the person, then Reset two-step sign-in. That also signs them out everywhere, and they set it up again at their next sign-in.
- A password reset keeps two-step sign-in on. The authenticator secret is encrypted with the deployment key, and recovery codes are stored as digests only.
- People who sign in through company sign-in (SAML) follow that provider's own sign-in policy and are not asked for a second code.
Some changes ask a person to confirm it is them when their last check was more than ten minutes ago, such as creating an API key, changing two-step sign-in or weakening the rule. They confirm with their password, plus a code when they use two-step sign-in.
Google, Microsoft and OpenID Connect
In the Google Cloud console, configure the OAuth consent screen (Internal if everyone signs in with your Google Workspace), then create an OAuth client ID of type Web application with the authorized redirect URI https://kestrel.example.com/oauth/google/callback.
GOOGLE_CLIENT_ID=<id>.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=<secret>
OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true
OAUTH_ALLOWED_DOMAINS=example.comThe sign-in page then shows Continue with Google.
How Google accounts link and which domains pass
With OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true, an account an administrator created signs in with Google the first time and stays linked, including accounts created as Google sign-in only. Without it, hubzoid doctor warns (auth.google_merge).
A Google Workspace domain listed in OAUTH_ALLOWED_DOMAINS must also be the account's Workspace domain, so a personal Google account registered with a work address is refused. gmail.com addresses pass only when you list gmail.com.
The redirect path is exact, with no trailing slash. Register one redirect URI for every address people use.
How external sign-ins are checked and linked to accounts
Every external sign-in uses the authorization code flow with PKCE, state and nonce, and Hubzoid checks the ID token against the provider's published keys.
A sign-in goes to the account it is already linked to. Otherwise it links to an invited account with the same email only when the provider says the email is verified, OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, and nobody has used that account yet. An account already in use is never taken over by email: its owner signs in the usual way and links the provider under Security. OAUTH_ALLOWED_DOMAINS (comma-separated, unset or * for any) applies to every provider and is judged on a verified email only.
Company sign-in (SAML and SCIM)
Install pip install "hubzoid[saml]" and the xmlsec1 system package, then open Settings → Organization → Sign-in → Company sign-in. Add the identity provider's metadata, paste the values Hubzoid shows into Okta or Microsoft Entra ID, create a SCIM token for provisioning, test sign-in, then turn it on.
The directory decides who exists. A provisioned account signs in only through its provider and has no Hubzoid password, and deactivating someone in the directory ends their sessions, API keys, MCP connections and scheduled work in one step. Not yet supported: SAML single logout, LDAP, group sync, and joining existing accounts to a directory.
Local mode
With HUBZOID_AUTH unset or false, every request that addresses this machine is the local owner, admin@localhost, an administrator.
What counts as this machine, and opening local mode to a network
That covers localhost, IP addresses, the address the server listens on, and the addresses in HUBZOID_PUBLIC_URL and HUBZOID_ALLOWED_ORIGINS. A request with any other host name is not treated as the owner, which stops another web page from reaching your Hub through DNS rebinding.
Local mode keeps the public port on loopback. hubzoid run --host 0.0.0.0 without sign-in stops with exit code 2 and explains how to turn sign-in on. On a network you trust you can accept the risk with HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true, and hubzoid doctor then warns (exposure.local_mode). The local owner never signs in with a password, a link or a provider, including after you turn sign-in on.
| Deployment | Put the sign-in settings in |
|---|---|
| One Hub | The Hub's .env |
| Several separate Hubs | Each Hub's .env. Each Hub has its own accounts. |
Several Hubs in one hubzoid.yaml | sign_in: true in hubzoid.yaml, and provider settings in the .env next to it |
From a laptop Hub to team sign-in
A Hub that ran without sign-in has a local owner that owns its conversations. To keep them, turn that owner into a real account on the Hub's own machine, then turn sign-in on and restart:
hubzoid auth bootstrap ./my-hub --email sam@example.com --password-stdinIt keeps the same account id, so the conversations and grants stay yours.
Without bootstrap, and scheduled workflows
Without it, the local owner keeps its account and grants but nobody can sign in as it. Scheduled workflows that ran as admin@localhost keep running as it until you set HUBZOID_WORKFLOW_USER or run_as to a real account. See access administration.
Passwords and one-time links
People change their own password, and an administrator can reset one with a one-time link.
Password rules, resets and one-time links
- New passwords are hashed with Argon2id and are 8 to 1024 characters.
- People change their own password under Settings → Account → Profile. That needs their current password and ends their other sessions.
- An administrator resets a password under People, the person, Reset password, or with
hubzoid admin reset-password <email>. The current password stops working at once, the person's sessions end, and a one-time link lets them choose a new one. ENABLE_LOGIN_FORM=falseorENABLE_PASSWORD_AUTH=falseturns password sign-in off, so people use an external provider only.
A one-time link lasts 72 hours (HUBZOID_LINK_HOURS) and works once. A newer link for the same person, or any password change, cancels it, and using it ends the person's other sessions. Only a SHA-256 digest of the token is stored. Links are built on HUBZOID_PUBLIC_URL when it is set. Hubzoid removes link tokens from its own access logs, but a reverse proxy logs them unless you redact the /api/auth/link/ path and the token, code and state query parameters.
Self sign-up and approval
Both kinds of self sign-up are off by default.
ENABLE_SIGNUP=trueadds Create an account to the sign-in page. A new password account always waits for approval.ENABLE_OAUTH_SIGNUP=truecreates an account at a person's first external sign-in. It waits for approval, unless the provider verified the email and its domain is listed explicitly inOAUTH_ALLOWED_DOMAINS(not*).
An account waiting for approval cannot sign in. An organization administrator approves it under People, or with hubzoid admin set-role <email> user. A new account has no access to any agent until someone grants it.
Sessions, keys and limits
A session lasts up to 30 days, personal API keys let a script act as one person, and repeated failed sign-ins lock for 15 minutes.
Session cookie, lifetime and when sessions end
- The session cookie
hz_sessionis HttpOnly,SameSite=LaxandSecureover https. The database stores only a SHA-256 digest of it. - A session lasts at most
HUBZOID_SESSION_DAYS(30) days and ends afterHUBZOID_SESSION_IDLE_DAYS(7) days without use. Each person may have ten sessions, and an eleventh sign-in signs the least recently used device out. - A session ends when the person signs out, changes their password or uses a one-time link, and when an administrator resets their password, changes their role, blocks them or deletes them.
- Sign out everywhere and end all keys, under Security, ends every other session, every personal API key and every assistant connection at once. Administrators find Sign out everywhere on the person's page.
- Changes from a browser must come from the deployment's own page. The
Originheader must match the host or a configured address.
Personal API keys (hzp_...) let a script or the Hubzoid command line act as one person. Make one under Security, or with hubzoid auth keys create "CI evals" --preset read --server https://kestrel.example.com. The key is shown once, never does more than its owner can do right now, and expires.
How failed sign-ins are counted and locked
Failed sign-ins are counted per client address and per email in the shared store. After HUBZOID_AUTH_MAX_FAILURES (10) failures within 15 minutes, the address or the email is locked for 15 minutes. The client address comes from X-Forwarded-For, so put a TLS proxy that sets it in front. Without one, a client can send its own header and avoid the per-address limit. The per-email limit always applies. Sign-ins, failed sign-ins, password changes, used links and sign-ups appear in Activity for organization administrators.
More than one public address
HUBZOID_PUBLIC_URL=https://agents.example.com
HUBZOID_ALLOWED_ORIGINS=https://agents.internal.example.com,http://localhost:3080Every listed address may send changes from a browser, gets sign-in callbacks on its own address and counts as this machine in local mode. Links Hubzoid writes use HUBZOID_PUBLIC_URL. For several Hubs, see several Hubs at more than one address.
The admin command
hubzoid admin works on the deployment's account store without signing in, with the authority of whoever runs it on the server. Pass any Hub of the deployment, or run it in a Hub or deployment folder.
Every admin command and its options
Passwords are typed at a hidden prompt, never passed as arguments, and never printed. Changes appear in Activity with the operator as cli:<user>@<host>.
| Command | What it does |
|---|---|
hubzoid admin create <email> [hub] [--name NAME] [--admin] [--owner] [--password] [--google-only] | Creates an active account. --admin makes an Administrator, and --owner also gives the owner's access on every Hub. Prints a one-time link unless --password or --google-only. |
hubzoid admin reset-password <email> [hub] [--password] | Ends the current password and the person's sessions, then prints a link, or sets the password you type |
hubzoid admin list [hub] [--json] | Every account, with its role, status, how it signs in and its last sign-in |
hubzoid admin set-role <email> admin|user [hub] | Sets the Administrator role, signs the person out so it applies at once, and approves an account that was waiting. Refuses to demote the last administrator. |
--google-only creates an account with no password. It can sign in only when Google is configured with OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true. The hubzoid auth commands act as you on a running deployment instead: login, whoami, keys, sessions, mfa and bootstrap.
If something goes wrong
| What you see | What to do |
|---|---|
| The provider says the redirect URI does not match | Register <your address>/oauth/google/callback (or microsoft, oidc) exactly, with no trailing slash, for every address people use. |
/auth?error=sign_in_off | Sign-in is off. Set HUBZOID_AUTH=true and restart. |
/auth?error=not_linked | The email has an account, but OAUTH_MERGE_ACCOUNTS_BY_EMAIL is not true, or the account is already in use. |
| "Too many attempts" | Wait 15 minutes, or reset the password with hubzoid admin reset-password. |
| Lost the authenticator and the recovery codes | Another administrator uses Reset two-step sign-in on your page under People. |
| Signed in, but no agent | The account has no access yet. An administrator grants Use this agent in Settings. |
More problems and fixes
| What you see | What to do |
|---|---|
/auth?error=not_configured | The provider's client ID or secret is missing in the environment of the process that serves the web app. |
/auth?error=email_not_verified | The provider did not vouch for the email. For Microsoft, add the xms_edov claim. |
/auth?error=domain_not_allowed | The email's domain is not in OAUTH_ALLOWED_DOMAINS, or a Google Workspace domain does not match. |
/auth?error=state_mismatch | The sign-in took longer than 10 minutes, or started on another address. Start again from the sign-in page. |
| "This request came from another site" | The page's address is not HUBZOID_PUBLIC_URL or in HUBZOID_ALLOWED_ORIGINS. |
| Nobody can administer | Run hubzoid admin set-role <email> admin <hub> on the server. |

