One Data PlatformHomeWhyArchitectureGlossaryGatewayRBACAuditConnectorsMount appOrchestrationBuild log
← Home

Step 1: The Gateway + Login (authentication)

What we built, why each piece exists, and how to run it yourself. Prereqs: skim 00-glossary and 02-architecture.


What this step does (and doesn't)

Does: lets a user log in with email + password, hands them a tamper-proof token, and can tell who they are on every later request.

Doesn't yet: decide what they're allowed to do (that's RBAC, Step 2) or route them to apps (Step 2). We're only answering "who are you?" - authentication.

One step, one concept. We resist doing more.

The three files (read in this order)

1. gateway/auth.py - the security primitives

Hand-written so nothing is a black box. Two halves:

Passwords. We never store your real password. We store a hash - run the password through a one-way "meat grinder" (pbkdf2, 100,000 rounds) mixed with a random salt. - Why a hash? If the file leaks, attackers still don't have passwords. - Why a salt? So two people with the same password get different hashes, and attackers can't use precomputed tables. - To check a login we grind the typed password the same way and compare. We compare with hmac.compare_digest so the timing of the comparison can't leak hints.

Tokens (the wristband). After login we issue payload.signature: - payload = readable JSON {"email","role","exp"} (base64). - signature = HMAC of the payload using a secret PLATFORM_SECRET. - Anyone can read the payload, but can't change it without the secret - the signature wouldn't match. Change one character and verify_token returns None. - exp is an expiry timestamp - the wristband dies after 8 hours.

This IS what a real JWT does. We built the 30-line version so the magic is gone.

2. gateway/users.py - the user store

A JSON file (users.json) mapping email → {password_hash, role}. Seeds four demo accounts on first run (admin / analyst / data_scientist / ai_engineer). It stores only hashes, never plain passwords. Later we swap the JSON for SQLite/Postgres without touching the gateway - that's why it hides behind authenticate().

3. gateway/app.py - the gateway (front door)

A small FastAPI service: - GET / - workspace if logged in, else the login page. - POST /login - check credentials → put a signed token in an HttpOnly cookie. - GET /me - "who am I?" (401 if not logged in). Proves the token round-trip. - POST /logout - clears the cookie.

Two security choices worth noticing: - Same error for bad email and bad password ("invalid email or password") - never tell an attacker which half was right. - HttpOnly cookie - JavaScript can't read the token, blocking a common theft trick.

How a login flows (tie it to the architecture doc)

browser ──login(email,pw)──▶ /login ──authenticate()──▶ users.json
                                  │  (password verified)
                                  ▼
                          create_token(email,role)
                                  │
   browser ◀── Set-Cookie: platform_token=<payload.signature> ──┘

later:
browser ──(cookie)──▶ /me ──verify_token()──▶ {email, role}   ✅ who you are

Run it yourself

cd one-data-platform/gateway
pip install -r requirements.txt
uvicorn app:app --reload
# open http://localhost:8000

Log in with phoebe@team.io / admin123 (or any demo account on the login page). Try a wrong password. Open /me in another tab. Log out and watch /me go 401.

Heads-up: it prints [WARN] PLATFORM_SECRET is the dev default. That's intentional - before any real use, set a real secret: export PLATFORM_SECRET="something-long-random".

What you learned this step

What's next - Step 2: RBAC + app registry

Now that we know who you are, we decide what you can open. We'll add apps.yaml (the directory board) listing each app and the role it needs, and the gateway will show you only the apps your role allows. Explainer will be 11-rbac-registry.md.