Authentication & profile
TRUSCA supports two interactive sign-in methods — email + password and OAuth (GitHub or Google) — plus a self-service password-recovery flow. This page walks through each path and covers identity management on the /profile page.
Any signed-in user. No special role required to manage your own identities. The OAuth buttons appear only when the operator has configured the relevant *_CLIENT_ID / *_CLIENT_SECRET environment variables.
Sign in with email + password
- Open
/login. - Enter your email and password.
- Submit.

What happens server-side
- The password is hashed with bcrypt cost 12 at registration; the login compares the candidate against the stored hash in constant time.
- Public registration is rate-limited to 5 attempts per minute per IP because each request performs a bcrypt hash. Excess requests return HTTP 429 with a
Retry-Afterheader. - A successful login returns a JWT access token (30 min) and a refresh token (7 days, rotated on every use, with reuse-detection that revokes the entire chain).
- Refresh tokens live in an
HttpOnly,Secure,SameSite=Laxcookie. They are never visible to JavaScript. - The login endpoint is rate-limited to 5 attempts per minute per IP. Excess requests return HTTP 429 with a
Retry-Afterheader. - Failed attempts are also counted per email address, so guessing spread across many source addresses is slowed too. After a few failures that address is refused for a short while, and each successive refusal lasts longer. The reply says how long to wait and is identical whether or not the address belongs to an account.
If you see "Invalid email or password", check the email is correct and try once more — the message is intentionally generic so an attacker cannot enumerate accounts.
If you are told to wait
An address is refused after several wrong passwords, and the count is not only yours: anyone who knows your email can supply failures for it. Three things get you back in.
- Waiting. A refusal always ends on its own, and re-opening one costs a fresh run of failures rather than a single attempt, so somebody would have to keep guessing to keep you out.
- Resetting your password. This clears the count immediately, and it is the way back that does not depend on whoever is guessing stopping, because it needs your inbox rather than your password. Use the most recent reset email: requesting a new one invalidates the previous link, so an older message in your inbox may no longer work.
- Asking an administrator. A super admin can clear the count for you, which is the answer when you have lost access to the inbox as well.
Forgot your password
- From
/login, click Forgot password? to open/forgot-password. - Enter the email associated with your account.
- Submit. The portal always returns a 204 No Content response — even if no account exists for that email — so an attacker cannot enumerate users.
- Check your inbox. If an account exists, a message with the subject "Reset your TRUSCA password" arrives within ~30 seconds.
The reset link is valid for 24 hours and can be used once. After expiry or first use, the token is revoked.

Reset your password
The link in the email lands on /reset-password?token=<opaque>.
- Enter the new password (≥ 12 characters, must not match the breach dictionary).
- Confirm it in the second field.
- Submit.
On success you are redirected to /login. The new password is bcrypt-hashed and the reset token is consumed. Every other session ends immediately: refresh tokens are revoked, access tokens issued before the change are refused rather than being allowed to run out their remaining lifetime, and a sign-in or token renewal running alongside the reset cannot leave a session behind it. Somebody who resets because they think a credential leaked does not have to wait for the stolen session to expire.
API keys are not affected. They are a separate credential with their own lifecycle, so a build that authenticates with one keeps working. If you are resetting because of a suspected compromise, review your keys on the Integrations page as well.
If the token has expired or has already been used, the page renders an error with a link back to /forgot-password to request a fresh one.
Two-step sign-in
A password can be guessed, reused, or phished. Turning on a second factor means a sign-in also needs a six-digit code from an app on your phone, which changes every 30 seconds and never travels over email.
This is off until you turn it on, and you turn it on for yourself.
Turn it on
- Open
/profileand find Two-step sign-in. - Click Set up, then enter your current password. You are asked again even though you are signed in: what setting this up hands out keeps working after a password change, so an open browser tab is not enough on its own.
- The portal shows a QR code and, below it, the same secret as text.
- Scan the code with an authenticator app, or type the text version in if you are on a desktop app, your camera will not focus, or you are using a screen reader.
- Enter the six digits the app shows, and submit.
The step between showing the secret and asking for a code is not ceremony. Nothing is switched on until a code proves the app really has the secret, so closing the tab halfway leaves you signed in exactly as before.
Turning it on protects the sign-ins that come after it. It does not end sessions that are already open, and it is not the thing to reach for if you think somebody else is signed in as you: reset your password, which ends every other session immediately.
When it succeeds the portal shows ten recovery codes. This is the only time they are readable: they are stored as hashes, so nobody, including an administrator, can show them to you again. Save them somewhere that is not the phone holding the authenticator.
Sign in from then on
- Enter your email and password as before.
- The page then asks for the code. Enter the six digits.
You have five minutes between the two steps. If you take longer, start again from the password: nothing was signed in yet, so nothing is lost.
Each code works once. A code you have already used is refused even inside the 30 seconds it is still displayed, which is what stops somebody who watched you type it from following you in.
If you lose the authenticator
Use a recovery code where the six digits are asked for. Each one works once and then it is gone, so ten of them is ten sign-ins, not ten attempts.
Once you are back in, /profile will issue a fresh set. It asks for your
password or a code from the app first, for the same reason setting it up does.
Issuing a set cancels every code from the old one, so the list you printed
before is no longer worth anything to whoever finds it.
You are notified in the portal whenever a factor is set up on your account or new recovery codes are issued. If a notice arrives that you did not cause, change your password and tell your administrator: somebody else is signed in as you.
If you have neither the app nor a code, a super admin can clear the second factor from your account, and you are told when that happens.
What is not covered
- API keys keep working. A key is already something you hold rather than something you know, and a build that stops for a code on your phone is a build that never finishes. Treat a key as equivalent to a password and revoke it the way you would change one.
- OAuth still asks. Signing in with GitHub or Google proves the provider account, not the second factor, so the portal asks for the code after the provider hands you back.
Sign in with OAuth
If GitHub or Google is configured, the /login page shows the corresponding buttons below the email field.
- Click Continue with GitHub or Continue with Google.
- Approve the access request on the provider's consent screen.
- You are redirected back to the portal and signed in.
First-time OAuth sign-in auto-creates an account from the provider's verified email. A personal team is provisioned automatically (named <your-handle>'s team).
If you have turned on two-step sign-in, the portal asks for the code after the provider redirects you back, before anything is signed in.
Subsequent sign-ins look up the existing identity by (provider, provider_user_id). The provider's email field is never used to match — this prevents account-takeover via a recycled email address at the provider.
Errors are surfaced as i18n-mapped messages. The seven distinct codes cover provider denial, missing scope, expired state, repeated state, identity collision, suspended account, and provider 5xx. Each code points the user to a specific recovery action.
Manage connected accounts on /profile
The /profile page has a Connected Accounts section that lists the OAuth identities currently attached to your account:
- GitHub — present if you have ever signed in with GitHub.
- Google — present if you have ever signed in with Google.

