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. Refuses to start on blocking config errors.
gated-access check [--config PATH] Config doctor: reports missing secrets/credentials, bad adapter settings, and risky production choices before you deploy. Exit 1 on blocking problems.
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.


Embedding in front of another app

Gated Access is a standalone gate, not a library you paste into your app. To gate a feature in your product (e.g. "only users who starred my repo can use this"), run the gate on its own subdomain and integrate at two points in your own code.

1. Run the gate at e.g. gate.yoursite.com with identity: github, verify: { type: github_star, repo: "you/repo" }, a real email provider, sqlite storage, and set app_return_url (or GATED_ACCESS_APP_RETURN_URL) to a URL on your app. After a user passes the gate, the success page shows a "Return to your app →" button that sends the browser to:

https://yoursite.com/unlock-return#ga_token=<JWT>

The token rides in the URL fragment, so it never reaches servers or referrers.

2. Consume it in your app. Your front end reads the fragment and posts it to your backend; your backend verifies the JWT with the same secret the gate signs with (GATED_ACCESS_JWT_SECRET) and records the user as unlocked:

from gated_access.tokens import verify_access_token   # same HS256 logic the gate uses

claims = verify_access_token(SHARED_SECRET, token)    # checks signature, expiry, issuer
if not claims:
    raise Unauthorized()
mark_unlocked(current_user.id, github_sub=claims["sub"])   # persist in YOUR db

Then gate your feature on that stored flag. Because the JWT is signed and stateless, your app verifies it without calling the gate. To test the consume side without OAuth, mint a token directly with issue_access_token(SHARED_SECRET, subject="123", username="me", ttl_seconds=3600).

A complete runnable host app (unlock-return page, /unlock endpoint, gated route) lives in examples/host_app.py.


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 · check · secrets · init · version)
│   ├── _version.py      single source of truth for the version
│   ├── templates/       demo UI
│   └── static/          styles, client logic, logo
├── examples/            runnable host app showing the consume side
├── 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.1.0.tar.gz (36.3 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.1.0-py3-none-any.whl (38.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: gated_access-1.1.0.tar.gz
  • Upload date:
  • Size: 36.3 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.1.0.tar.gz
Algorithm Hash digest
SHA256 f72ed82811c755ceefda8e1e80fcd297993bab5c9f9f465a06098a7cb57b1aa9
MD5 d1f58f8f80f56019576739a1654a9df9
BLAKE2b-256 54cde0cb6a653820d73379da354f8414ebfb627a5e42ecc9f3b584936703f94b

See more details on using hashes here.

File details

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

File metadata

  • Download URL: gated_access-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 38.6 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f8785535caf7fa7722e7703aa744ee4b8f83692288bbc8ff96c726e5f5af766f
MD5 c02e4106d5be67d948a067a14b210e89
BLAKE2b-256 d5b31ee1cfd8a330b13a1c489149a287a217d2b65271bb9babd2fcd21d19740f

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