Jesús Torres

All work

Gatekeeper: delegated access to third-party APIs

How Gatekeeper keeps third-party API credentials server-side and gives each user access only to the providers they were granted.

Stack
TypeScript, Express, MongoDB, Redis
Links
gatekeeper.jatv.dev (live demo)
gitlab.com/JTOpenSource/tools/Gatekeeper

The problem

An external API usually gives you one credential: a client ID and secret, or a token. As soon as several people or services need that API, the credential gets copied into each of them. Rotating it means finding every copy. Removing one person’s access means rotating it for everyone.

The idea

Separate two things that usually travel together: holding a credential and being allowed to use it. Gatekeeper holds the provider credentials. Users hold only a Gatekeeper session, and an admin decides which providers each user may call. The consuming app never sees the provider’s secret.

Your appholds a Gatekeeper tokenprovider secrets never cross this lineGatekeeperwho is calling?granted this provider?decrypt secretAdmin + TOTP

grants, revokes, stores secrets

External APIOAuth / bearer / basic

You can try it in the live demo: it signs you in as a worker with one sandbox provider. Call it and you get its data; ask for any other provider and Gatekeeper answers 403. The sandbox's token never reaches the page.

How a call flows

  1. The user signs in to Gatekeeper and gets a session token. The session is tracked in Redis.
  2. The app sends Gatekeeper the provider name, method, path and body of the call it wants to make.
  3. Gatekeeper verifies the session, loads the user from the database and checks that this provider is on their list. If it is not, the answer is 403.
  4. It loads the provider record and decrypts its secret, stored with AES-256-GCM, at the moment of use.
  5. It authenticates the way that provider expects (OAuth client credentials, bearer token or basic auth), forwards the call and returns the response.

Permissions are read from the database on every call, not baked into the token. Revoking a provider takes effect on the next request, not when the token expires.

Administration

Admins create and remove users, switch roles between admin and worker, and grant or revoke provider access per user. Every admin operation goes through a rate limiter, a session check, a role check and a TOTP code.

Because sessions live in Redis and not only inside the token, signing out invalidates all of a user’s active sessions at once.

Gatekeeper diagram with three flows. New user: request an account from the admin, receive credentials, change the password. Admin: log in, set up Google Authenticator, create users, grant provider access. User: log in, request provider access, execute the provider API call, receive the data. All flows go through the Gatekeeper core, which validates the JWT and admin rights, backed by Redis for sessions and MongoDB for users and encrypted credentials.
The three flows as I drew them when designing Gatekeeper: onboarding, administration and the proxied call.

What it is built on

TypeScript and Express, MongoDB for users and providers, Redis for sessions. Encryption, sessions, the HTTP client and database access come from tslib, my shared TypeScript library, so the same pieces are reused across my other services.

The web console is a small SvelteKit app used as a backend for frontend: the browser never talks to the API, the session token lives in an httpOnly cookie, and provider secrets are write-only in the admin screens. It runs on the same host as the API so rate limits still count each visitor, not the console.

Trade-offs

Every proxied call now depends on Gatekeeper: if it is down, those integrations are down too, and it adds one network hop. Centralizing secrets is the point, and it also makes the Gatekeeper server the one machine that has to be protected best.

It is deliberately small. It authenticates, authorizes and forwards; it is not an API management platform.