Skip to main content

Pluggable action-gated access for any stack: verify an action, email a one-time code, issue a signed JWT.

Project description

Gated Access logo

Gated Access

Pluggable action-gated access for any stack.

Require a verifiable action — star a repo, follow a user, or your own custom check — then email a one-time code and issue a signed access token.

FastAPI · zero-setup local demo · swap an adapter, not your project


The idea

Plenty of products want to gate access behind an action: "do this, then you're in." The fragile way is to hard-wire that one action into your app. When the platform or the rules change, you rewrite everything.

Gated Access treats the action as a swappable adapter. The engine only knows the flow:

  ┌──────────┐    ┌───────────┐    ┌──────────────┐    ┌─────────────┐
  │ Identify │ -> │  Action   │ -> │  One-time    │ -> │   Access    │
  │ (OAuth)  │    │ (verify)  │    │  code (mail) │    │   (JWT)     │
  └──────────┘    └───────────┘    └──────────────┘    └─────────────┘

The user signs in, Gated Access checks the action against their real account, emails a single-use code, and exchanges that code for a JWT your app can verify with a shared secret. Identity, verification, email, and storage are each an adapter — change one and nothing else moves.

"Any stack" means the engine is one small headless HTTP API plus a drop-in frontend. Your app can be anything; it just talks to four endpoints (or verifies the JWT directly).


Quickstart — the local demo

No GitHub app, no email provider, no credit card. It runs entirely on your machine.

pip install gated-access
gated-access run        # open http://localhost:8000

Click through all four steps. In demo mode Gated Access:

  • simulates the GitHub sign-in (no real OAuth needed),
  • lets you toggle "I did the action" to flip what the verifier sees,
  • prints the email to the console and shows it in an on-screen demo inbox so you can read the code without sending real mail.

CLI

Command What it does
gated-access run [--host H] [--port P] [--config PATH] Start the server. Host/port default to the config values; --config points at an alternate gated-access.yaml.
gated-access secrets Print ready-to-paste GATED_ACCESS_JWT_SECRET= / GATED_ACCESS_SESSION_SECRET= lines (cryptographically random).
gated-access init [--force] Interactively write a commented gated-access.yaml in the current directory. Pressing Enter at every prompt yields the demo config; non-demo choices print the env vars you still need to set.
gated-access version Print the installed version.

python -m gated_access also starts the server, and uvicorn gated_access.main:app remains the plain ASGI entrypoint for deployment.


How it fits together

Four adapter families, each behind a tiny interface in gated_access/adapters/base.py:

Adapter Demo default Production options
Identity — who is this user? demo (fabricated user) github (OAuth web flow)
Verification — did they do it? manual (simulated) github_star, github_follow
Email — deliver the code console (+ demo inbox) resend, smtp
Storage — sessions & codes memory sqlite

Adding a new check (a webhook, an on-chain event, "is a paying customer") is just a new VerificationAdapter subclass — the routes, codes, tokens, and UI stay exactly the same.


Configuration

Everything is driven by gated-access.yaml, with environment variables overriding the file so secrets never live in config. Common keys:

