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
Context
Section titled “Context”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 client1c7stvxY1tzwUepGeruBNQS2ZOrziDn5; 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
unassignedwith no app access until an admin promotes them.
Non-goals
Section titled “Non-goals”- 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.
Proposed approach
Section titled “Proposed approach”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.
| Role | Auth0 name | Phase I personas | What they can see / do |
|---|---|---|---|
| Admin | admin | Sophia Mann, Andrew Kent (and Armando per the auth doc) | Everything. Manages users + roles. Reads all data. Can flip operator-name visibility, anonymization, etc. |
| Engineer | engineer | Ronald Gonzalez Suarez | Ronald’s full dashboard (workstation grid, per-station live feed, lost-time codes, Excel export, threshold/standard-time settings). Cannot manage users or roles. |
| Executive | executive | Mariana 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:
| Role | Auth0 name | Meaning |
|---|---|---|
| Unassigned | viewer (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 name | Phase II+ personas |
|---|---|---|
| Supervisor | supervisor | Future per-module supervisors. Read-only on their assigned module(s) via module_scope. |
| Research | research | ITBA team. R1/R2/R3 scope question is parked alongside the role. |
Auth0 mechanism
Section titled “Auth0 mechanism”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/role— singular 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 ornull.nullfor every Phase I user.
Specifically:
-
Create three roles in Auth0 dashboard → User Management → Roles for Phase I:
admin,engineer,executive. (supervisorandresearchare created when their parking-folder doc thaws.) Permissions can be left empty in Phase I (roles themselves are the access primitive). -
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 D1if (!userRow || !userRow.active) return;api.accessToken.setCustomClaim('https://lbzfai.com/role', userRow.role); // singular stringapi.accessToken.setCustomClaim('https://lbzfai.com/org_id', userRow.org_id);api.accessToken.setCustomClaim('https://lbzfai.com/module_scope', userRow.module_scope); // null in Phase Iapi.idToken.setCustomClaim('https://lbzfai.com/role', userRow.role); // mirror to ID token for client convenience}; -
In the SPA (
src/pages/index.astro+ future/app/*pages), read the role from the ID token’shttps://lbzfai.com/roleclaim (singular string). -
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.
Why RBAC and not Organizations (parked)
Section titled “Why RBAC and not Organizations (parked)”- 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_idinformationally 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_idfield 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.
App-side role checks
Section titled “App-side role checks”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:
import type { Auth0Client } from '@auth0/auth0-spa-js';
export const ROLE_CLAIM = 'https://lbzfai.com/role'; // singular stringexport const ORG_ID_CLAIM = 'https://lbzfai.com/org_id';export const MODULE_SCOPE_CLAIM = 'https://lbzfai.com/module_scope'; // array | nullexport 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.
Page → role mapping (matches information-architecture.md)
Section titled “Page → role mapping (matches information-architecture.md)”| Route | Allowed roles |
|---|---|
/ | public |
/soporte | public |
/app | any authenticated (then redirects by role) |
/app/ejecutivo | executive, admin |
/app/ejecutivo/modulo/* | executive, admin |
/app/ejecutivo/historico | executive, admin |
/app/ingeniero | engineer, admin |
/app/ingeniero/abrir-jetson | engineer, admin |
/app/ingeniero/exportar | engineer, admin |
/app/investigacion | research, admin |
/app/admin | admin |
/app/cuenta | any authenticated |
http://<jetson>:5000/* | engineer, admin (enforced by Jetson-side auth — see information-architecture.md) |
The /app dispatcher (Phase I — 3 roles)
Section titled “The /app dispatcher (Phase I — 3 roles)”/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.
Persona → Auth0 user mapping (Phase I)
Section titled “Persona → Auth0 user mapping (Phase I)”| Persona | Auth0 user identifier | Role |
|---|---|---|
| Sophia Mann | sophiamann@evasglobal.com (or sophiainesmann@gmail.com) | admin |
| Andrew Kent | TBD email | admin |
| Ronald Gonzalez Suarez | ingenierialbarton@gmail.com | engineer |
| Mariana Saker | TBD email | executive |
| Armando Mann | armandomann@gmail.com or apps4evas@gmail.com | admin (justified: factory liaison; downgraded if/when supervisor thaws) |
| ITBA team (on ITBA Jetson tailnet) | TBD emails | admin 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:
- User Management → Roles: create
admin,engineer,executivefor Phase I. (supervisorandresearchare created when their parking-folder doc thaws.) - 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. - Applications → lbzfai SPA: confirm
Allowed Callback URLsinclude 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. - 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 JWTaudclaim that Flask/FastAPI on the Jetson validates). Owner: Agent B if/when Jetson auth migrates from Tailscale-only to Auth0. - User Management → Users: create the Phase I users. Assign roles.
- Tenant Settings → Settings → Languages: add Spanish (
es) and Spanish-Colombia (es-COif available) so the Universal Login page renders Spanish for LBZF users. Seelocalization-es-co.md. - Tenant Settings → Branding: add the LBZF brand mark to Universal Login for a less-foreign first impression.
How this gets documented
Section titled “How this gets documented”- 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.
Change log
Section titled “Change log”| Date | Change | Author |
|---|---|---|
| 2026-05-10 | Doc drafted. No Auth0 dashboard changes yet. | Agent C |
| (next) | Roles created in Auth0; Action deployed | Sophia + Andrew |
Default deny + audit
Section titled “Default deny + audit”- New users have no role. They authenticate but cannot reach any
/app/*page. They land on/app/cuentawith a banner:Acceso pendiente. Contacte a Sophia. - All role changes are logged in Auth0’s built-in audit log. The admin view
/app/adminshould 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.
Public-by-design Auth0 SPA values
Section titled “Public-by-design Auth0 SPA values”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.
Alternatives considered
Section titled “Alternatives considered”- 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.
Open questions
Section titled “Open questions”- 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.
Cross-bucket dependencies
Section titled “Cross-bucket dependencies”- 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
engineerrole’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
/soporteor/app/cuentalink. - Decision on whether Phase I rolls Operations into multi-tenant Organizations.
What’s weak in this doc
Section titled “What’s weak in this doc”- 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.
- 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.
- 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.
- Custom claim namespace risk. Using
https://lbzfai.com/roleis conventional, but Auth0’s behavior around claim filtering depends on token settings. Worth a smoke test before Ronald’s go-live. - 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.
- 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.
Rollout
Section titled “Rollout”- 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
roleclaim; create users for Sophia, Andrew, Mariana, Ronald, Armando. - 2026-05-22 → 2026-06-01: wire
/appdispatcher + 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.mdif a supervisor is hired; thaw60-parking/cloudflare-tunnel-from-jetson.mdif Mariana needs no-Tailscale viewing; MFA for admin. - Phase III: thaw
60-parking/multi-tenant-auth0.mdif a non-LBZF customer signs.
Appendix / references
Section titled “Appendix / references”~/lbzfai/src/pages/index.astro~/lbzfai/CLAUDE.md- Auth0 docs: https://auth0.com/docs/manage-users/access-control/rbac
- Auth0 Actions: https://auth0.com/docs/customize/actions
- This bucket’s
information-architecture.md - This bucket’s
../../business/user-personas.md - This bucket’s
../../business/ronald-dashboard-ux.md - This bucket’s
../../business/mariana-dashboard-ux.md