Eb Frappe Jwt
Drop-in JWT authentication for Frappe v16 — Bearer tokens on every endpoint via auth_hooks, with refresh rotation and revocation
- Author: eBasics-Services-P-Limited
- Repository: https://github.com/eBasics-Services-P-Limited/eb_frappe_jwt
- GitHub stars: 0
- Forks: 0
- License: MIT
- Category: Integrations
- Maintenance: Actively Maintained
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:
[](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_hookspipeline; 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 infrappe.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_urlsafevalues; 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_secretout of version control; it lives insite_config.jsonper 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