REST API Advanced

JWT and OAuth2 with Django: SimpleJWT, Refresh Rotation, Blacklisting, and Social Auth Done Right

Stop hand-rolling token auth. Production-grade JWT for Django REST Framework: access/refresh tokens with rotation, blacklist-on-logout, secure storage (httpOnly cookies vs localStorage), and OAuth2 social auth with allauth.

DjangoZen Team Apr 25, 2026 20 min read 137 views

Token authentication sounds simple until you face the real questions: where do you store a token safely in a browser, how do you revoke one before it expires, what stops a stolen refresh token from living forever, and how does "Sign in with Google" actually work underneath? Getting these wrong produces auth that is either insecure or infuriating. This tutorial covers JWT and OAuth2 in Django the way production demands — SimpleJWT, refresh rotation, blacklisting, and social auth done right.

What a JWT actually is

A JSON Web Token is three base64 parts — a header, a payload of claims, and a signature — joined by dots. The payload carries data like the user ID and an expiry; the signature, computed with a secret only the server holds, proves the token was issued by you and has not been altered. Crucially, a JWT is signed, not encrypted: anyone can read the payload, so it must never contain secrets. The signature is what matters — it lets the server verify a token without a database lookup, because the math proves authenticity. Understanding that a JWT is a tamper-evident, self-contained claim, readable by anyone but forgeable by no one, is the foundation for everything that follows.

Stateless authentication and its tradeoff

The appeal of JWTs is statelessness: because the signature proves validity, the server can authenticate a request by verifying the token alone, with no session store to consult. This scales beautifully across many servers — there is no shared session database to coordinate. But statelessness has a sharp cost that defines the rest of this topic: if the server does not track tokens, it cannot easily revoke one. A valid signed token is valid until it expires, even if the user logged out or was compromised. Every production JWT decision flows from managing this single tension between stateless convenience and the need to revoke.

SimpleJWT in Django

The standard library for JWT in Django REST Framework is djangorestframework-simplejwt, which provides token issuance, verification, and the refresh machinery out of the box:

pip install djangorestframework-simplejwt
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
}
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
}

Those settings already encode the most important production choices, which the next sections unpack.

The access/refresh token pair

The core pattern is two tokens with very different lifetimes. The access token is short-lived — fifteen minutes is typical — and is sent with every request; because it expires quickly, a stolen access token is dangerous only briefly. The refresh token is long-lived and used solely to obtain new access tokens when they expire, so the user is not forced to log in every fifteen minutes. This split is the heart of practical JWT auth: short access tokens limit the damage of theft, while the refresh token preserves a long, convenient session. The security of the whole scheme then hinges on protecting the refresh token, since it is the powerful, long-lived credential.

Refresh token rotation

A long-lived refresh token is a juicy target, and rotation limits its risk. With rotation on, every time a refresh token is used to get a new access token, the refresh token itself is replaced with a fresh one and the old one is invalidated. This means a given refresh token works exactly once. If an attacker steals a refresh token and uses it, the legitimate user's next refresh fails (their token was already rotated away), surfacing the compromise; conversely, if the user refreshes first, the attacker's stolen token is now dead. Rotation transforms a refresh token from a long-lived skeleton key into a single-use credential, dramatically shrinking the window of a leak.

Blacklisting and revocation

Rotation handles refresh tokens, but you still need to revoke on logout or compromise, which reintroduces state. SimpleJWT's blacklist app stores invalidated refresh tokens so they are rejected even though their signature is still valid. This is the pragmatic compromise: the access token stays stateless and short, while refresh tokens are checked against a blacklist, giving you revocation where it matters most. On logout you blacklist the user's refresh token; on a suspected compromise you blacklist all of theirs. Yes, this means a database lookup on refresh — but only on refresh, not on every request — which keeps the common path fast while restoring the ability to revoke.

Where to store tokens in the browser

This is where many implementations quietly become insecure. Storing tokens in localStorage is convenient but exposes them to any cross-site scripting flaw — a single XSS and the attacker reads the token and impersonates the user. The safer pattern is to keep the refresh token in an httpOnly, Secure, SameSite cookie that JavaScript cannot read, so XSS cannot steal it, while the short-lived access token is held in memory. This trades some implementation complexity for a meaningful security gain. Token storage is not an afterthought — it is the difference between auth that resists XSS and auth that hands an attacker the keys at the first script injection.

Choosing token lifetimes

Token lifetimes are a security-versus-convenience dial. Shorter access tokens mean a stolen one is useful for less time, but more frequent refreshes; longer ones are convenient but raise the cost of theft. Fifteen minutes for access and a few days to a couple of weeks for refresh is a common, sensible balance. High-security applications shorten both and may require re-authentication for sensitive actions; consumer apps lean longer for convenience. The right answer depends on your threat model and how much friction your users tolerate, but the reasoning is always the same: every minute of lifetime is a minute a stolen token remains dangerous, weighed against the annoyance of logging in again.

Custom claims and authorization

Because a JWT carries a payload, you can embed claims beyond the user ID — roles, permissions, a tenant ID — that the server reads without a database lookup. This is convenient for authorization, but it has a catch tied to statelessness: claims are baked in at issuance and do not update until the token is reissued. If you revoke a user's admin role, their existing token still claims it until it expires. So put slowly-changing, low-risk data in claims, and check anything sensitive or fast-changing against the database. Embedding claims is a useful optimization, not a license to trust a token's assertions about authority that may have changed since it was signed.

OAuth2: delegated authorization

