Skip to content

Authentication

This page covers every way to prove who you are to a thinkingface instance: signing in to the web UI, issuing and using access tokens, registering an SSH key, and the security controls around all of it. It's aimed at anyone using an existing instance; for the environment variables that configure these behaviors on the server side, see Configuration.

Signing in to the web UI

The web UI's login page (/login) has Log in and Sign up tabs. Logging in successfully sets a tf_session cookie: signed, HttpOnly, SameSite=Lax, and marked Secure whenever the instance is served over HTTPS. The cookie carries no readable information itself — the server verifies its signature and checks it against the account's current session state on every request, so signing out or changing your password invalidates it immediately even though the cookie value itself hasn't expired yet.

By default the cookie is valid for 7 days after issue (configurable by the instance operator via TF_SESSION_TTL). Signing out clears it; the browser session then falls back to anonymous access, which still works for any repository that's public.

Self-service signup

Whether the Sign up tab actually creates an account depends on the instance: operators can disable self-service signup (TF_ALLOW_SIGNUP=false), leaving only accounts an admin creates directly. When signup is open, a new account needs a username, an email address, and a password of at least 8 characters (and, because passwords are hashed with bcrypt, at most 72 bytes).

Two further controls can narrow signup without closing it:

  • TF_SIGNUP_EMAIL_DOMAINS limits it to a list of email domains. The match is case-insensitive and exact — an instance that lists example.com accepts alice@example.com and refuses alice@sub.example.com, because a domain silently admitting every subdomain of itself is the kind of surprise you only notice as an unwanted account. A refused address is told which domains are accepted, so the form is still fillable.
  • TF_SIGNUP_REQUIRE_APPROVAL puts new signups in a waiting room. The account is created, but no session is issued and it authenticates on nothing — not its password, not an access token, not an SSH key — until an administrator approves it at Settings → Users. The signup form says so rather than pretending to sign you in, and trying to log in before approval answers 403 account_pending (only ever with the correct password, so it tells nobody else that the account exists).

