mibudge

Authentication

mibudge supports two ways to authenticate against the REST API:

Method Credential Intended for
Password + JWT Authorization: Bearer <access> Interactive user agents: the SPA, mobile and desktop apps
API key Authorization: Api-Key <key> Machine clients: transaction importers, 3rd-party services

OAuth2 for registered third-party apps (hosted import services, MCP servers) is planned as the next phase of machine authentication.


Interactive sessions: the JWT two-token pattern

Passwordless accounts

Users created via the invitation flows (bank-account co-ownership or admin user invitations – see invitations.md) start with no password set (has_usable_password() == False). Accepting an invitation activates the account and sends an allauth password-reset email so the user sets their first password via /accounts/password/reset/; acceptance never issues credentials directly. While an account has no usable password, the change-password and change-email endpoints refuse to operate. The SPA detects this state via the has_usable_password field on GET /api/v1/users/me/ and prompts the user accordingly.

Self-service email change

Users can change their login email address via a verified two-step flow. A 7-day post-confirmation revocation window lets the legitimate owner cancel even if an attacker confirmed the change first, with automatic session invalidation on revocation. See email-change.md for the full flow and security policy.


Machine credentials: API keys

API keys let 3rd-party services (and the importers in importers/) call the REST API on a user’s behalf without holding the user’s password.

Creating and managing keys

Keys are managed at /api/v1/users/me/api-keys/ (or in the SPA under Settings -> API keys):

Endpoint Action
POST /api/v1/users/me/api-keys/ Create a key (name, optional expiry_days)
GET /api/v1/users/me/api-keys/ List your keys (active, expired, and revoked)
GET /api/v1/users/me/api-keys/{uuid}/ Retrieve one key
POST /api/v1/users/me/api-keys/{uuid}/revoke/ Permanently revoke a key

Using a key

Send the plaintext key on every request:

Authorization: Api-Key mib_...

The importer CLIs accept --api-key, the MIBUDGE_API_KEY environment variable, a 1Password secret reference to the field holding the key (--api-key-onepassword-url / MIBUDGE_API_KEY_ONEPASSWORD_URL), or a Vault secret key api_key – see docs/importers.md.

What API keys may not do

Machine credentials get blanket access to the budgeting domain (bank accounts, budgets, transactions, allocations, funding, …) but are denied on user/security endpoints:

The one carve-out: GET /api/v1/users/me/ is allowed for machine credentials. It returns only profile facts (no security levers), and the importers need the timezone field to anchor bank-statement dates correctly. Writes to the profile remain interactive-only.

This is enforced by the users.permissions.RequiresInteractiveAuth permission – a blocklist gate attached to those views – and its read-only variant RequiresInteractiveAuthForWrites (used on /users/me/, gating only mutating methods). When fine-grained scopes are introduced, these gates become scopes (e.g. the read carve-out maps to a profile:read scope). When adding a new sensitive user/security endpoint, attach RequiresInteractiveAuth to it; use RequiresInteractiveAuthForWrites only when machine consumers genuinely need the reads.

Implementation

Piece Location
APIKey model app/users/models.py
DRF authentication app/users/authentication.py (ApiKeyAuthentication)
Interactive-auth gate app/users/permissions.py (RequiresInteractiveAuth)
Management endpoints app/users/api/v1/views.py (APIKeyViewSet)
Client support importers/client.py (MibudgeClient(api_key=...))

NOTE: the stored digest is an unsalted SHA-256 of the plaintext – safe because the secret is a high-entropy random token (not a low-entropy password), and a deterministic hash allows an indexed O(1) lookup on every authenticated request.