Django Hijack (django-hijack) is a widely used package for adding user impersonation to a Django app. As the django-hijack docs (opens in a new tab) describe, it lets an authorized user, by default a superuser, log in as another user without their password, shows a notification bar while they do, and gives them a “stop impersonating” button. For a small team that trusts its superusers, it may be all you need.
What django-hijack doesn’t decide is whether a particular support session should happen, what the staff member may do inside it, or when it ends. This guide covers how the package works, how to configure it for support, and what to add when impersonation becomes a support process rather than a developer tool.
What does django-hijack do?
django-hijack is a small Django app with two views: one to start a hijack and one to release it. You add hijack to INSTALLED_APPS, add HijackUserMiddleware, and include its URLs. To start, you POST a form with a CSRF token and the target user’s primary key. Adding hijack.contrib.admin puts a hijack button in the Django admin’s user list (opens in a new tab).
A few design choices, set out in the django-hijack security policy (opens in a new tab), are worth knowing:
- POST only. Both views use Django’s CSRF protection, which is why there are no GET links.
- A fresh session each time. It uses Django’s
login(), which flushes the session when you hijack and when you release, so session data doesn’t leak between users. - Your identity stays in the session. In django-hijack’s views code (opens in a new tab), the original user’s primary key is pushed onto a
hijack_historylist in the session, and the customer’slast_loginisn’t updated. - No models. The package source (opens in a new tab) defines no models, so it stores nothing in your database.
Who can hijack a user by default?
Only superusers. In django-hijack’s conf.py (opens in a new tab), the default HIJACK_PERMISSION_CHECK is hijack.permissions.superusers_only, which lets a superuser hijack any active user. The built-in alternative check (opens in a new tab), superusers_and_staff, also lets staff members hijack users who are neither staff nor superusers. Neither built-in check allows hijacking inactive users.
Making support agents superusers just so they can impersonate gives them far more than they need. A custom permission function is the better route. It takes hijacker and hijacked as keyword arguments and returns a boolean:
# support/permissions.py
def support_can_hijack(*, hijacker, hijacked):
if not hijacked or not hijacked.is_active:
return False
if hijacked.is_staff or hijacked.is_superuser:
return False # support never impersonates staff
return hijacker.has_perm("support.hijack_customer")
# settings.py
HIJACK_PERMISSION_CHECK = "support.permissions.support_can_hijack"
support.hijack_customer is a custom permission you declare on one of your models. The same function also controls the can_hijack template filter. The django-hijack docs warn (opens in a new tab) that writing custom permission checks is “highly dangerous”, so test every combination of staff, superuser, inactive and customer accounts.
If support should only reach certain customers, add that check here too. The assigned-customer guide shows the pattern.
What does the staff member see, and how do they stop?
While hijacked, the middleware injects a notification before </body> that names the user and offers “stop impersonating”, which POSTs to the release view. You can override the hijack/notification.html template. Setting HIJACK_INSERT_BEFORE to None removes it. The docs say to do that only if you’ve built an equivalent notice into your own layout.
In django-hijack’s middleware (opens in a new tab), the injection only happens on HTML responses that aren’t streamed or gzip-compressed. If your front end is a single-page app reading a JSON API, the bar won’t appear. Expose request.user.is_hijacked through your API and render your own banner.
There is no separate time limit. According to its security policy (opens in a new tab), django-hijack carries the original session’s expiry over to the hijacked session and doesn’t extend it. With Django’s default SESSION_COOKIE_AGE (opens in a new tab) of two weeks, a forgotten hijack can last as long as any normal login.
How do you log django-hijack sessions?
Connect receivers to the hijack_started and hijack_ended signals. Each one receives the hijacker, the hijacked user and the request.
# support/signals.py (import this module from your AppConfig.ready())
from hijack import signals
from .models import SupportAccessEvent
def record_start(sender, hijacker, hijacked, request, **kwargs):
SupportAccessEvent.objects.create(event="started", staff=hijacker, customer=hijacked)
def record_end(sender, hijacker, hijacked, request, **kwargs):
SupportAccessEvent.objects.create(event="ended", staff=hijacker, customer=hijacked)
signals.hijack_started.connect(record_start)
signals.hijack_ended.connect(record_end)
Two gaps remain. hijack_ended is only sent from the release view (opens in a new tab), so a hijack that ends because the session expired leaves no end event. And the start form only carries the user ID and redirect, so there’s nowhere to record a reason without a custom view. For the events in between, write both identities into your business audit events, reading the staff ID from hijack_history. User impersonation audit logs covers what to capture.
django-hijack vs django-impersonate
django-impersonate is the other common Django package. According to its documentation on PyPI (opens in a new tab), it makes different default choices:
| django-hijack | django-impersonate | |
|---|---|---|
| Who can impersonate by default | Superusers | Any staff user, unless REQUIRE_SUPERUSER is set |
| Impersonating superusers | Allowed for superusers | Blocked unless ALLOW_SUPERUSER is set |
| How it starts | POST with CSRF token | Visit /impersonate/<user-id>/ |
| Read-only mode | None | READ_ONLY allows only GET, HEAD and OPTIONS |
| Time limit | Normal session lifetime | Optional MAX_DURATION in seconds |
| Built-in log | Signals only | ImpersonationLog records by default |
| Limit target users | Custom permission function | CUSTOM_USER_QUERYSET |
Choose django-hijack if you want a small, security-focused package and plan to build logging yourself. Choose django-impersonate if its read-only mode, duration cap and built-in log match what you need.
What does a support workflow still need?
Both packages answer “can this person log in as that user?”. A support process also needs answers to “should they, right now, and for how long?”.
| Requirement | django-hijack | What you add |
|---|---|---|
| Who may start | Permission function | Eligibility or assignment checks per customer |
| Approval | None | A request that someone else approves first |
| Reason | None | A reason field stored with the session |
| Scope | Full access as the customer | Read-only by default, enforced in the backend |
| Expiry | Normal session lifetime | A time limit per approved session |
| Ending it remotely | Release button only | Server-side revocation |
| Audit | Signals | A durable record of who, whom, why and how it ended |
Scope is the one that hurts most when missing. A hijacked session can do anything the customer can, including change passwords, billing or roles. If you stay on django-hijack, add middleware that checks request.user.is_hijacked and rejects writes and sensitive endpoints. Read-only user impersonation explains how to build that inventory.
How do Devora’s Django packages fit?
Devora is not a django-hijack add-on, and it doesn’t use django-hijack’s views or settings. It’s a separate route that covers the workflow parts in the table above. As Devora’s overview for teams (opens in a new tab) describes, a team member requests access with a scope, duration and reason, and an administrator approves it.
On the Django side you install devora-python and devora-django, as described in Devora’s Django guide (opens in a new tab). They provide signed routing, mounted with django_urlpatterns, and a native scope guard middleware. You implement three handlers and your application’s authentication.
- Start. After approval, Devora calls your
IMPERSONATEhandler with verified context. Your code returns a short-lived credential for the customer. With the custom session bridge (opens in a new tab), the browser exchanges that credential once for an HTTP-only session. Create a distinct impersonation session; it must not mark the customer’s MFA (opens in a new tab) as completed. - Scope. Put the guard middleware after your authentication middleware. It reads the stored context from your verified session, never from browser headers. Read sessions allow
GET,HEADandOPTIONSby default, and “always blocked” rules (opens in a new tab) apply to both scopes. Anon_blockedcallback in the Python scope guard (opens in a new tab) lets you audit blocked requests. - End. Devora calls your
TERMINATEhandler for every ending, making up to 5 attempts over 6 hours (opens in a new tab) if delivery fails. Record the session ID as revoked and reject credentials that carry it.
Two practical details. First, the Django setup guide (opens in a new tab) says to set SECURE_CROSS_ORIGIN_OPENER_POLICY = None (or unsafe-none on the landing route), or the impersonation link can’t be redeemed. Second, your pages need Devora’s browser SDK (opens in a new tab), which redeems the link and hands the credential to your code.
Test it in staging
Run these with synthetic accounts and a non-staff test customer. These are pass conditions to check, not results we’ve run.
| Scenario | Pass condition |
|---|---|
| Support user without the permission tries to start | Request refused; no session change |
| Support user targets a staff or superuser account | Refused |
| Write request during read-only access | Rejected by the backend; data unchanged |
| JSON API call while impersonating | Your own banner or flag shows impersonation |
| Session expires without “stop impersonating” | Next request is refused, and your log shows how it ended |
| Access ended remotely with the tab closed | Next request with the old cookie is refused |
If django-hijack plus a permission function and a signal receiver covers your needs, keep it. If you need approval, scope, time limits and access history on top, create a Devora account and try one staging session.
Sources
- django-hijack documentation, 3.7.8: Installation and usage (opens in a new tab)
- django-hijack documentation, 3.7.8: Security policy (opens in a new tab)
- django-hijack source, 3.7.8: views.py (opens in a new tab)
- django-hijack source, 3.7.8: package contents (opens in a new tab)
- django-hijack source, 3.7.8: conf.py (opens in a new tab)
- django-hijack documentation, 3.7.8: Customization (opens in a new tab)
- django-hijack source, 3.7.8: middleware.py (opens in a new tab)
- Django 5.2 documentation: SESSION_COOKIE_AGE (opens in a new tab)
- django-impersonate 1.9.5 on PyPI (opens in a new tab)
- Devora: Overview for teams (opens in a new tab)
- Devora: Django (opens in a new tab)
- Devora: Custom JWT or cookie session (opens in a new tab)
- Devora: Scope and security (opens in a new tab)
- Devora: Python reference, scope guard (opens in a new tab)
- Devora: Session lifecycle (opens in a new tab)
- Devora: Quickstart (opens in a new tab)