Django user impersonation for support: django-hijack and what to add

How django-hijack works: who can hijack by default, permission hooks, the notification bar, release and signals, plus what a support workflow still needs.

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_history list in the session, and the customer’s last_login isn’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-hijackdjango-impersonate
Who can impersonate by defaultSuperusersAny staff user, unless REQUIRE_SUPERUSER is set
Impersonating superusersAllowed for superusersBlocked unless ALLOW_SUPERUSER is set
How it startsPOST with CSRF tokenVisit /impersonate/<user-id>/
Read-only modeNoneREAD_ONLY allows only GET, HEAD and OPTIONS
Time limitNormal session lifetimeOptional MAX_DURATION in seconds
Built-in logSignals onlyImpersonationLog records by default
Limit target usersCustom permission functionCUSTOM_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?”.

Requirementdjango-hijackWhat you add
Who may startPermission functionEligibility or assignment checks per customer
ApprovalNoneA request that someone else approves first
ReasonNoneA reason field stored with the session
ScopeFull access as the customerRead-only by default, enforced in the backend
ExpiryNormal session lifetimeA time limit per approved session
Ending it remotelyRelease button onlyServer-side revocation
AuditSignalsA 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.

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.

ScenarioPass condition
Support user without the permission tries to startRequest refused; no session change
Support user targets a staff or superuser accountRefused
Write request during read-only accessRejected by the backend; data unchanged
JSON API call while impersonatingYour 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 closedNext 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

Quick answers

Common questions, answered clearly.

Direct answers to the questions teams usually ask when evaluating this approach.

What is django-hijack?

django-hijack is a Django package that lets an authorized user log in as another user without knowing their password. It adds acquire and release views, a notification bar while you are working as someone else, a Django admin button, and signals when a hijack starts or ends.

Who can use django-hijack by default?

Only superusers. The default permission check lets a superuser hijack any active user. You can switch to a built-in check that also lets staff hijack non-staff users, or point the HIJACK_PERMISSION_CHECK setting at your own function.

Does django-hijack keep an audit log?

No. The package has no models, so it stores nothing beyond a list of original user IDs in the session. It sends hijack_started and hijack_ended signals, and you connect a receiver that writes your own log. A hijack that ends because the session expires does not send hijack_ended.

Can django-hijack be read-only?

Not by itself. django-hijack has no read-only setting, so a hijacked session can do anything the customer can. You would add your own middleware or backend checks using request.user.is_hijacked. The separate django-impersonate package has a READ_ONLY setting that only allows GET, HEAD and OPTIONS requests.

Does Devora integrate with django-hijack?

No. Devora's Django packages are a separate route. They mount Devora's signed endpoints and add a scope guard middleware, while your own code creates the customer session after an approved request. You can keep django-hijack for developer use, but the two don't share sessions or settings.

Put it into practice

Give your team context.Keep control of access.

Bring approvals, scoped sessions, and access history into your support workflow.