django-offline-auth
Reusable Django package for securely issuing offline authentication credentials to mobile applications.
The Django side is intentionally user-centric and device-agnostic.
It does not store device IDs, IMEI numbers, device ownership, Android private keys, or extra copies of user password verifiers.
The host application decides who can authenticate offline. django-offline-auth handles how their credentials are securely issued.
Architecture
Host Django Application
│
│ selects eligible users
▼
django-offline-auth
│
│ encrypts offline credentials
▼
Android Application
│
│ protects private key with Android Keystore
▼
Offline Authentication
Installation
pip install django-offline-auth
Add it to Django:
INSTALLED_APPS = [
# ...
"django_offline_auth",
]
Run migrations:
python manage.py migrate
Public API
from django_offline_auth.application import (
disable_offline_auth,
enable_offline_auth,
is_enabled_offline_auth,
issue_offline_credentials,
rotate_offline_credentials,
)
Enable offline authentication:
enable_offline_auth(user)
Check whether it is enabled:
if is_enabled_offline_auth(user):
print("Offline authentication is enabled")
Disable it:
disable_offline_auth(user)
Rotate the user's offline credential state:
rotate_offline_credentials(user)
credential_version is incremented when credentials are explicitly rotated or when offline authentication is disabled and later re-enabled.
Issuing Offline Credentials
The host application decides which users should be available offline:
snapshot = issue_offline_credentials(
users=eligible_users,
public_key=android_public_key,
)
The package:
- checks that each user is active and enabled for offline authentication;
- validates the user's Django password verifier;
- builds an offline credential payload;
- encrypts the payload with AES-256-GCM;
- wraps the AES key using the Android RSA public key;
- returns an encrypted synchronization snapshot.
Synchronization Request
The mobile application sends its RSA public key:
{
"public_key": "MIIBIjANBgkqh..."
}
The host application must decide which users are eligible. Clients should never be allowed to submit arbitrary user IDs to determine who receives offline credentials.
Synchronization Response
Example:
{
"protocol_version": 1,
"credentials": [
{
"user_id": "1",
"username": "mary",
"envelope": {
"key_algorithm": "RSA-OAEP-256",
"content_algorithm": "A256GCM",
"encrypted_key": "...",
"nonce": "...",
"ciphertext": "..."
}
}
],
"failures": [
{
"user_id": "2",
"username": "john",
"reason": "unusable_password"
}
]
}
A problem with one user does not have to fail the entire synchronization batch.
Possible user-level failures include:
unusable_password
unsupported_password_algorithm
invalid_password_verifier
Offline Flow
User enters username + password
↓
Android finds that user's encrypted credential
↓
Android Keystore private key unwraps the AES key
↓
AES-GCM decrypts the credential payload
↓
PBKDF2 verifies the password
↓
Success / Failure
A single Android installation may cache credentials for multiple users.
The client should look up the entered username first and verify only that user's credential.
Cryptography
Protocol version 1 uses:
Password verifier:
pbkdf2_sha256
Content encryption:
AES-256-GCM
AES key wrapping:
RSA-OAEP-SHA256
Private key storage:
Android Keystore
The Android private key must never leave Android Keystore.
The public key may be sent to Django during synchronization and does not need to be stored by the backend.
Credential Policy
Offline credentials:
- use the user's current Django password verifier;
- are encrypted before leaving Django;
- are never returned as plaintext API data;
- expire after a configurable period;
- use a default lifetime of 24 hours;
- are refreshed during successful synchronization.
If a password changes while a device is offline, the previous cached credential may remain valid until the next synchronization or credential expiry.
Authoritative Snapshot
The synchronization result is treated as the authoritative set of offline credentials.
If Android previously has:
Mary
John
Peter
and the latest successful synchronization returns only:
Mary
Android should keep/update Mary and remove John and Peter.
Configuration
Default credential lifetime:
DJANGO_OFFLINE_AUTH_CREDENTIAL_TTL_SECONDS = 24 * 60 * 60
Minimum RSA key size:
DJANGO_OFFLINE_AUTH_MIN_RSA_KEY_SIZE = 2048
An individual request may override the default TTL:
from datetime import timedelta
snapshot = issue_offline_credentials(
users=eligible_users,
public_key=public_key,
ttl=timedelta(hours=6),
)
Security Rules
- Never include Django password hashes in JWTs.
- Never expose password verifiers as plaintext API data.
- Never log passwords, verifiers, AES keys, or private keys.
- Never persist another plaintext copy of
user.password. - Never store Android private keys outside Android Keystore.
- Always validate supplied public keys.
- Always use HTTPS.
- Use a fresh AES key and nonce for every credential.
- Enforce credential expiry on the mobile client.
- Keep decrypted verifier material inside native Android code.
Application-level encryption does not replace TLS.
Responsibilities
Host Django Application
Responsible for:
authentication
authorization
business rules
selecting eligible users
API integration
django-offline-auth
Responsible for:
OfflineAuth state
credential generation
credential versioning
expiry
password verifier validation
AES-GCM encryption
RSA-OAEP key wrapping
synchronization response
Android Application
Responsible for:
Android Keystore
private-key protection
encrypted local storage
credential decryption
offline password verification
expiry enforcement
attempt limiting
authoritative snapshot replacement
Protocol Version
Current protocol:
protocol_version = 1
Protocol v1 supports:
Password algorithm: pbkdf2_sha256
Key wrapping: RSA-OAEP-256
Encryption: A256GCM
Breaking changes to the wire format or cryptographic contract should increment protocol_version.
Metadata
Release files for django-offline-auth 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_offline_auth-0.1.2.tar.gz | 14.7 kB | Without provenance | Details for django_offline_auth-0.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_offline_auth-0.1.2-py3-none-any.whl | Python 3 | none | any | Details for django_offline_auth-0.1.2-py3-none-any.whl |
Total release size: 31.9 kB
Release files / django_offline_auth-0.1.2.tar.gz
| Download URL | django_offline_auth-0.1.2.tar.gz |
|---|---|
| Size | 14.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8f5c2565b0198aec35321bb001749afd73d075e92eef2b468394f0bbdf52cc47
|
|
BLAKE2b-256 checksum How to use checksums |
bded3fb97afe26c7a20885ce552cd5bb928bc6c5fe843ee17b9cfb3a8e63d56b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / django_offline_auth-0.1.2-py3-none-any.whl
| Download URL | django_offline_auth-0.1.2-py3-none-any.whl |
|---|---|
| Size | 17.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1eb12d3ca987a205396be22061002f4e32ac0d05b9b42ee50a423a35274c731c
|
|
BLAKE2b-256 checksum How to use checksums |
a12fbcfacf5bbacd520e5c83fdd337ab7636ffbef0da127242b2d254e3201ee1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for django-offline-auth 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.