Onboarding on Auth0¶
The steps a Manager or the operator follows to put a team on Winner AI when
the installation signs in through Auth0. The team is one Auth0 Organization
on your tenant, the one AUTH0_DOMAIN names, and the control plane's
bootstrap organisation is that Organization.
On an installation from the bundle, read the server's commands as
Commands on the other pages
says: in <install>, as the user who owns <install>/.env, with plain
docker compose and no sudo. Most blocks below are not server commands:
they run in a shell signed in to the tenant (auth0 login; the block under
A manager needs two more scopes), and need no server.
They are written for bash: on a Mac, whose terminal runs zsh, type bash
first, since zsh reads the # ... after a command as more of the command.
The organisation¶
One Auth0 Organization per installation, provisioned in the control plane
before anyone signs in. It is the one AUTH0_TEST_ORG_ID
(FLEETVIEW_BOOTSTRAP_ORG_ID) names, and the backend provisions it at every
start. The installation serves that organisation alone; a manager of any other
organisation the control plane once provisioned is refused at the
Dashboard.
Who may sign in¶
The sign-in gate admits every session the identity provider admits and checks no group or role, so the tenant alone decides who reaches the control plane. Winner AI sets none of these settings for you.
- The gate's application (
AUTH0_PROXY_CLIENT_ID) requires an organisation:organization_usage: requirewithorganization_require_behavior: pre_login_prompt, so the login page asks for the organisation first. Enable only this installation's Organization on it. - The Organization's enabled connection keeps
assign_membership_on_login: false. Where database signup is open (the default), anyone who registers and verifies an address can name the Organization at the prompt and join it. - The callback and logout addresses. The gate's application lists
https://<control-plane-host>/oauth2/callbackamong its Allowed Callback URLs andhttps://<control-plane-host>among its Allowed Logout URLs.
The post-login Action (below) also refuses a sign-in through any Organization but this installation's, or through none. It checks which Organization a sign-in came through, not how the person joined it, so both settings stay required.
Check it by hand after creating the application or changing its settings:
sign in as a verified user who belongs to no organisation. Auth0 should stop
them before any page of the control plane loads. The Action's "This
installation does not serve the organization you signed in with" means the
application does not require an organisation and the Action caught it; fix
the setting all the same. Reaching Home, even its empty page, means a
setting is wrong: read the application's organisation settings and each
enabled connection's assign_membership_on_login.
People¶
This page is the Auth0 path. How any provider's people sign in, who is a Manager and how people join and leave is the identity provider guide.
Every person is an Auth0 user in the tenant's database connection, which
must be named Username-Password-Authentication, the name Admin and the
terminal block below create users in. Each is a member
of exactly one organisation provisioned on this deployment, with one
org-scoped role there. The control plane
keeps a row per person in control.users and keeps its role true to Auth0
on its own, with no operator step. To remove someone, remove them from the
organisation in Auth0 (Organizations, the org, Members) or delete the
user; nothing on the host needs editing. Without Auth0 Management credentials
no sync asks Auth0, so a Manager removes the person on Admin instead, and
Restore brings them back as a Member. If the manager group names them, or, where
no manager group is set or no usable groups claim arrives, FLEETVIEW_MANAGER_EMAILS
does, their next request lifts them to Manager, so remove that entitlement before
Restore for Member-only access.
One organisation per person. The installation serves one organisation, but the control plane may still hold rows of another it once provisioned. Admin resolves a person from their existing row by email and, failing that, from the first provisioned organisation Auth0 lists them in; enrollment refuses a person whose row belongs to another organisation. Adding an address that already has an Auth0 user adds that user to this organisation with the chosen role and no password; they sign in with their existing login or reset it. The add is refused, before anything changes in Auth0, when the address or its Auth0 user has a roster row in another organisation the control plane once provisioned, or when that user is an Auth0 member of one. Membership of an Auth0 organisation this deployment has not provisioned does not refuse. To see which organisation holds a refused address:
auth0 api get "users-by-email?email=<address>" | jq '.[].user_id'
auth0 api get "users/<user_id>/organizations" | jq '.[].id'
Then on the server, in <install>, open psql in the database container
and look the row up:
docker compose exec db sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"'
select u.id, u.email, o.auth0_org_id from control.users u
join control.orgs o on o.id = u.org_id
where lower(u.email) = '<address>' or u.auth0_user_id = '<user_id>';
Nothing in Admin moves a person between organisations. Add them under
another address, or, when the other organisation's row is stale, delete
their Auth0 user and the row, in the same psql, with the id the lookup
printed (delete from control.users where id = '<id>';; a row that still
has devices refuses, since its devices reference it), then add them again.
Delete the user rather than only removing them from that organisation:
the other organisation's manager may hold its temporary password, and a
user this control plane created is linked without a verified address.
An existing login must be vouched for. Where database signup is open (the default), an Auth0 user for an address proves only that someone typed it. Admin links one only when Auth0 has verified the address, this control plane created the user, the user is already in this organisation, or their id is on this organisation's roster, revoked rows included. Otherwise the add is refused with nothing changed in Auth0. Have the person verify their address, which the verification email does:
jq -n --arg u "<user_id>" '{user_id:$u}' | auth0 api post jobs/verification-email
Delete that Auth0 user only once the person who holds the address
confirms the login is not theirs, or it is confirmed stale and nobody
uses it; otherwise verification is the path. An add creates a fresh
user, with a temporary password, only when no Auth0 user holds the
address; the users-by-email lookup above lists every one.
A manager¶
Two ways to add one: Admin hands a new user a temporary password, the terminal emails a set-password link.
Through Admin (/admin, "Add member", role manager), which needs a
Manager already and the control plane's Management API credentials
(AUTH0_MGMT_CLIENT_ID and AUTH0_MGMT_CLIENT_SECRET; without them the add
fails): the control plane creates the user, if the address has none, with a temporary password
shown once on the next page, adds them to the organisation and assigns the
role. Hand the password over out of band; nothing makes the person change
it. An existing user is added without a password and signs in with their
existing login or resets it.
From a terminal, with the Auth0 CLI logged in to the tenant, when there
is no manager yet or you would rather not see a password at all.
ROLE_NAME is manager or member; the same block adds either.
The CLI's default login cannot add a member to an Organization or give them a role there. Log it in with the two scopes those steps need, which it requests beside its defaults; a CLI already logged in without them logs in again:
auth0 login --scopes "create:organization_members,create:organization_member_roles"
Then, in the same shell:
set +x # no shell tracing while a password is in a variable
ORG=<organization id> # org_...
ROLE_NAME=manager # or member
EMAIL=<address>; NAME=<display name>
TENANT=<tenant domain>; PROXY_CLIENT_ID=<AUTH0_PROXY_CLIENT_ID>
ROLE=$(auth0 roles list --json | jq -er --arg n "$ROLE_NAME" '.[] | select(.name==$n) | .id')
PW="Fv-$(openssl rand -base64 18 | tr -d '/+=')!x9" # never printed; replaced by the email below
USER_ID=
if [ -z "$ROLE" ]; then
echo "no role named $ROLE_NAME: stop" >&2
elif [ "${#PW}" -lt 20 ]; then
echo 'no password was generated: stop' >&2
# the password reaches jq on stdin, never as a process argument
elif ! USER_ID=$(printf '%s' "$PW" | jq -n --rawfile p /dev/stdin --arg e "$EMAIL" --arg n "$NAME" \
'{connection:"Username-Password-Authentication", email:$e, name:$n, password:$p, email_verified:true}' \
| auth0 api post users | jq -er .user_id); then
echo 'the user was not created: stop' >&2
elif ! jq -n --arg u "$USER_ID" '{members:[$u]}' | auth0 api post "organizations/$ORG/members"; then
echo "$USER_ID was created but not added to the organisation: stop" >&2
elif ! jq -n --arg r "$ROLE" '{roles:[$r]}' | auth0 api post "organizations/$ORG/members/$USER_ID/roles"; then
echo "$USER_ID was added but given no role: stop" >&2
else
# Auth0 emails a set-password link; the generated password is never used
jq -n --arg c "$PROXY_CLIENT_ID" --arg e "$EMAIL" \
'{client_id:$c, email:$e, connection:"Username-Password-Authentication"}' \
| curl -sS --fail-with-body -X POST "https://$TENANT/dbconnections/change_password" \
-H 'Content-Type: application/json' --data-binary @-
fi
unset PW
auth0 api post reads the body from stdin on purpose: --data hangs.
Each step runs only once the one before it succeeded: jq -er fails on a
missing id, and --fail-with-body on a non-2xx from Auth0. A failure prints
which step stopped, and leaves your shell open with the variables set. A
stop after the user was created names its id: a second run of the block
would be refused because the user exists, so run the remaining lines by
hand with that id. The control plane writes the person's row the first time
they sign in, so nothing else is needed.
Why not Auth0 organisation invitations: the sign-in gate does not forward
the invitation parameters to /authorize, so an invitee would reach the
sign-in page without their invitation; the set-password email above gives
the same experience without that hop.
Members and the first team¶
A member, a leaver and the first team are the same on every provider: the identity provider guide describes them. What stays here is what only an Auth0 installation does, which is creating the person in Auth0, in the two sections above.
Enrollment lands a laptop in the organisation of the person who approved
its code at /login/approve, as that person: the laptop pass carries both,
and the installation serves that one organisation. The proxy admits members
as well as managers, so each person approves their own laptop's code. Anyone
in another organisation is refused at sign-in by the post-login Action
(below), and the Dashboard and the approval would
refuse them too: /login/approve resolves only an organisation provisioned
here. A winner from before the
control plane's own login still uses the Auth0 device flow, whose token
carries no organisation: it lands in the control plane's bootstrap
organisation (the one AUTH0_TEST_ORG_ID names) and is refused for a
person who is not a member of it. That check reads only the first page of
members Auth0 returns, so in a larger organisation it may refuse a member
too; a current winner is not affected.
The post-login Action¶
The tenant's post-login Action puts the roles and organisation claims on
the tokens. Where database signup is open (the default), anyone can
register an address a team's roster trusts without owning it; the Action
therefore refuses, in the browser and in winner login alike, a sign-in
unless Auth0 verified the address or the control plane created the account
(its app_metadata tag, which the user cannot write). Admin's rule is
wider: it also links an existing user already in this organisation or on
its roster (An existing login must be vouched for, under People), and the
Action still refuses that user at sign-in until the address is verified.
For the sign-in
gate's application
only, it also refuses a sign-in through any Organization but this
installation's, or through none: without Management credentials the gate's
group decides who is a Manager, so another Organization enabled on that
application would make its Managers Managers here. A winner that still
uses the Auth0 device flow carries no Organization, so that winner login
is not checked for one. A tenant runs the copy each release ships in its
bundle, backend/auth0-post-login-claims.action.js, deployed by hand. Run
these on the operator's own machine, not the server, which needs neither
jq nor a tenant login: in the folder of the release's bundle unpacked there
and checked with the release key, the signature and sha256sum, as
Verify the bundle's signature
shows (its server check, ./scripts/check-host.sh, is for the server, not
this machine), from a shell signed in to the tenant (auth0 login). Auth0 takes the code as a JSON string, and auth0 api
reads a body from stdin because --data hangs.
First: mark verified the accounts you created yourself and know, before the Action is deployed (the last list below says which, and how). Auth0's documentation says post-login Actions run on a refresh-token exchange, so a laptop still enrolled through the Auth0 device flow, for an account the Action would refuse, fails at its next hourly report.
Second: the Action's two secrets. The Action reads
AUTH0_PROXY_CLIENT_ID, the gate's application, and AUTH0_TEST_ORG_ID,
this installation's Organization, as Action secrets that hold the same
values as the installation's .env lines of those names; keep the two in
step. Every command below sends both with the code, so have the .env
values to hand. Without either, or with one empty, the Action refuses every
sign-in on the tenant, winner login included, and the next hourly report of
every laptop still enrolled through the Auth0 device flow, whose report
exchanges a refresh token (Auth0's documentation says post-login Actions run
on that exchange; a laptop on the control plane's own pass is not affected);
recover by running the update below with the values, or in the Auth0
dashboard. A wrong AUTH0_PROXY_CLIENT_ID skips only the Organization
check: the Action does not recognise the gate and checks no Organization,
so only the tenant settings in Who may sign in stand,
while the verified-or-created check still refuses as before. The pair
belongs to the Action, and the last update sent wins: a second
installation's operator who updates the same Action on the same tenant
replaces the first's pair, and it is then the first installation's gate
that goes unchecked.
Deploy the release's copy. The first time on a tenant, create it; the
trigger version v3 is the one Auth0 offers for post-login:
NAME=fleetview-roles # the name the tenant already uses, when it has one
PROXY_CLIENT_ID=<AUTH0_PROXY_CLIENT_ID from the installation's .env>
ORG_ID=<AUTH0_TEST_ORG_ID from the installation's .env>
if [ -z "$PROXY_CLIENT_ID" ] || [ -z "$ORG_ID" ]; then
echo 'both secret values are needed: stop' >&2
else
jq -n --arg n "$NAME" --rawfile c backend/auth0-post-login-claims.action.js \
--arg p "$PROXY_CLIENT_ID" --arg o "$ORG_ID" \
'{name:$n, supported_triggers:[{id:"post-login", version:"v3"}], code:$c,
secrets:[{name:"AUTH0_PROXY_CLIENT_ID",value:$p},{name:"AUTH0_TEST_ORG_ID",value:$o}]}' \
| auth0 api post actions/actions
fi
An action does nothing until it is deployed and bound to the Login flow. The bindings endpoint replaces the whole list, so a body holding only this Action would unbind every other post-login Action the tenant has (an MFA or policy Action). Build the body from the current bindings plus this Action, and read it before the PATCH; the list's order is the order the Actions run in, and this appends the Action last:
# the lookup filters by name; check the parameter against your tenant
ACTION_ID=$(auth0 api get "actions/actions?actionName=$(jq -rn --arg n "$NAME" '$n|@uri')" | jq -er --arg n "$NAME" '.actions[] | select(.name==$n) | .id')
if [ -z "$ACTION_ID" ]; then
echo 'no action named that: stop' >&2
else
echo '{}' | auth0 api post "actions/actions/$ACTION_ID/deploy"
auth0 api get actions/triggers/post-login/bindings | jq . # read what is bound now
auth0 api get actions/triggers/post-login/bindings \
| jq --arg id "$ACTION_ID" --arg n "$NAME" \
'{bindings: ([.bindings[] | {ref:{type:"action_id", value:.action.id}, display_name:.display_name}]
| if any(.ref.value == $id) then . else . + [{ref:{type:"action_id", value:$id}, display_name:$n}] end)}' \
| tee /dev/stderr | auth0 api patch actions/triggers/post-login/bindings
fi
The body is printed to the terminal before it is sent. If any existing entry is missing from it, the field names differ on your tenant: stop and build the list by hand from the first command's output.
To update an action the tenant already has, replace its code and both
secrets, and deploy. It sends both secrets every time, so no update depends
on what Auth0 keeps from an earlier one. It stops with a message, rather
than exiting the shell, when a value or the action is missing, so NAME and
ACTION_ID stay set for the checks after it:
NAME=<the action's name>
PROXY_CLIENT_ID=<AUTH0_PROXY_CLIENT_ID from the installation's .env>
ORG_ID=<AUTH0_TEST_ORG_ID from the installation's .env>
ACTION_ID=$(auth0 api get "actions/actions?actionName=$(jq -rn --arg n "$NAME" '$n|@uri')" | jq -er --arg n "$NAME" '.actions[] | select(.name==$n) | .id')
if [ -z "$PROXY_CLIENT_ID" ] || [ -z "$ORG_ID" ]; then
echo 'both secret values are needed: stop' >&2
elif [ -z "$ACTION_ID" ]; then
echo 'no action named that: stop' >&2
else
jq -n --rawfile c backend/auth0-post-login-claims.action.js \
--arg p "$PROXY_CLIENT_ID" --arg o "$ORG_ID" \
'{code:$c, secrets:[{name:"AUTH0_PROXY_CLIENT_ID",value:$p},{name:"AUTH0_TEST_ORG_ID",value:$o}]}' \
| auth0 api patch "actions/actions/$ACTION_ID" \
&& echo '{}' | auth0 api post "actions/actions/$ACTION_ID/deploy"
fi
Compare what the tenant runs with the release's copy. Print the action
and read its output: the code that runs is on its deployed version, the
draft is the top-level code. Check the field names against your
tenant's output before trusting a clean diff:
ACTION_ID=$(auth0 api get "actions/actions?actionName=$(jq -rn --arg n "$NAME" '$n|@uri')" | jq -er --arg n "$NAME" '.actions[] | select(.name==$n) | .id')
auth0 api get "actions/actions/$ACTION_ID" | jq . # find the deployed version's code in this output
auth0 api get "actions/actions/$ACTION_ID" | jq -j '.deployed_version.code' > /tmp/deployed-action.js
diff /tmp/deployed-action.js backend/auth0-post-login-claims.action.js
No output from diff means they match. An empty or null file means
the field path is wrong or the action was never deployed; take the code from
the printed output instead. Do the compare after every release that changes
the file and on every tenant a deployment uses.
Check the deployed Action holds both secrets. This reads the deployed version's secret names, the ones the running code sees, and never their values (the field path is from Auth0's documentation, not yet seen on a tenant):
auth0 api get "actions/actions/$ACTION_ID" | jq -r '.deployed_version.secrets[]?.name' | sort
Expect exactly AUTH0_PROXY_CLIENT_ID and AUTH0_TEST_ORG_ID; if it prints
nothing, the deployed version holds no secrets or the field path differs on
your tenant: find the secrets' names in the printed action above.
Auth0 shows no secret's value back, so sign in to the control plane yourself
afterwards: a wrong organisation id refuses you with "This installation does
not serve the organization you signed in with". Nothing shows a wrong client
id; when in doubt, run the update again with the .env values.
Which accounts need marking verified. Mark verified only accounts you created yourself and know. Marking one is vouching for its address; marking a stranger's self-signup does exactly what the Action exists to stop.
- Created by Admin ("Add member"): nothing to do. The control plane tags what it creates, and the person signs in with the temporary password as before.
- Created by the terminal block under "A manager" above: nothing, it sets
email_verified: true. - Created another way, in the Auth0 dashboard or by an import, by you, for
an address you know is the person's: mark it verified (the loop below),
but only with your own record of the Auth0 user id that create returned.
A lookup by address finds whoever holds it, a stranger's earlier
self-signup included, even after your create answered 409. After a 409,
or without that record, do not mark it: the account may be the person's
own, used by other applications. Ask them to prove the address with the
verification email under "An existing login must be vouched for" above,
and delete an account only once you know it is not theirs.
created_atdoes not tell such an account from a self-signup for the same address, so never use it as the evidence. - Anyone else who is unverified, including every self-signup: do not mark them. They prove the address with the verification email under "An existing login must be vouched for" above, or you delete them.
The listing below shows who the Action will refuse, not who to mark. It
returns the first 100 users; add &page=1, &page=2 and so on for more. A
search query stops at 1,000 matching records however far you page, so a
larger tenant is not listed in full this way: narrow the query (by
connection or by a created_at range in q) or use Auth0's bulk user
export from the dashboard (the menu names vary; check yours).
auth0 api get 'users?q=email_verified%3Afalse&search_engine=v3&fields=email%2Capp_metadata%2Ccreated_at&include_fields=true&per_page=100&page=0' \
| jq -r '.[] | select(.app_metadata.fleetview_created != true) | [.email, .created_at] | @tsv'
for USER_ID in '<user id>' '<user id>'; do # the ids your own creates returned, never a lookup by address
echo '{"email_verified":true}' | auth0 api patch "users/$USER_ID"
done
Check the listing's query syntax against your tenant if it returns an error or nothing. Where you keep a test tenant, do all of this there before deploying the Action on the tenant your team signs in with.
Checks¶
auth0 orgs members list <org id>lists everyone expected, andauth0 api get organizations/<org id>/members/<user id>/rolesshows the role each should have.- Each manager signs in at
https://<control-plane-host>and sees Home and the Dashboard; each member'swinner loginends with the push service installed, and the roster (/admin) shows the laptop from enrollment. "Awaiting first push" there also means the roster could not read the org schema, so a push is proven by a session count or a last-push time.