OAuth2 is often confused with authentication, but it is fundamentally about delegated authorization — letting a user grant your app limited access to their data on another service without sharing their password. The user is redirected to the provider, approves specific scopes, and your app receives a token it can use to act on their behalf within those scopes. This is the machinery behind "connect your Google Calendar" or "access your GitHub repos." Understanding OAuth2 as a permission-granting protocol, not a login mechanism, clears up most of the confusion — the login use case ("Sign in with Google") is a specific application of it, layered on top with an identity component.

The authorization code flow

The standard, secure OAuth2 flow for web apps is the authorization code flow. The user is redirected to the provider with your client ID and requested scopes; they authenticate and consent; the provider redirects back to your app with a short-lived authorization code; and your server exchanges that code, plus its client secret, for an access token in a back-channel request the browser never sees. This indirection matters: the powerful token is obtained server-to-server, never exposed in the browser or URL. For public clients, PKCE adds a proof step that prevents code interception. Following this flow correctly is what keeps an OAuth2 integration secure rather than leaky.

Social login done right

"Sign in with Google" is OAuth2 plus identity, and libraries like django-allauth handle the heavy lifting. The flow authenticates the user with the provider and returns verified profile information, which you map to a local user account. The critical detail done right is account linking: decide deliberately what happens when someone signs in with Google using an email that already has a password account — link them after verification, or you create duplicate accounts and confusion. Always verify the email is confirmed by the provider before trusting it for linking. Social login is convenient and reduces password risk, but the account-matching logic is where correctness and security live.

When JWTs are the wrong choice

JWTs are fashionable, but for many apps Django's built-in session authentication is simpler and better. If you have a single backend serving a browser front-end on the same domain, sessions give you easy revocation (delete the session), no token-storage headaches, and battle-tested security with far less code. JWTs earn their complexity when you genuinely need statelessness across many services, mobile or third-party clients, or cross-domain APIs. Reaching for JWTs by default — and then bolting on blacklists and rotation to recover the revocation that sessions give for free — is a common mistake. Choose JWTs for the problems they actually solve, and use sessions when they fit.

Common pitfalls

A handful of mistakes recur. Storing tokens in localStorage and exposing them to XSS. Making access tokens long-lived, so theft is catastrophic. Skipping rotation and blacklisting, leaving no path to revoke. Putting sensitive data in the readable payload. Trusting stale claims for authorization. Forgetting that logout must actually invalidate the refresh token, not just drop it client-side. Each of these turns a reasonable-looking implementation into an insecure one, and each is avoided by understanding the stateless tradeoff at the core of JWTs. The patterns in this tutorial exist precisely to close these gaps.

Signing algorithms: symmetric versus asymmetric

How a JWT is signed matters for multi-service architectures. Symmetric signing (HMAC) uses one shared secret to both sign and verify, which is simple but means every service that verifies tokens must hold the signing secret — and any of them could forge tokens. Asymmetric signing (RSA or EC) signs with a private key and verifies with a public one, so the issuer alone can mint tokens while many services verify them with the freely-distributable public key. For a single backend, symmetric is fine; for multiple services verifying tokens, asymmetric is the safer design because verifiers cannot forge. Choosing the right algorithm for your topology is a security decision, not just a configuration detail.

Token validation pitfalls

Verifying a JWT correctly means more than checking the signature. You must validate the expiry so old tokens are rejected, the issuer and audience claims so a token minted for another service or purpose is not accepted, and the algorithm itself — a notorious class of vulnerability involves tricking a verifier into accepting a token signed with a weaker or no algorithm. Libraries like SimpleJWT handle these correctly by default, which is a strong reason to use them rather than hand-rolling verification. The lesson is that JWT security lives in rigorous validation, and the safe path is trusting a well-maintained library to check everything rather than verifying signatures yourself and missing a claim.

The logout problem in depth

Logout exposes the core tension of stateless tokens. Truly logging a user out means their tokens stop working immediately, but a stateless access token remains valid until it expires no matter what the client does. The practical answer is layered: keep access tokens short so the window is small, blacklist the refresh token on logout so no new access tokens can be minted, and for high-security needs maintain a server-side check that can invalidate access tokens too — accepting that this reintroduces the state JWTs sought to avoid. Logout is where teams confront that stateless and revocable are in tension, and the right balance depends on how quickly you truly need a logout to take effect.

Tokens for mobile and third-party clients

JWTs genuinely shine for clients that are not a same-origin browser: mobile apps and third-party API consumers, where cookie-based sessions are awkward. For these, the token storage rules differ — a mobile app uses the platform's secure storage rather than a browser cookie — but the principles hold: short access tokens, secure storage of the refresh token, and rotation. Third-party API access often layers OAuth2 on top so external developers obtain scoped tokens to act on users' behalf. Recognizing that JWTs are most clearly the right tool for these non-browser clients — and that browser apps often do better with sessions — helps you apply them where their statelessness is a genuine advantage rather than a self-inflicted complication.

Summary

A JWT is a signed, readable, self-contained claim that the server can verify without a lookup — which gives you stateless authentication and, as its central tradeoff, makes revocation hard. Manage that tradeoff with the production patterns SimpleJWT provides: short access tokens and long refresh tokens, refresh rotation so a refresh token is single-use, and blacklisting so logout and compromise can actually revoke. Store the refresh token in an httpOnly cookie so XSS cannot steal it, keep sensitive authorization checks against the database rather than trusting stale claims, and choose token lifetimes by your threat model. Understand OAuth2 as delegated authorization built on the authorization code flow, implement social login with careful account linking and verified emails, and — importantly — reach for sessions instead when statelessness is not something you actually need. Done right, token auth is secure and convenient; done by cargo-cult, it is neither.