Key Meaning
identity.provider demo or github
verify.type manual · github_star (needs verify.repo) · github_follow (needs verify.username)
email.provider console · resend · smtp
storage.backend memory · sqlite
code.length / code.ttl_seconds / code.single_use one-time code policy
access_ttl_seconds JWT lifetime
demo_mode enables the simulated login + /dev/* endpoints — set false in production
branding.* product name, copy, and accent colour shown in the UI

See .env.example for the full list of secret/deploy variables.


HTTP API

The frontend is optional — these are all you need to integrate from any language.

Method & path Purpose
GET /auth/login Begin sign-in (redirects to the identity provider)
GET /auth/callback Finish sign-in; sets a signed, http-only session cookie
GET /api/session Current status: authenticated? action done? verified?
POST /api/verify Run the verification adapter; on success, email a one-time code
POST /api/redeem Body {"code": "..."}{"access": true, "token": "<JWT>"}
GET /api/me Example protected resource — requires Authorization: Bearer <JWT>
GET /healthz Liveness check
POST /dev/perform-action, GET /dev/inbox Demo-only helpers (disabled when demo_mode: false)

Your backend doesn't even have to call /api/me — verify the JWT yourself with the shared jwt_secret (HS256, issuer gated-access).


Going to production

  1. Generate secrets and set them in the environment:
    gated-access secrets    # prints GATED_ACCESS_JWT_SECRET=... and GATED_ACCESS_SESSION_SECRET=...
    
    Gated Access refuses to start with demo_mode: false while the dev placeholder secrets are still in place.
  2. Create a GitHub OAuth app at https://github.com/settings/developers. Set the callback URL to https://your-domain/auth/callback and export GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_REDIRECT_URI.
  3. Switch the adapters in gated-access.yaml:
    demo_mode: false
    identity: { provider: github }
    verify:   { type: github_star, repo: "you/your-repo" }
    email:    { provider: resend }       # set RESEND_API_KEY in env
    storage:  { backend: sqlite }
    
  4. Serve it behind HTTPS (the session cookie is marked Secure automatically when base_url is https):
    uvicorn gated_access.main:app --host 0.0.0.0 --port 8000
    

Because it's a plain ASGI app with no required cloud services, it deploys anywhere that runs Python — a container, Render, Railway, Fly, a VM, etc.


Security notes

  • One-time codes drawn from an unambiguous alphabet (no 0/O/1/I/L), single-use and time-limited, and bound to the session that earned them — a code minted for one user is rejected for another.
  • JWT access tokens (HS256) with issuer and expiry; verify with your shared secret.
  • OAuth state is single-use and short-lived (CSRF protection); the session cookie is signed (itsdangerous), http-only, and Secure over HTTPS.
  • The provider access token is held only until verification succeeds, then dropped.
  • /api/redeem is rate-limited per session + IP to blunt code-guessing.

A note on GitHub's rules

GitHub's Acceptable Use Policies prohibit incentivizing stars ("inauthentic interactions" and giving stars in exchange for any benefit). So a real "star my repo to unlock" gate aimed at the public would violate those terms.

This project is built as a portfolio / reference demo of the pattern, not a growth hack. Keep it on the right side of the line by pointing the verifier at your own repo for testing, or by using a check that isn't incentivized starring — github_follow, a webhook, "is a customer", or any custom VerificationAdapter you write. The whole point of the adapter design is that the gate survives exactly this kind of rule change.


Project layout

gated_access/
├── gated_access/
│   ├── adapters/        identity · verification · email · storage (+ base interfaces)
│   ├── service.py       the engine: begin_auth → verify → redeem
│   ├── factory.py       builds adapters from config and wires the FastAPI routes
│   ├── security.py      signed cookies, OAuth state, rate limiter
│   ├── tokens.py        one-time codes + JWT
│   ├── config.py        typed config (YAML + env)
│   ├── cli.py           the `gated-access` command (run · secrets · init · version)
│   ├── _version.py      single source of truth for the version
│   ├── templates/       demo UI
│   └── static/          styles, client logic, logo
├── tests/               token unit tests + full end-to-end flow
├── pyproject.toml       packaging (hatchling) — the source of truth for dependencies
├── gated-access.yaml         configuration (demo defaults)
├── .env.example         production secrets template
└── requirements*.txt    convenience for clone-based development

Development

git clone https://github.com/NI3singh/gated-access && cd gated-access
pip install -e .[dev]
pytest -q
gated-access run            # or: python -m gated_access

Publishing (maintainers)

python -m build                                # dist/gated-access-X.Y.Z.tar.gz + .whl
twine check dist/*
twine upload --repository testpypi dist/*      # dress rehearsal
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple gated-access
twine upload dist/*                            # real release (needs a PyPI API token)

The version lives in gated_access/_version.py only — bump it there.


License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gated_access-1.0.1.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gated_access-1.0.1-py3-none-any.whl (35.4 kB view details)

Uploaded Python 3

File details

Details for the file gated_access-1.0.1.tar.gz.

File metadata

  • Download URL: gated_access-1.0.1.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for gated_access-1.0.1.tar.gz
Algorithm Hash digest
SHA256 284198a4e4c09508e45fffa19ec504ff04503a23dcc7195ccc1bf00f5dbec67a
MD5 9ec7a722290d6c311e451abdfc32aa92
BLAKE2b-256 502f8a7e810e7952aa823d5b361ea96cbf166c15b28607a5ec08dd717ada9047

See more details on using hashes here.

File details

Details for the file gated_access-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: gated_access-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 35.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for gated_access-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fe7875bec1abc3a13b746c6a40cdfe987c10161d5b475d4129e198889782c8eb
MD5 e94eb82f020c3ef0fe6c92f0cadd59da
BLAKE2b-256 208af91e06f50e0f51915c5d1daf04a35b61a5725df083b35c2ea03d37511c1b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page