FGA as an API Gateway Enforcement Point

A real Cloudflare Worker gateway, a real OpenFGA store — the access token carries no permissions claim at all; the allow/deny decision happens entirely at the gateway.

Session → Access Token → Gateway (PEP) → OpenFGA (PDP) → Decision

tokens app

session, no RBAC scopes on this token

Access Token

forwarded server-side, as a Bearer header

Real gateway

Cloudflare Worker

PEP — verifies JWT (RS256, exact aud/iss)

Real FGA store

OpenFGA Check

PDP — can_read / can_write, ReBAC tuples

Allow / Deny

real document response, or a real 403 / 503

1This app's access token carries no permissions claim at all — unlike rbac-permissions, the token itself proves identity only. Every authorization decision happens out-of-band, at the gateway.
2The Worker is a real Policy Enforcement Point in front of real resource routes (GET/PUT /documents/:id) — it derives the FGA check from the actual incoming method and path, never from a client-asserted body field.
3OpenFGA Check resolves can_read/can_write against real ReBAC tuples: owner, direct editor/viewer, or team membership (member from team) — a single tuple change can flip access to every document a team was granted, with no code change.
4Three outcomes are possible, not two: an explicit allow, an explicit FGA deny (403), and an infrastructure failure (503) when the FGA call itself is unreachable — the Worker never renders a 503 as a deny.

Not a token-hiding boundary

The proxy route below forwards your access token to the Worker server-side, but this page also shows you that same token via the standard token viewer — you can see exactly what the Worker receives. The server-side hop exists to control how the token reaches the Worker (a known audience, a controlled forward), not to hide it from you.

The model and tuples behind this demo

This is the actual OpenFGA authorization model deployed to the real store the Worker checks against — not a simplified stand-in. viewer propagates through team membership via the member from team indirect-relationship pattern; can_read/can_write are the only two relations the gateway ever checks, never viewer/editor directly.

model
  schema 1.1

type user

type team
  relations
    define member: [user]

type document
  relations
    define owner: [user]
    define team: [team]
    define viewer: [user] or owner or member from team
    define editor: [user] or owner
    define can_read: viewer or editor
    define can_write: editor

And the seed tuples that produce alice/bob/carol's three different outcomes (user:alice stands in for the real auth0|... subject in the live store):

userrelationobject
user:alicememberteam:platform-eng
user:bobmemberteam:platform-eng
user:aliceownerdocument:doc-001
team:platform-engteamdocument:doc-002
user:aliceeditordocument:doc-002

Carol has no row in this table at all — that absence is the entire reason she's denied everywhere, not a separate deny rule.

Demo login

This demo needs a specific, pre-built OpenFGA tuple graph to show the contrast between the three identities below, so it isn't self-serve for a brand-new account. Sign in with one of three shared test accounts:

fga-gateway-alice@atko.email

owner of doc-001, editor of doc-002 (team + direct) — allow on everything

fga-gateway-bob@atko.email

viewer-only on doc-002 via team membership, no relation to doc-001 at all — read allowed on doc-002, everything else denied

fga-gateway-carol@atko.email

no OpenFGA tuples at all — denied on every document, every action

Password for each account was set during this demo's provisioning and is not repeated here — ask whoever set up this tenant if you don't have it.

Log in to try it →

Switching identities? Sign out first, then use this same link again — it forces a fresh credential prompt (prompt=login) rather than silently re-using whichever Auth0 SSO session is still active in this browser.

Security Implications

Strengths

  • A centralized policy enforcement point decouples authorization logic from application code — the Worker, not any individual backend route, is the single place that decides allow/deny, so the same check logic is enforceable at any gateway in front of any backend.
  • Relationship-based access control (ReBAC) via OpenFGA scales past flat RBAC — a single tuple change (e.g. team membership) can flip access to many resources at once, without touching application code or redeploying anything.
  • The gateway fails closed, not open: an OpenFGA outage or timeout returns 503 (unavailable), never a false allow or a false deny — the demo deliberately distinguishes "denied" from "the authorization service itself is down," which most hand-rolled integrations don't bother to separate.

Risks & Considerations

  • Every protected request now has a real network dependency on OpenFGA, on top of the gateway itself — a slow or unreachable FGA store adds latency (or, correctly, a 503) to every single request, not just a rare edge case.
  • A validly-audienced access token can call the Cloudflare Worker directly, bypassing this tokens app entirely — the audience check proves the token is for this API, not that it was obtained through this specific application's login flow. This is a stated, accepted residual risk for a demo, not something this architecture eliminates on its own.
  • The OpenFGA store and its authorization model are a single point of trust for every downstream decision — if the model is misconfigured, or the pinned model ID is ever repointed carelessly, every protected route behind the gateway is affected at once.

Hardening Checklist

  • Fail closed on infrastructure errors rather than fail open — already built this way here (503 on FGA outage/timeout, never silently treated as an allow or a deny).
  • Pin the authorization model ID explicitly on every check — already done here — rather than letting checks silently resolve against whatever model is "latest" in the store.
  • Rate-limit the check path in production; a single compromised or leaked token could otherwise generate unbounded authorization-check traffic.
  • For a real production deployment (not this demo), consider mTLS or a signed-request scheme between the gateway and the actual backend it fronts, so the backend itself can verify a request genuinely passed through the enforcement point rather than trusting network topology alone.