Skip to content

Auth0 roles & access

Status: reviewed (Phase B seams applied — JWT claim canonicalized to https://lbzfai.com/role; 5-role matrix and multi-tenant Organizations parked; Phase I uses 3 effective roles) Author: Agent C (frontend bucket, round 1) + Phase B reconciliation Reviewers: Sophia Mann, Andrew Kent, Agent B (integration / Jetson auth), Agent E (privacy + tenancy) Last updated: 2026-05-12

Today the only Auth0 surface is the SPA login on src/pages/index.astro. Anyone in the Auth0 tenant who can log in is, effectively, an “admin”. As we add roles (Ronald, Mariana, ITBA, future supervisors), we need a defensible role model that the application code, the Cloudflare Workers static site, and the Jetson dashboard all consume consistently.

Anchor artifacts:

  • ~/lbzfai/src/pages/index.astro (the current Auth0 SPA setup)
  • ~/lbzfai/CLAUDE.md §“Architecture” (Auth0 tenant: dev-sr1ucvolmqcviwvo.us.auth0.com; SPA client 1c7stvxY1tzwUepGeruBNQS2ZOrziDn5; SPA client values are public-by-design)
  • This bucket’s user-personas.md
  • Auth0 docs: RBAC, Organizations, Actions
  • A small, durable set of roles that maps 1:1 onto the personas in user-personas.md.
  • A concrete Auth0 mechanism: roles + permissions, with Organizations as a future-ready stub but not depended on for Phase I.
  • A clear list of dashboard config changes — what gets created in the Auth0 dashboard, who creates it, and where it’s documented (this file is canonical).
  • Role checks consistent between lbzfai.com (Cloudflare Workers) and the Jetson :5000 dashboard.
  • A defensible default: until proven otherwise, roles are deny-by-default. New users land as unassigned with no app access until an admin promotes them.
  • Replacing the hardcoded-fallback Auth0 SPA Client ID and Domain. Per CLAUDE.md, those are public-by-design and stay.
  • A full identity-provider migration (no SAML / SCIM in Phase I).
  • A user-management UI inside the app. Phase I admins do user management directly in the Auth0 dashboard.
  • The exact Jetson-side enforcement mechanism — that depends on the information-architecture.md “Jetson auth model” open question. This doc specifies the role contract; Agent B implements its enforcement on the Jetson.

Phase I uses 3 effective roles (full 5-role matrix is parked)

Section titled “Phase I uses 3 effective roles (full 5-role matrix is parked)”

Canonical role list across the project: admin / engineer / executive / supervisor / research, plus viewer deny-by-default. Phase I exercises only admin / engineer / executive; supervisor and research are preserved in docs/design/60-parking/full-role-matrix.md and thaw when a real person attaches to them.

RoleAuth0 namePhase I personasWhat they can see / do
AdminadminSophia Mann, Andrew Kent (and Armando per the auth doc)Everything. Manages users + roles. Reads all data. Can flip operator-name visibility, anonymization, etc.
EngineerengineerRonald Gonzalez SuarezRonald’s full dashboard (workstation grid, per-station live feed, lost-time codes, Excel export, threshold/standard-time settings). Cannot manage users or roles.
ExecutiveexecutiveMariana Saker/app/ejecutivo: module tiles, weekly/monthly trend, per-module drill-down (aggregates only). Stills, never video. No operator names.

Plus the implicit zero state:

RoleAuth0 nameMeaning
Unassignedviewer (or no role)Authenticates successfully but lands on a “Acceso pendiente — Contacte a Sophia” page. No data access.

The two parked roles, for reference (see 60-parking/full-role-matrix.md for the full permission matrix):

Role (parked)Auth0 namePhase II+ personas
SupervisorsupervisorFuture per-module supervisors. Read-only on their assigned module(s) via module_scope.
ResearchresearchITBA team. R1/R2/R3 scope question is parked alongside the role.

Use Auth0 RBAC (Roles + Permissions on the Auth0 tenant), surfaced via custom claims in the access token.

The canonical claim shape — single source of truth, identical across backend/user-org-and-auth.md, this doc, and the Jetson middleware:

  • https://lbzfai.com/rolesingular string ("admin" | "engineer" | "executive"). The claim is namespaced and singular; not a non-namespaced name, not a plural array, not a nested object.
  • https://lbzfai.com/org_id — integer (1 = LBZF, 2 = ITBA informational).
  • https://lbzfai.com/module_scope — array or null. null for every Phase I user.

Specifically:

  1. Create three roles in Auth0 dashboard → User Management → Roles for Phase I: admin, engineer, executive. (supervisor and research are created when their parking-folder doc thaws.) Permissions can be left empty in Phase I (roles themselves are the access primitive).

  2. Add an Auth0 Action (in Flows → Login) that looks up the user and writes the three custom claims:

    // Auth0 Action — Login flow — "Add role to token"
    exports.onExecutePostLogin = async (event, api) => {
    const userRow = await lookupByEmail(event.user.email); // users.json or D1
    if (!userRow || !userRow.active) return;
    api.accessToken.setCustomClaim('https://lbzfai.com/role', userRow.role); // singular string
    api.accessToken.setCustomClaim('https://lbzfai.com/org_id', userRow.org_id);
    api.accessToken.setCustomClaim('https://lbzfai.com/module_scope', userRow.module_scope); // null in Phase I
    api.idToken.setCustomClaim('https://lbzfai.com/role', userRow.role); // mirror to ID token for client convenience
    };
  3. In the SPA (src/pages/index.astro + future /app/* pages), read the role from the ID token’s https://lbzfai.com/role claim (singular string).

  4. On the Jetson side, Phase I uses Tailscale-injected identity headers per ADR-005; the JWT-validation path is parked alongside the Cloudflare-Tunnel design.

  • Phase I has one customer (LBZF). Organizations is the right primitive for multi-tenant SaaS — separate orgs per customer with isolated user pools. We do not need that yet.
  • ITBA’s twin is a separate Jetson; same Auth0 tenant for convenience, distinct org_id informationally only. No multi-tenant enforcement code in Phase I.
  • Migration to Organizations is preserved in docs/design/60-parking/multi-tenant-auth0.md. The Phase I seeds (namespaced claims, org_id field on every user) make that migration mechanical when it happens.
  • For Phase I, we set per-user metadata lbzfai.tenant = "lbzf" (or "itba-bsas" for ITBA users) so the future migration is mechanical.

Use a small client-side helper (TypeScript) usable from any Astro page. The role claim is a singular string, so the helper is a getRole() that returns one value or null:

src/lib/auth/roles.ts
import type { Auth0Client } from '@auth0/auth0-spa-js';
export const ROLE_CLAIM = 'https://lbzfai.com/role'; // singular string
export const ORG_ID_CLAIM = 'https://lbzfai.com/org_id';
export const MODULE_SCOPE_CLAIM = 'https://lbzfai.com/module_scope'; // array | null
export type Role = 'admin' | 'engineer' | 'executive' | 'supervisor' | 'research';
export async function getRole(auth0: Auth0Client): Promise<Role | null> {
const user = await auth0.getUser();
const raw = user?.[ROLE_CLAIM];
return (typeof raw === 'string' ? (raw as Role) : null);
}
export async function hasRole(auth0: Auth0Client, role: Role): Promise<boolean> {
return (await getRole(auth0)) === role;
}
export async function hasAnyRole(auth0: Auth0Client, ...roles: Role[]): Promise<boolean> {
const r = await getRole(auth0);
return r !== null && roles.includes(r);
}

This file does not exist yet. It is part of the Phase I scaffolding. The Role union type intentionally includes supervisor and research so the parked roles compile-check; Phase I just never sees those values at runtime.

RouteAllowed roles
/public
/soportepublic
/appany authenticated (then redirects by role)
/app/ejecutivoexecutive, admin
/app/ejecutivo/modulo/*executive, admin
/app/ejecutivo/historicoexecutive, admin
/app/ingenieroengineer, admin
/app/ingeniero/abrir-jetsonengineer, admin
/app/ingeniero/exportarengineer, admin
/app/investigacionresearch, admin
/app/adminadmin
/app/cuentaany authenticated
http://<jetson>:5000/*engineer, admin (enforced by Jetson-side auth — see information-architecture.md)
/app → read role from https://lbzfai.com/role (singular string):
role == "admin" → /app/admin (default; admin can manually navigate elsewhere)
role == "engineer" → /app/ingeniero
role == "executive" → /app/ejecutivo
// parked: role == "research" → /app/investigacion
// parked: role == "supervisor" → /app/ejecutivo/modulo/<assigned-module>
else (null/viewer) → /app/cuenta with banner: "Acceso pendiente. Contacte a Sophia."

role is a single string per the canonical claim; there is no “multiple roles → which wins” logic to worry about in Phase I.

PersonaAuth0 user identifierRole
Sophia Mannsophiamann@evasglobal.com (or sophiainesmann@gmail.com)admin
Andrew KentTBD emailadmin
Ronald Gonzalez Suarezingenierialbarton@gmail.comengineer
Mariana SakerTBD emailexecutive
Armando Mannarmandomann@gmail.com or apps4evas@gmail.comadmin (justified: factory liaison; downgraded if/when supervisor thaws)
ITBA team (on ITBA Jetson tailnet)TBD emailsadmin on the ITBA Jetson user list; no role on LBZF Jetson

OPEN: Confirm Mariana’s preferred email for Auth0 account. Owner: Sophia + Armando → Mariana, target: 2026-05-20.

OPEN: ITBA team emails. Owner: Sophia → Andrew → ITBA team, target: 2026-05-13 (Argentina trip).

OPEN: Armando as admin? Owner: Sophia + Andrew, target: 2026-05-13. Default proposed: yes for Phase I.

The research role and the ITBA-on-LBZF-data scope (R1/R2/R3) are parked together in 60-parking/full-role-matrix.md. Phase I treats ITBA as admin on their own Jetson and nothing on LBZF’s.

The “research role and the LBZF data” question — PARKED (Phase II)

Section titled “The “research role and the LBZF data” question — PARKED (Phase II)”

ITBA receives twin hardware on 2026-05-15. The R1/R2/R3 question (do they see LBZF data, only their own, or aggregates of LBZF) is parked alongside the research role itself in 60-parking/full-role-matrix.md.

Phase I posture: ITBA team members are admin on the ITBA Jetson (the twin) and have no role on the LBZF Jetson. Sophia + Andrew are cross-org admin (manually present in both Jetsons’ users.json) so they can pull aggregates for the paper. No /app/investigacion page ships in Phase I.

Trigger to thaw: ITBA explicitly requests a view of LBZF data and Mariana signs off on the scope (R1/R2/R3) with Agent E’s privacy review.

Dashboard config changes (Auth0 dashboard)

Section titled “Dashboard config changes (Auth0 dashboard)”

The Auth0 changes needed for Phase I, in order:

  1. User Management → Roles: create admin, engineer, executive for Phase I. (supervisor and research are created when their parking-folder doc thaws.)
  2. Actions → Flows → Login: add the “Add role to token” custom action (code snippet above — emits singular-string claim https://lbzfai.com/role) and bind it to the Login flow.
  3. Applications → lbzfai SPA: confirm Allowed Callback URLs include all currently-used origins (http://localhost:4321, https://lbzfai.sophiainesmann.workers.dev, https://lbzfai.com). When Jetson auth lands (Phase I+, post-information-architecture decision), add the Jetson surface here.
  4. APIs → (new) lbzfai-jetson-api: create a custom API representing the Jetson backend, audience URL https://api.lbzfai.com (placeholder; even if no public hostname, this is used as the JWT aud claim that Flask/FastAPI on the Jetson validates). Owner: Agent B if/when Jetson auth migrates from Tailscale-only to Auth0.
  5. User Management → Users: create the Phase I users. Assign roles.
  6. Tenant Settings → Settings → Languages: add Spanish (es) and Spanish-Colombia (es-CO if available) so the Universal Login page renders Spanish for LBZF users. See localization-es-co.md.
  7. Tenant Settings → Branding: add the LBZF brand mark to Universal Login for a less-foreign first impression.
  • This file (auth0-roles-and-access.md) is canonical for the role model.
  • Each change in the Auth0 dashboard is recorded here under a “Change log” section (next).
  • The Auth0 Action code lives in the repo at infra/auth0/actions/add-roles-to-token.js (new path; not created yet). Cross-link from this doc when it lands.
DateChangeAuthor
2026-05-10Doc drafted. No Auth0 dashboard changes yet.Agent C
(next)Roles created in Auth0; Action deployedSophia + Andrew
  • New users have no role. They authenticate but cannot reach any /app/* page. They land on /app/cuenta with a banner: Acceso pendiente. Contacte a Sophia.
  • All role changes are logged in Auth0’s built-in audit log. The admin view /app/admin should surface this log (Phase II — for Phase I, admins use Auth0 dashboard directly).
  • Token TTL: Auth0 default (rolling sliding window). For an industrial app where Ronald may have a session open for hours, set Refresh Token rotation on. OPEN: confirm token TTL and refresh policy. Owner: Agent B + Sophia, target: 2026-06-01.

Per ~/lbzfai/CLAUDE.md: the SPA Client ID 1c7stvxY1tzwUepGeruBNQS2ZOrziDn5 and Domain dev-sr1ucvolmqcviwvo.us.auth0.com are intentionally hardcoded with env fallback. Do not remove the hardcode unless build-time env vars are wired through Cloudflare’s Builds → Build Variables surface, and the swap is documented in this file and CLAUDE.md.

Migration path to Auth0 Organizations — PARKED

Section titled “Migration path to Auth0 Organizations — PARKED”

Preserved in docs/design/60-parking/multi-tenant-auth0.md. The Phase I seeds (namespaced https://lbzfai.com/… claims, per-user lbzfai.tenant metadata, integer org_id on every user row) make the eventual migration mechanical when it thaws.

  • Auth0 Organizations from day 1. Rejected — overkill for one customer + one research lab, and Organizations changes the default Universal Login experience (org-picker) in ways Ronald might find confusing.
  • Permissions instead of roles (e.g., dashboard:read, export:write). Rejected for Phase I — six users, five roles; permissions would multiply maintenance without value. Re-add if/when a custom permission emerges that doesn’t fit a role.
  • No RBAC, just check email domains. Rejected — fragile (gmail.com vs evasglobal.com), no audit trail, can’t model ITBA cleanly.
  • App-defined roles in a separate database, Auth0 used only for identity. Rejected — duplicates Auth0’s existing feature and complicates Jetson-side enforcement.
  • Single shared password for the Jetson dashboard, no per-user auth. Rejected for any role beyond Phase 0 development.
  • CLOSED via ADR-005: Jetson auth model. Tailscale-only for Phase I; Tailscale-User-Login header is the identity injection. Auth0-on-Jetson + Cloudflare Tunnel are parked in 60-parking/cloudflare-tunnel-from-jetson.md.
  • PARKED: Research role data scope (R1 / R2 / R3). Lives in 60-parking/full-role-matrix.md. ITBA team has no LBZF role in Phase I.
  • OPEN: Mariana’s, Andrew’s, ITBA team’s Auth0 emails. Owner: Sophia, target: 2026-05-20.
  • OPEN: Token TTL + refresh policy. Owner: Agent B + Sophia, target: 2026-06-01.
  • PARKED: Multi-tenancy posture for the long term. Lives in 60-parking/multi-tenant-auth0.md.
  • OPEN: Is Armando admin? Owner: Sophia + Andrew, target: 2026-05-13.
  • OPEN: How does admin role-change happen in Phase I — only via Auth0 dashboard, or an in-app surface? Owner: Sophia, target: 2026-06-01. Proposed: dashboard-only in Phase I.
  • From backend/integration (Agent B): Jetson-side JWT validation if/when we move off Tailscale-only.
  • From hardware/ML (Agent A, D): none directly; indirectly, the engineer role’s ability to see live video depends on whether the inference pipeline produces anonymized vs raw frames.
  • From business/cross-cutting (Agent E):
    • Confirmation that the research role scope (R2) is legally defensible with respect to operator data.
    • Privacy policy text that lists what each role can see — surfaced as a /soporte or /app/cuenta link.
    • Decision on whether Phase I rolls Operations into multi-tenant Organizations.
  1. The Jetson enforcement story is missing. This doc names the role contract, but if Jetson auth stays “Tailscale-only” for Phase I, Auth0 roles don’t actually gate anything on Ronald’s working dashboard. That’s tolerable only because everyone who is on Tailscale today is already trusted — it breaks the moment a fourth Tailscale user appears with the wrong intentions. Round 2: revisit with Agent B.
  2. No automated provisioning. Phase I is “Sophia clicks through the Auth0 dashboard for each user”. That doesn’t scale past ~10 users; it also has no audit trail beyond what Auth0 logs natively. We need either a small admin UI in the app or a documented manual runbook before user count grows.
  3. ITBA’s R2 proposal hasn’t been negotiated with ITBA. Imposing “you can see aggregated LBZF data” on a six-person collaborator team is a conversation, not a unilateral decision. Until that conversation happens (Argentina trip), R2 is a proposal.
  4. Custom claim namespace risk. Using https://lbzfai.com/role is conventional, but Auth0’s behavior around claim filtering depends on token settings. Worth a smoke test before Ronald’s go-live.
  5. No formal threat model. What does “an attacker steals an Auth0 session cookie from Mariana’s phone” actually let them do? We have not enumerated.
  6. No 2FA / MFA decision. For an industrial monitoring system, 2FA on admin is table stakes; we haven’t said so. Should be a Phase I requirement for admin role.
  • Argentina demo (2026-05-13): present 3-role Phase I model to Andrew + ITBA.
  • 2026-05-15 → 2026-05-22: create the three Phase I roles in Auth0; deploy the Login Action with the singular-string role claim; create users for Sophia, Andrew, Mariana, Ronald, Armando.
  • 2026-05-22 → 2026-06-01: wire /app dispatcher + per-page role checks on lbzfai.com.
  • 2026-06-01 → 2026-06-15: ITBA users provisioned on the ITBA Jetson’s user list (not LBZF’s).
  • 2026-07 deploy: three Phase I roles (admin, engineer, executive) live.
  • Phase II: thaw 60-parking/full-role-matrix.md if a supervisor is hired; thaw 60-parking/cloudflare-tunnel-from-jetson.md if Mariana needs no-Tailscale viewing; MFA for admin.
  • Phase III: thaw 60-parking/multi-tenant-auth0.md if a non-LBZF customer signs.