Every instance also seeds one admin account on first boot, from TF_ADMIN_USERNAME / TF_ADMIN_PASSWORD / TF_ADMIN_EMAIL (defaulting to admin / admin / admin@example.com if the operator hasn't changed them — change these before exposing an instance to anyone else).

Changing your password

Change your own password at Settings → Account (/settings/account). The form asks for your current password, the new one, and the new one again. The current password is always verified: being signed in isn't on its own permission to replace the credential the session was minted from. A new password must be at least 8 characters and at most 72 bytes — the same rule signup applies.

Changing it signs out every other browser. The session cookie carries a revocation counter that the change bumps, so anything still holding an old cookie is rejected on its next request; the tab you changed it in is re-issued a fresh one and stays signed in.

Your access tokens keep working. A token is a separate credential with its own list and its own delete button, and a routine password change says nothing about any of them — revoking them all would break unattended clients (CI jobs, git, the tf CLI) for no gain. If a token itself leaked, delete that token at Settings → Access tokens.

When an admin resets a password

TF_ADMIN_PASSWORD only ever creates the very first account, on an instance whose user table is empty; changing it afterwards has no effect. So a forgotten password is fixed by another administrator, not by editing an environment variable — or, when there is no other administrator, from the server's own command line (Recovering a locked-out instance).

Accounts with site administrator rights get a Settings → Users entry (/settings/admin/users) listing every account on the instance, with a search box over usernames and email addresses. Each row offers:

  • Reset password — set a new password for that account. It signs out every browser they have open; their access tokens are untouched, exactly as for a self-service change. There is no email round trip, so hand the new password over out of band and let them change it themselves at Settings → Account.
  • Make admin / Revoke admin — grant or remove site administrator rights. This is how an instance gets a second administrator, and therefore how it stays recoverable if the first one loses their password.
  • Suspend / Restore — turn an account off and back on. A suspended account authenticates on nothing: not the web UI, not its password over HTTP Basic, not its access tokens, not its registered SSH keys. This is what an offboarding needs, because a password reset on its own leaves the departing account's tokens and keys working for git push. Suspending destroys nothing, so restoring brings the account back as it was — minus its sessions, which stay revoked.
  • Revoke credentials — delete every access token and SSH key the account holds, and sign it out. Unlike suspension this cannot be undone, and it does not stop the account working: it is for credentials you think have leaked (a lost laptop, a token in a build log) on an account that should keep going once new ones are issued.
  • Approve / Hold for approval — let an account out of the signup waiting room, or put it back. This only appears on instances running TF_SIGNUP_REQUIRE_APPROVAL, where a self-registered account starts out unable to authenticate on any path at all; approving is what actually lets that person in. Accounts waiting for approval are badged and sorted to the top of the list, with a banner above it, so nobody sits locked out unnoticed. Holding one again signs it out everywhere immediately. It is independent of suspension: approving does not un-suspend, and restoring does not approve.

The list also shows each account's Last login — the last time a password signed it in. Access tokens and SSH keys have their own last-used dates and deliberately don't move this one, because the question it answers is "is anybody still using this account", which a nightly CI token answers wrongly. Never means exactly that, including for every account that existed before the column did.

Add user at the top of the same screen creates an account outright: a username, an email address, a password, and optionally the administrator flag. It works whether or not self-service signup is open — that is the point of it. On an instance running TF_ALLOW_SIGNUP=false this is the only way anyone new gets an account, so closing signup is not a one-way door. The username becomes that account's namespace (dana/*) and can never be changed, so it goes through the same rules signup applies, reserved names included. As with a reset, there is no invitation email: pass the password on out of band and have them change it at Settings → Account.

Two rules stop an instance from locking itself out: you can't revoke your own administrator access or suspend your own account (ask another administrator), and the last administrator who is still active can't be demoted or suspended at all. A suspended administrator does not count towards that last one — an account that cannot sign in is no more able to recover the instance than a missing one. The restriction is enforced by the server for every one of these actions, not merely hidden by the UI.

Site administration is browser-only

Every /api/v1/admin endpoint accepts the session cookie and nothing else. An access token or HTTP Basic credentials are refused with 403 session_required, even when the account behind them is an administrator. A single leaked write-scoped token used to be enough to create accounts, reset anyone's password, and register an SSH key that outlives the token's revocation; requiring a browser session keeps automation out of that blast radius. Use the screens under Settings.

Recovering a locked-out instance

Everything above assumes somebody can still sign in. When nobody can — the only administrator forgot their password, and /api/v1/admin accepts a browser session and nothing else — the repair runs on the server itself, next to the database:

# In the container or on the host running the server, with the same
# DATABASE_URL the server uses:
thinkingface admin passwd alice     # prompts twice, with no echo
thinkingface admin promote alice    # grant site administrator rights
  • The password is never an argument. It's read from the terminal without echo (twice, and the two must match), or from standard input when there is no terminal, so an automated run can do it unattended:
printf '%s' "$NEW_PASSWORD" | thinkingface admin passwd alice

A password passed as an argument would sit in the shell history of whoever typed it and in the process list for every other user on the machine, which is why there is no flag for it.

  • admin passwd applies the same rules the web UI does — at least 8 characters, at most 72 bytes — and, like every other password change, signs the account out everywhere while leaving its access tokens and SSH keys alone.
  • admin promote is how an instance regains an administrator when it has none. There is no matching "demote": taking rights away is ordinary administration, not an emergency, and it already has a screen and a last-administrator guard.
  • Both commands say what they did and to whom, and warn you when the account they just fixed is still suspended or still waiting for approval — both of which refuse even a correct password, so without the warning you would think you were done.

A third subcommand, admin token create, mints an access token from the same place without a browser — for provisioning an instance or a CI job. See Minting a token from the command line.

Authorization here is shell access to the deployment: anyone who can run this could already read the database directly. That is the point — it is the one repair that does not need a working account, and it replaces "edit the users table by hand" as the answer of last resort.

Failed background jobs

Administrators also get Settings → Sync jobs (/settings/admin/sync-jobs). Every push schedules background work that rebuilds the repository's file listing, search entry and object storage export. When that work fails repeatedly it parks, and the repository stays indexed at its previous push — the files you see are stale even though the push succeeded. This screen lists the parked jobs with the error from their last attempt, and Retry puts one back in the queue with a fresh budget. An empty list is the healthy state.

Access tokens

An access token (tf_xxxxxxxxxxxx) is what every non-browser client uses: huggingface_hub, git, the tf CLI, and direct API calls. Issue one at Settings → Access tokens (/settings/tokens), signed in to the web UI:

The access token page, showing the create-token form and a list of existing tokens

Give the token a name, a scope, an expiration and, optionally, the repositories it is restricted to, then create it:

Scope Meaning
read Can read repositories you have access to; cannot push, create, delete, or manage tokens/SSH keys
write Everything read can do, plus pushing commits, creating and deleting repositories, and managing your own tokens and SSH keys

Expiration is one of No expiration, 7, 30, 60, 90, or 365 days (365 is the maximum). Once a token's expiration date passes it stops authenticating requests — the same as if it had been deleted — though it still appears in the token list, badged as expired, so you can tell why it stopped working and remove it.

The token's value is displayed exactly once, immediately after creation — copy it before navigating away, since the server only ever stores its hash and cannot show it to you again. If you lose it, delete the token and create a new one.

The token list shows each token's name, scope, the repositories it is restricted to (All for an unrestricted token), creation date, last-used date, and expiration (or Never for one with no expiration), and lets you delete a token (with a confirmation step) at any time — deleting it takes effect immediately, the same as letting it expire.

Minting tokens needs a write-scoped credential

Creating or deleting a token or an SSH key always requires write scope, even though reading the list only requires being signed in. A read-scoped token cannot use itself to mint a more powerful one, and neither can a repository-restricted token (below).

Restricting a token to repositories

A write token can be restricted to a list of repositories. Fill in Restrict to repositories on the token form — one per line, as datasets/NAMESPACE/NAME or models/NAMESPACE/NAME (the plural kind, as in the web UI's URLs) — or leave it empty for an ordinary, unrestricted token. The field appears for write tokens only: a read token changes nothing, so a list would restrict nothing.

A restricted token:

  • reads exactly like an unrestricted token of the same user — the restriction is about what it may change, not what it may see;
  • may write — git push, huggingface_hub uploads and commits, LFS uploads, file edits, experiment ingest, run annotations, project notes and metric goals — only to the listed repositories, and only while you yourself still have write access to them;
  • may not perform administrative repository operations anywhere, listed repositories included: delete, archive, rename, change settings, transfer;
  • may not perform account-level writes: create repositories, mint or revoke tokens, register SSH keys, change your profile or password, manage organizations or webhooks.

Anything outside that is refused with 403 and the error type token_restricted. One huggingface_hub convenience keeps working: create_repo(..., exist_ok=True), which push_to_hub and most upload scripts call first, succeeds for a listed repository that already exists.

Each listed repository must exist and you must have write access to it when the token is created, so create the repository first — a restricted token cannot create one. The list is stored by repository, not by name: the grant follows a repository through a rename or transfer, and never passes to a different repository that later takes a listed name. A token whose repositories have all been deleted can change nothing; it does not become unrestricted.

Over the API, pass the list as repos when creating the token (at most 32 entries):

curl -X POST http://localhost:8080/api/v1/tokens \
  -H "Authorization: Bearer tf_xxxxxxxxxxxx" -H "Content-Type: application/json" \
  -d '{"name": "ocr-agent", "scope": "write", "expires_in_days": 7,
       "repos": ["datasets/alice/trackio-metrics"]}'

The credential making this request must itself be an unrestricted write credential.

Give AI agents and CI a restricted, expiring token

A token handed to an AI agent, a CI job or a rented GPU machine should be able to do exactly what that job needs and nothing else. A write token restricted to the one experiment repository the job logs to, expiring in 7 or 30 days, lets it ingest metrics, annotate runs and edit notes there — and if it leaks, it cannot delete a repository, push anywhere else, or mint itself a successor. Every request it makes is attributable in the server's access log (token_id, token_name). See Using thinkingface from AI agents.

Minting a token from the command line

thinkingface admin token create mints an access token without a browser, for provisioning a new instance or handing a token to automation. Like admin passwd, it runs next to the database with the same DATABASE_URL the server uses, and applies exactly the rules the web UI does:

thinkingface admin token create <username> [--name NAME] [--scope read|write]
    [--expires-in-days N] [--repo KIND/NS/NAME ...] [--output FILE]
Flag Default Meaning
--name admin-cli the label shown in the token list
--scope write read or write
--expires-in-days 0 (never) 1–365 days
--repo (unrestricted) restrict the token to this repository, as datasets/NS/NAME or models/NS/NAME. Repeatable
--output (stdout) write the token to this file instead, created with mode 0600; refused if the file exists

The token — and nothing else — goes to stdout (or to --output); what was created, for whom, and until when goes to stderr, so TOKEN=$(thinkingface admin token create ...) captures exactly the token. Under Docker Compose, run it inside the api container with -T so that no terminal is allocated (a terminal would merge stderr into stdout):

docker compose exec -T api thinkingface admin token create admin \
    --name ci --expires-in-days 30 --repo datasets/admin/trackio-metrics > ci-token.txt

Minting is logged as access token created with actor: admin-cli. As with the other admin commands, whoever can run this already has the database.

Using a token

With huggingface_hub / datasets: point HF_ENDPOINT at your instance and set HF_TOKEN to the token value (also set HF_HUB_DISABLE_XET=1 — see Compatibility for why):

export HF_ENDPOINT=http://localhost:8080
export HF_TOKEN=tf_xxxxxxxxxxxx
export HF_HUB_DISABLE_XET=1

With git over HTTP: use the token as the password in Basic auth; the username is ignored, so anything non-empty works:

git clone http://admin:tf_xxxxxxxxxxxx@localhost:8080/datasets/admin/imdb-reviews.git

With the tf CLI: run tf login interactively, pass --token, or set THINKINGFACE_API_KEY in the environment. See tf CLI for the full precedence order.

With the raw API: send it as a bearer token:

curl -H "Authorization: Bearer tf_xxxxxxxxxxxx" http://localhost:8080/api/whoami-v2

Git over HTTP also accepts this same header form; the Basic-auth form above is only needed where a tool insists on a username/password prompt.

SSH keys

If the instance operator has enabled git-over-SSH, you can register a public key at Settings → SSH keys (/settings/ssh-keys) and clone or push over SSH instead of HTTP, without a token in the URL:

git clone ssh://git@localhost:2222/admin/imdb-reviews.git

Paste the full line from your .pub file (for example ssh-ed25519 AAAA... you@example.com). Accepted key types are Ed25519, ECDSA, and RSA (2048 bits or larger); DSA keys are rejected with an explanation. Registering a key requires a write-scoped credential, the same as minting a token. Each fingerprint can only be registered to one account instance-wide.

Security notes

  • By default, authentication gates writes, not reads. thinkingface has no per-repository visibility setting, so every repository is readable — including by unauthenticated callers — by anyone who can reach the instance. A token controls what its holder may write; it is not what keeps a repository unread. See Compatibility. An operator can close anonymous reads instance-wide with TF_REQUIRE_AUTH_FOR_READ (Deployment); every signed-in account and every valid token can then still read every repository.
  • Rate limiting: failed password attempts (both the web UI's login form and HTTP Basic auth, which every route accepts) are throttled per client address (an IPv4 address, or an IPv6 /64), at half that rate per username from that address, and at five times it per username across all addresses — so failures from a single host or network can't lock an account out for someone signing in from elsewhere, while guessing spread over many addresses is still capped. The flip side of that cap is that an attacker spread across enough networks can exhaust it and keep the account throttled while the attack lasts. Only failures count; successful logins never consume the budget. A throttled request gets 429 with a Retry-After header. The instance operator controls the base rate via TF_AUTH_RATE_LIMIT_PER_MIN (10 per minute by default; 0 disables the limiter entirely).
  • Only the smart git protocol is accepted. A dumb-HTTP git client gets a clear error telling it to upgrade rather than failing obscurely.
  • CORS: state-changing requests authenticated via the session cookie are only accepted from an allowlisted origin (the web UI's own origin, plus localhost:3000 over plain HTTP in development). A token-authenticated request isn't subject to this, since it can't be triggered ambiently by a browser the way a cookie can. When the web UI reaches the API through its own same-origin proxy (the default), the proxy performs the equivalent check against its own origin before forwarding.

See also

  • tf CLI — credential resolution order and the tf login/tf status commands
  • Compatibility — what HF_TOKEN, git, and SSH each cover
  • Configuration — the environment variables mentioned above, from the operator's side