Eb Frappe Jwt

Drop-in JWT authentication for Frappe v16 — Bearer tokens on every endpoint via auth_hooks, with refresh rotation and revocation

Install Eb Frappe Jwt

bench get-app https://github.com/eBasics-Services-P-Limited/eb_frappe_jwt

Tags

  • authentication
  • frappe
  • frappe-app
  • jwt

Add the Frappe Gems badge to your README

Maintain Eb Frappe Jwt? Paste this into your README:

[![Listed on Frappe Gems](https://frappegems.com/api/method/frappe_gems.seo.badge?app=eBasics-Services-P-Limited%2Feb_frappe_jwt)](https://frappegems.com/gems/apps/eBasics-Services-P-Limited/eb_frappe_jwt)

About Eb Frappe Jwt

EB Frappe JWT

Drop-in JWT authentication for any Frappe v16 site.

Install the app, set one config value, and every REST endpoint on your site accepts Authorization: Bearer — no changes to your own apps required. Built for mobile apps, SPAs, and server-to-server integrations that need stateless token auth instead of session cookies.

Features

  • 🔑 Access + refresh token pair — short-lived JWT access tokens (1 hour) + long-lived opaque refresh tokens (30 days)
  • 🔄 Refresh token rotation — every refresh token is single-use; reuse is rejected
  • 🚫 Revocation — log out one device or all devices at once
  • 🔌 Zero-touch integration — uses Frappe's official auth_hooks pipeline; works alongside session, OAuth, and API-key auth without interfering
  • 🧩 Custom claims hook — any app on the site can inject its own JWT claims
  • 🧹 Self-cleaning — daily scheduled job deletes expired and revoked tokens
  • 🔒 Hashed at rest — refresh tokens are stored as SHA-256 hashes, never raw
  • 📱 Device tracking — optional device label per token, so users can see/revoke individual sessions

Requirements

  • Frappe Framework v16 (Python ≥ 3.14)
  • PyJWT ~2.10 (installed automatically by bench)

Installation

cd $PATH_TO_YOUR_BENCH
bench get-app https://github.com/eBasics-Services-P-Limited/eb_frappe_jwt
bench --site your-site.local install-app eb_frappe_jwt

Set a signing secret (per site, never commit it):

bench --site your-site.local set-config eb_jwt_secret "$(openssl rand -base64 32)"

That's it. All endpoints on the site now accept Bearer JWTs.

API Reference

All endpoints are standard Frappe whitelisted methods under /api/method/.

1. Get a token pair (login)

POST /api/method/eb_frappe_jwt.api.get_token
Field Type Required Description
usr string yes Frappe username / email
pwd string yes Password
device string no Device label, e.g. "iPhone 15"
curl -X POST https://your-site.com/api/method/eb_frappe_jwt.api.get_token \
  -H "Content-Type: application/json" \
  -d '{"usr": "user@example.com", "pwd": "secret", "device": "iPhone 15"}'
{
  "message": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "kV3x9q...",
    "token_type": "Bearer",
    "expires_in": 3600
  }
}

2. Use the access token

Send it on any Frappe endpoint — REST resources, whitelisted methods, file uploads:

curl https://your-site.com/api/method/frappe.auth.get_logged_user \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

curl https://your-site.com/api/resource/ToDo \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

The request runs as the token's user with their normal roles and permissions.

3. Refresh (rotate) the token pair

POST /api/method/eb_frappe_jwt.api.refresh_token
Field Type Required Description
refresh_token string yes The refresh token you hold
device string no Device label for the new pair
curl -X POST https://your-site.com/api/method/eb_frappe_jwt.api.refresh_token \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "kV3x9q..."}'

Returns a new access + refresh pair. The old refresh token is revoked immediately (rotation) — store the new one.

4. Log out (revoke one token)

POST /api/method/eb_frappe_jwt.api.revoke_token
curl -X POST https://your-site.com/api/method/eb_frappe_jwt.api.revoke_token \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "kV3x9q..."}'

5. Log out everywhere (revoke all tokens)

POST /api/method/eb_frappe_jwt.api.revoke_all_tokens

Revokes every refresh token of the currently authenticated user.

> Note: Access tokens stay valid until they expire (max 1 hour) — revocation stops new tokens from being issued. Keep access token expiry short for this reason.

JWT Claims

The access token payload:

{
  "sub": "user@example.com",
  "roles": ["System Manager", "..."],
  "iat": 1717500000,
  "exp": 1717503600,
  "jti": "f3a9c1d24be07a16"
}

Custom claims from your own app

Any app on the site can inject extra claims by defining a hook — no changes to ebfrappejwt needed.

In your app's hooks.py:

jwt_payload_hook = "my_app.auth.get_jwt_payload"

In my_app/auth.py:

def get_jwt_payload(user):
    return {
        "tenant": "acme",
        "employee_id": frappe.db.get_value("Employee", {"user_id": user}, "name"),
    }

The returned dict is merged into every access token issued on the site.

Configuration

All settings live in site_config.json — tune each site independently, no code changes:

Key Default Description
eb_jwt_secret — (required) HS256 signing secret
eb_jwt_access_expiry 3600 Access token lifetime, in seconds
eb_jwt_refresh_expiry_days 30 Refresh token lifetime, in days
# e.g. a stricter site: 15-minute access tokens, 7-day refresh tokens
bench --site your-site.local set-config eb_jwt_access_expiry 900
bench --site your-site.local set-config eb_jwt_refresh_expiry_days 7

> Frappe caches site_config.json in web workers for up to 60 seconds — config changes apply within a minute, or immediately after bench restart.

How It Works

Client                        Frappe + eb_frappe_jwt
  │  POST get_token (usr/pwd)   │
  │ ───────────────────────────▶│  check_password → sign HS256 JWT
  │ ◀─────────────────────────── │  + opaque refresh token (SHA-256 hash stored
  │   access + refresh pair      │    in "EB JWT Token" doctype)
  │                              │
  │  GET /api/... + Bearer JWT   │
  │ ───────────────────────────▶│  frappe.auth.validate_auth()
  │                              │    └─ auth_hooks → validate_jwt_auth()
  │                              │         verify signature + expiry
  │ ◀─────────────────────────── │         frappe.set_user(sub) → normal request
  • Registered via Frappe's auth_hooks — the official extension point in frappe.auth.validate_auth(). No core overrides, no monkey-patching, upgrade-safe.
  • Runs after core OAuth and API-key checks, and only acts if the user isn't already authenticated — so it coexists with every other auth method.
  • Refresh tokens are 256-bit secrets.token_urlsafe values; only their SHA-256 hash touches the database.
  • A daily scheduler job (cleanup_expired_tokens) purges expired and revoked rows.

Testing

From your bench directory:

env/bin/python apps/eb_frappe_jwt/run_jwt_tests.py your-site.local

The runner creates a temporary user (jwt-test@example.com), exercises the full token lifecycle (login, claims, rotation, single-use enforcement, revocation, device tracking), and deletes the user afterwards.

── EB Frappe JWT Tests ─────────────────────────────────
  PASS  get_token returns access + refresh tokens
  PASS  access token is valid JWT with correct claims
  PASS  wrong password is rejected
  PASS  unknown user is rejected
  PASS  refresh_token issues new token pair
  PASS  refresh token is single-use (rotation)
  PASS  revoke_token invalidates refresh token
  PASS  invalid refresh token is rejected
  PASS  device field is stored on token record

── 9 passed · 0 failed ───────────────────────────

Security Notes

  • Always use HTTPS — Bearer tokens are credentials.
  • Keep eb_jwt_secret out of version control; it lives in site_config.json per site.
  • Rotating the secret invalidates all outstanding access tokens immediately (refresh tokens still work — they're opaque, not JWTs).
  • Disabled users are rejected at login, refresh, and on every authenticated request.
  • Failed JWT validation falls through to Frappe's standard rejection (401) — invalid tokens never silently become Guest sessions on protected endpoints.

Uninstalling

bench --site your-site.local uninstall-app eb_frappe_jwt

Clean removal: the auth hook, doctype, table, and scheduled job all go with the app. Session, OAuth, and API-key auth are untouched. Optionally remove eb_jwt_secret from site_config.json.

Contributing

This app uses pre-commit for code formatting and linting. Please install pre-commit and enable it for this repository:

cd apps/eb_frappe_jwt
pre-commit install

Pre-commit is configured to use the following tools for checking and formatting your code:

  • ruff
  • eslint
  • prettier
  • pyupgrade

License

MIT © 2026 eBasics Services P Limited

Related Integrations apps for Frappe & ERPNext

  • Insights — Open Source Business Intelligence Tool
  • Raven — Simple, open source team messaging platform
  • Frappe Whatsapp — WhatsApp cloud integration for frappe
  • Frappe Assistant Core — Infrastructure that connects LLMs to ERPNext. Frappe Assistant Core works with the Model Context Protocol (MCP) to expose ERPNext functionality to any compatible Language Model
  • Biometric Attendance Sync Tool — A simple tool for syncing Biometric Attendance data with your ERPNext server
  • Frappe React Sdk — React hooks for Frappe
  • Frappe Js Sdk — TypeScript/JavaScript library for Frappe REST API
  • Mcp — Frappe MCP allows Frappe apps to function as MCP servers