JWT Authentication in JavaScript and Python: Decode, Verify, and Refresh Tokens Safely
jwtauthenticationapisjavascriptpythonsecurity

JWT Authentication in JavaScript and Python: Decode, Verify, and Refresh Tokens Safely

TThe Code Website Editorial Team
2026-11-01
7 min read

Compare JWT decoding and verification, then implement safer JavaScript and Python access-token, refresh-token, storage, and expiration flows.

JWT authentication can be simple to add and difficult to secure. This guide explains how JSON Web Tokens work, compares decoding with signature verification, and provides reusable JavaScript and Python patterns for access tokens, refresh tokens, expiration handling, storage, and failure responses.

Overview

A JSON Web Token, or JWT, is a compact string commonly used to carry claims between an issuer and an application. A token normally has three Base64URL-encoded parts separated by periods: a header, a payload, and a signature.

The header identifies details such as the signing algorithm. The payload contains claims, such as a subject identifier, issuer, audience, issued-at time, or expiration time. The signature allows a server to detect whether the signed content was changed after issuance. Because the header and payload are encoded rather than encrypted by default, anyone who possesses a JWT can usually decode those two sections. Do not place passwords, private keys, payment details, or other confidential values in the payload.

This distinction is essential when using a JWT decoder. Decoding answers, “What data is inside this token?” Verification answers, “Was this token issued by a trusted party, has it been altered, and is it acceptable for this request?” A decoded token is useful for debugging or displaying non-sensitive metadata, but decoded claims must never be treated as proof of identity or authorization.

A typical architecture uses a short-lived access token for API requests and a longer-lived refresh token to obtain a replacement access token. The access token is presented to a protected endpoint, usually in an Authorization: Bearer header. When it expires, the client sends the refresh token to a dedicated refresh endpoint. The server validates the refresh token and issues a new access token, subject to the application's rotation and revocation rules.

How to compare options

There is no single JWT setup that fits every application. Compare an implementation across the following decisions rather than choosing a library only because it offers a convenient decode function.

  • Signing method: Decide whether the service uses a shared secret or an asymmetric key pair. Shared-secret signing can be straightforward for closely controlled services. Asymmetric signing can be useful when one issuer signs tokens and multiple services need to verify them without receiving the private signing key.
  • Verification behavior: The server should explicitly allow the expected algorithm, issuer, audience, and key. A library's general-purpose verify function is not a substitute for configuring those checks.
  • Token lifetime: Short access-token lifetimes reduce the useful window of a stolen token, but they require a reliable refresh flow. Longer lifetimes can reduce refresh traffic while increasing exposure if a token leaks.
  • Refresh-token management: Decide whether refresh tokens are rotated, stored server-side for revocation, associated with a device or session, and invalidated after logout or suspected compromise.
  • Client storage: Browser applications must balance JavaScript access, cross-site request risks, and deployment complexity. HttpOnly cookies reduce direct JavaScript access, while in-memory storage limits persistence. Web storage is convenient but should be evaluated carefully when an application can be affected by script injection.
  • Error behavior: Expired, malformed, unverifiable, and insufficiently authorized tokens are different conditions. Internally distinguish them for logging and monitoring, while returning a deliberately limited response to clients.

Also compare the maintenance surface. Check whether the chosen JWT package is actively maintained, supports the algorithms you actually need, exposes verification options clearly, and has tests for expiration, issuer, audience, malformed input, and key rotation. Keep dependencies in a project-specific virtual environment for Python applications; the Python virtual environments guide covers practical isolation choices.

Feature-by-feature breakdown

Decode versus verify

Use decoding for inspection, not authorization. In JavaScript, a small decoder can reveal the payload without checking its signature:

const decodePart = (part) => JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));

function decodeJwt(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Malformed JWT');
  return {
    header: decodePart(parts[0]),
    payload: decodePart(parts[1])
  };
}

This example is appropriate for local diagnostics, but its output is untrusted. For a real API, use a maintained JWT library and call its verification function with an explicit secret or public key, expected algorithm, issuer, and audience. The same rule applies in Python: a function that reads a payload is not equivalent to a function that verifies a signature.

Verifying a token in JavaScript

With a commonly used Node.js JWT library, a protected route can verify a token before using its claims. The exact import syntax depends on the project's module configuration.

import jwt from 'jsonwebtoken';