The Connected Accounts panel highlights every external identity currently linked to your portal account:
Password sign-in is not displayed as a row in the Connected Accounts list in this release — it is implicit when your account was registered with email + password (or when you completed a password reset). Password recovery for OAuth-only accounts is on the roadmap; today the only path is the operator-side /admin/users/{id}/password-reset endpoint.
Each Connected Accounts row has an Unlink button. The portal protects you from locking yourself out:
- If unlinking would leave you with no sign-in method (e.g. you have only one OAuth identity and no password set), the request returns HTTP 409 and the UI shows an alert: "Set a password before unlinking your last OAuth identity."
- The fallback path is Forgot password — request a reset link, set a password, then return to
/profileand unlink.
Linking a new provider is symmetric: sign out, sign in with the new provider, and the new identity attaches automatically because the verified email matches the existing account.
Verify it worked
- After password sign-in, the global bar shows your initials and the team you are acting as.
Switching teams
If you belong to more than one team, the team name in the global bar is a menu. Picking a different team changes which team new projects are created under, and the choice survives a reload.
It does not filter what any screen shows. The Dashboard and the project list stay scoped to everything your memberships reach, whichever team is selected — a control that quietly narrowed a page would be easy to leave switched on by accident and hard to notice. Use a screen's own filters to narrow it.
If you belong to exactly one team the name is shown as a plain label, and if you belong to none — the seeded super admin, for instance — nothing is shown.
- After OAuth sign-in,
/profilelists the provider you used.
- After unlinking, the row disappears and the Unlink button on the remaining row is disabled if it would leave you stranded.
Licenses and notices on /about
About in the sidebar shows what this deployment is and the license notices it ships with. It needs no admin role — a license notice is something every user of a deployment is entitled to read.
The page reports the product name, the running version, the SPDX license id, the copyright holder, and a link to the published source. Below that, three tabs carry the notice files verbatim:
| Tab | File | What it covers |
|---|---|---|
| License | LICENSE | The license TRUSCA itself is distributed under (Apache-2.0). |
| Notice | NOTICE | TRUSCA's own copyright notice, required by Apache-2.0 section 4(d). |
| Third-party notices | THIRD_PARTY_NOTICES.md | Attribution for third-party material vendored into the source tree, and for the tools bundled in the container images. |
The same three files ship at /licenses/ inside every container image and inside
the Helm chart, so an operator with shell access can read them without the UI.
They are in the product as well because TRUSCA is self-hosted and supports
air-gapped installs, where following a link to GitHub is not an option.
If a tab reports the file as missing from the installation, the image was built without it — check the release artifacts rather than the portal.
See also
- Notifications — how the portal reaches you about events on your projects.
- Integrations — API keys for non-interactive clients.
- Users & teams — admin view of the same identities.
- Components & licenses — the licenses of the code you scan, as opposed to TRUSCA's own.