TILLAUTH · OPERATE

Session risk.

GA

A right password from the wrong place is how most accounts are taken. TillAuth scores every sign-in against where the user has signed in lately, and every refresh against the session's own history, and your policy decides whether a stolen password or refresh token gets anywhere.

01Score

What is scored

Each signal adds to a score out of 100. A request that matches the history scores 0.

SignalAddsWhen
impossible_travel60The user or token moved 500 km or more faster than 900 km/h since the last sighting.
listed_ip50The address is on TillGate’s confirmed or widely reported lists.
browser_changed40A browser or operating system not seen before. Version updates don’t count.
country_changed35A country not seen before.
network_changed10A different network: another /24 for IPv4, another /64 for IPv6.

A sign-in is compared with the user's sessions in the app over the last 90 days, up to 200 of them: a browser, country or network counts as new only if none of them had it, and travel is measured from the latest one with a location. A first sign-in has nothing to compare with, so only a listed address counts. Sessions ended by this policy or by token theft detection are not history.

A refresh is compared with the session it renews. Coming back to the country the session started in is not a change, so a trip home doesn't count against anyone.

A score under 30 is low, 30 to 59 medium, 60 or more high.

02Decide

The policy

ModeWhat happens
offSign-ins and refreshes are not scored.
monitor (default)Every sign-in and refresh is scored and the score is stored with the session. A score of 30 or more writes signin.risk or session.risk to the audit log. Nobody is stopped.
enforceAt reauth_at (default 60) a sign-in owes one more proof, and a refreshing session ends so the user signs in again. At revoke_at (default 90) a sign-in is blocked, and a refresh ends every session the user has in the app.

Set it on the app's Policies page, from a terminal, or through the API. Thresholds are whole numbers from 10 to 100, and reauth_at can't be above revoke_at. Fields you leave out keep their value.

tilldev auth apps update my-app --session-risk enforce --reauth-at 60 --revoke-at 90 PATCH https://tilldev.dev/api/auth/admin/apps/<app> { "settings": { "session_risk": { "mode": "enforce", "reauth_at": 60, "revoke_at": 90 } } }
Try monitor first
Run in monitor for a week and look at the app's risky sessions before you enforce. People on mobile networks change address often; that alone adds 10 and never crosses a threshold.
03Sign in

At sign-in

Signed in withAt reauth_atAt revoke_at
Password, Google, GitHub or your OIDC providerTheir authenticator code or push approval; with neither, a code we emailBlocked, and the user is emailed a password reset link
Magic linkNothing more: the link already proved the inboxBlocked, and emailed
PasskeyNothing moreAllowed. A passkey is the way back in for someone the policy blocks

The emailed code is 6 digits. It works for 10 minutes and once, only the newest one works, and a user gets five tries every 10 minutes. It proves the inbox, not a second factor, so the session it opens has mfa: false.

POST https://auth.tilldev.dev/v1/signin → 200 { "mfa_required": true, "method": "email", "challenge_token": "…", "email_hint": "a***@acme.dev", "expires_at": "…" } POST https://auth.tilldev.dev/v1/signin/email-code { "challenge_token": "…", "code": "482915" } → 200 { "ok": true, "user": { … }, "access_token": "…", "refresh_token": "…" } POST https://auth.tilldev.dev/v1/signin → 403 { "error": "This sign-in looked unusual, so it was blocked. …", "code": "signin_blocked" }

Google, GitHub and OIDC sign-ins return to your redirect with the same answers in the fragment: #mfa_required=1&method=email&challenge_token=…&email_hint=… or #error=signin_blocked. The hosted sign-in pages, @tillstack/auth-react 0.3 (useMfa().verifyEmailCode) and Till Authenticator handle both. Both emails can be edited on the app's Emails page, as Sign-in code and Sign-in refused.

04Handle

When a refresh is refused

A refresh the policy refuses answers 401 with a code. Sending the same token again gets the same answer.

POST https://auth.tilldev.dev/v1/refresh → 401 { "error": "Session expired. Please sign in again.", "code": "reauth_required" }
codeMeaning
reauth_requiredThis session ended; ask the user to sign in again.
risk_revokedEvery session of the user ended. Consider telling them to check their account.

With @tillstack/auth-react 0.3 or later, state.reason carries the code once the user is signed out, so you can say why. The OpenID Connect token endpoint answers invalid_grant, as the standard requires.

05Reach

How fast an ended session stops

The refresh token stops at once. Access tokens of the session are refused at their next check by TillAuth's own endpoints, such as /v1/me and /userinfo. A backend that verifies access tokens itself, against the JWKS, accepts them until they expire, at most 10 minutes later. The same holds when you end a session from the dashboard, the CLI, a TillShield playbook or your identity provider.

06Watch

Where it shows

  • The app's Sessions page has a country and risk column and a Risky filter; a user's page shows the same for their sessions.
  • tilldev auth sessions my-app --risky lists sessions scoring 30 or more with their signals.
  • The Policies page counts risky sessions, sessions ended by the policy, sign-ins that owed an extra step and sign-ins blocked, over the last 7 days.
  • The audit log records signin.risk, signin.risk_step, signin.risk_blocked, signin.email_code.ok, signin.email_code.bad, session.risk, session.risk_reauth and session.risk_revoked with the score, signals, travel speed and country, and admin.session_risk_changed for policy changes.
  • The signin.risk*, signin.email_code.ok and session.risk* events can be sent to your webhooks, and reach TillTell like every TillAuth event.
07Limits

What it doesn't do

  • It scores sign-ins and refreshes, not each request. Between refreshes, an access token works for up to 10 minutes.
  • Country and travel come from the edge network in front of TillAuth. A request that arrives without that location is not scored on them, and a session started that way has no location to compare against.
  • Locations are where the network is registered, not GPS. A VPN moves a user as far as its exit.
  • If the address lists can't be read quickly, that signal is skipped rather than holding up the request.
  • An emailed code is only as safe as the inbox. Users who matter should have an authenticator or a passkey; MFA enforcement can require one.
  • A sign-in form you build yourself must handle method: "email" and signin_blocked; one that only knows TOTP will show the wrong prompt.