function authenticate(req, res, next) {
  const header = req.get('authorization') || '';
  const [scheme, token] = header.split(' ');

  if (scheme !== 'Bearer' || !token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const claims = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: 'https://api.example.test',
      audience: 'web-client'
    });
    req.user = claims;
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

Keep the signing secret outside source control and validate that it exists when the application starts. The Node.js environment variables guide provides a useful pattern for configuration validation. Never silently fall back to a development secret in production.

Verifying a token in Python

Python applications can use a maintained JWT package and configure the accepted algorithm and claims explicitly:

import jwt
from jwt import ExpiredSignatureError, InvalidTokenError

def verify_access_token(token, key):
    try:
        return jwt.decode(
            token,
            key,
            algorithms=['HS256'],
            issuer='https://api.example.test',
            audience='web-client',
            options={'require': ['exp', 'iat', 'sub']}
        )
    except ExpiredSignatureError as exc:
        raise ValueError('Token expired') from exc
    except InvalidTokenError as exc:
        raise ValueError('Token invalid') from exc

Use a separate key or key-management strategy for each environment. If your service uses asymmetric signing, distribute only the verification key to consumers and protect the private signing key. Plan how verification keys are selected during rotation, commonly through a key identifier in the header and a trusted key set.

Expiration and refresh flow

Expiration is a server-side decision. A client may notice an expired access token and request a refresh, but the API must verify the token's expiration claim itself. A robust refresh endpoint should validate the refresh token, check its session or record if server-side tracking is used, rotate it where appropriate, and issue a new access token. If refresh-token reuse is detected, revoke the affected session family rather than continuing to issue tokens.

Do not automatically retry every failed request forever. Limit refresh attempts, prevent concurrent requests from creating competing refresh operations, and send the user to an authentication flow when refresh validation fails. Keep clock differences in mind when testing expiration; small clock-skew allowances may be supported by a library, but they should be deliberate rather than accidental.

Storage and transport

Always use HTTPS for tokens in transit. For browser clients, compare an HttpOnly, Secure cookie with a same-site policy against in-memory or web-storage approaches. Cookies can reduce token exposure to JavaScript but require protection against cross-site request forgery when the browser sends them automatically. In-memory tokens disappear on reload but are less persistent. Web storage is easy to integrate, yet any successful script injection can potentially read stored tokens.

There is no storage choice that removes the need for output encoding, dependency hygiene, content security controls, and careful cookie configuration. For a broader comparison of browser storage behavior, see Local Storage vs Session Storage vs Cookies.

Best fit by scenario

  • Single backend and one frontend: A short-lived access token, a carefully protected refresh mechanism, and explicit issuer and audience checks may provide a manageable design.
  • Several internal APIs: Centralize issuance and publish a consistent verification policy. Each service should still verify signatures and relevant claims rather than trusting a claim forwarded by another service.
  • Third-party API access: Separate your application's user session from credentials used to call the provider. Store provider secrets on the server and grant only the scopes required.
  • Browser-heavy application: Evaluate cookie-based sessions as well as JWTs. JWTs are not automatically better; a server-managed session can simplify revocation and reduce client-side token handling.
  • Debugging a failing integration: Decode the token locally to inspect its header and non-sensitive claims, then verify the issuer, audience, expiration, signing key, and algorithm. Never paste production tokens into an online JWT decoder.

For related API work, consistent JSON inspection can make authentication responses easier to diagnose. The site's JSON formatter and validator guide covers formatting and debugging without confusing readable output with validated security data.

When to revisit

Revisit this JWT design whenever the issuer, API audience, signing algorithm, key-management process, frontend storage model, or identity provider changes. Also review it after a dependency upgrade, a security incident, a change in browser architecture, or the introduction of a new service that consumes tokens.

Make the review practical: list every token type and its lifetime; confirm where each token is created, transmitted, stored, and revoked; test malformed, expired, replayed, and wrong-audience tokens; verify that logs do not contain complete tokens; and document the key-rotation procedure. Keep a small integration test suite for both JavaScript and Python clients if both are supported. Finally, check the implementation against current library documentation and your organization's security requirements rather than assuming that a previously safe configuration remains appropriate.

The core rule remains stable: decoding helps you inspect a JWT, while verification establishes whether your application should trust it. Build authorization decisions only from claims that passed signature, time, issuer, audience, and application-specific checks.

Related Topics

#jwt#authentication#apis#javascript#python#security
T

The Code Website Editorial Team

Developer Resources Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.