Skip to main content
Blog / JWT

Base64URL vs. Base64: JWT Decoding Differences Explained + Quick Fixes

Is your Base64 token failing? Understand the critical differences between Base64 and Base64URL in JWT decoding. Use our tool to troubleshoot encoding/decoding errors now.

DevToolsCenter Team
7 min read

JWT segments (header and payload) are Base64URL-encoded. If your decoder assumes standard Base64, you'll see errors like "Incorrect padding" or "Invalid character". Understanding why Base64URL exists and how to normalize it prevents frustrating decode failures.

Why Base64URL exists

JWTs are designed to be URL-safe. Standard Base64 uses characters (+, /) that have special meaning in URLs and require encoding. Base64URL replaces these with URL-safe alternatives (-, _) so tokens can be:

  • Passed in URL query parameters without encoding
  • Included in HTTP headers without escaping
  • Embedded in HTML without breaking parsing

The problem: Most programming languages have Base64 decoders, but they expect standard Base64 format. When you try to decode Base64URL with a standard Base64 decoder, it fails.

Base64 vs Base64URL: The differences

Character mapping

Standard Base64 Base64URL Why changed
+ - + is encoded as %2B in URLs
/ _ / is a path separator in URLs
= (padding) Often omitted Padding can be inferred from length

Visual examples: Character set differences

Example 1: Standard Base64 with + character

Standard Base64:  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9+signature
Base64URL:        eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9-signature
                                    ↑                    ↑
                              Same characters      + becomes -

Example 2: Standard Base64 with / character

Standard Base64:  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9/signature
Base64URL:        eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9_signature
                                    ↑                    ↑
                              Same characters      / becomes _

Example 3: Padding differences

Standard Base64:  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9==
Base64URL:        eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
                                    ↑
                              Padding (=) omitted

Key visual differences:

  • Character replacement: +- and /_ make tokens URL-safe
  • Padding removal: Base64URL often omits = padding characters
  • Length variation: Base64URL segments may not be multiples of 4 (without padding)

Use our JWT Decoder to see these differences in action - paste a token and watch how it handles Base64URL normalization automatically.

Why decoding fails

Error 1: "Invalid character"

Why it happens: Your decoder encounters - or _ which aren't valid Base64 characters.

Example:

// This fails
Buffer.from('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9', 'base64')
// Error: Invalid character

// Because Base64URL might have: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
// Which contains characters that standard Base64 doesn't recognize

How to fix: Convert Base64URL to Base64 before decoding.

Error 2: "Incorrect padding"

Why it happens: Base64 requires the input length to be a multiple of 4. Base64URL often omits padding (=), so the length might not be a multiple of 4.

Example:

// Base64URL without padding
const segment = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9';
// Length: 36 (not a multiple of 4)

// This fails
Buffer.from(segment, 'base64')
// Error: Incorrect padding

How to fix: Add padding (=) until the length is a multiple of 4.

Normalization function explained

Here's a complete normalization function with error handling:

function normalizeBase64URL(str) {
  if (!str || typeof str !== 'string') {
    throw new Error('Input must be a non-empty string');
  }
  
  // Step 1: Replace URL-safe characters with standard Base64 characters
  str = str.replace(/-/g, '+').replace(/_/g, '/');
  
  // Step 2: Add padding if needed
  // Base64 requires length to be multiple of 4
  const padding = (4 - (str.length % 4)) % 4;
  str += '='.repeat(padding);
  
  return str;
}

// Usage
function decodeJWT(token) {
  const [headerSegment, payloadSegment] = token.split('.');
  
  const decodeSegment = (segment) => {
    const normalized = normalizeBase64URL(segment);
    const decoded = Buffer.from(normalized, 'base64').toString('utf8');
    return JSON.parse(decoded);
  };
  
  return {
    header: decodeSegment(headerSegment),
    payload: decodeSegment(payloadSegment)
  };
}

Why this works:

  1. Character replacement converts Base64URL to Base64 format
  2. Padding ensures the decoder receives valid Base64
  3. The decoder can now process it normally

Real-world examples

Example 1: Node.js decoding

// Token segment (Base64URL)
const segment = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9';

// Normalize
const normalized = normalizeBase64URL(segment);
// Result: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' (adds padding if needed)

// Decode
const decoded = Buffer.from(normalized, 'base64').toString('utf8');
// Result: '{"alg":"RS256","typ":"JWT"}'

// Parse JSON
const header = JSON.parse(decoded);
// Result: { alg: 'RS256', typ: 'JWT' }

Example 2: Browser decoding

// Browser doesn't have Buffer, use atob
function normalizeBase64URL(str) {
  str = str.replace(/-/g, '+').replace(/_/g, '/');
  const padding = (4 - (str.length % 4)) % 4;
  return str + '='.repeat(padding);
}

function decodeSegment(segment) {
  const normalized = normalizeBase64URL(segment);
  const decoded = atob(normalized);
  return JSON.parse(decoded);
}

// Usage
const [headerSegment, payloadSegment] = token.split('.');
const header = decodeSegment(headerSegment);
const payload = decodeSegment(payloadSegment);

Example 3: Python decoding

import base64
import json

def normalize_base64url(s):
    # Replace URL-safe characters
    s = s.replace('-', '+').replace('_', '/')
    # Add padding
    padding = (4 - len(s) % 4) % 4
    return s + '=' * padding

def decode_segment(segment):
    normalized = normalize_base64url(segment)
    decoded = base64.b64decode(normalized).decode('utf-8')
    return json.loads(decoded)

# Usage
header_segment, payload_segment, _ = token.split('.')
header = decode_segment(header_segment)
payload = decode_segment(payload_segment)

Common symptoms and fixes

Symptom: "Invalid character" error

What you see:

Error: Invalid character in base64 encoding

Why it happens: Decoder encountered - or _ which aren't valid Base64 characters.

How to fix:

// Before decoding, normalize
const normalized = segment.replace(/-/g, '+').replace(/_/g, '/');
const decoded = Buffer.from(normalized, 'base64');

Prevention: Always normalize Base64URL before decoding, or use our JWT Encoder/Decoder which handles this automatically.

Need more help? If decoding errors persist, check our complete troubleshooting guide for invalid JWT errors which covers decode failures, verification issues, and common pitfalls.

Symptom: "Incorrect padding" error

What you see:

Error: Incorrect padding

Why it happens: Base64URL segment length isn't a multiple of 4 (padding was omitted).

How to fix:

// Add padding
while (segment.length % 4) {
  segment += '=';
}
const decoded = Buffer.from(segment, 'base64');

Prevention: Use the normalization function above which handles padding automatically.

Symptom: Header decodes but payload fails

What you see: Header decodes fine, but payload throws an error.

Why it happens: One segment might have been normalized while the other wasn't, or one segment has different Base64URL characteristics.

How to fix:

// Normalize both segments consistently
const normalize = (s) => {
  s = s.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';
  return s;
};

const header = JSON.parse(Buffer.from(normalize(headerSegment), 'base64').toString());
const payload = JSON.parse(Buffer.from(normalize(payloadSegment), 'base64').toString());

Prevention: Use a consistent normalization function for all segments.

Why libraries handle this automatically

Most JWT libraries (like jsonwebtoken in Node.js) handle Base64URL normalization internally. They know JWTs use Base64URL and convert it automatically.

When you need manual normalization:

  • Building custom decoders
  • Using raw Base64 decoding APIs
  • Debugging decode failures
  • Working with tokens in non-JWT contexts

Best practices

1. Use JWT-aware libraries when possible

Good:

const jwt = require('jsonwebtoken');
const decoded = jwt.decode(token); // Handles Base64URL automatically

Why: Libraries handle normalization, padding, and edge cases for you.

2. Normalize before raw decoding

If you must decode manually:

function safeDecode(segment) {
  const normalized = normalizeBase64URL(segment);
  return Buffer.from(normalized, 'base64').toString('utf8');
}

3. Test with real tokens

Use our JWT Encoder/Decoder to see how real tokens decode, then replicate that logic in your code.

4. Handle errors gracefully

function decodeSegment(segment) {
  try {
    const normalized = normalizeBase64URL(segment);
    const decoded = Buffer.from(normalized, 'base64').toString('utf8');
    return JSON.parse(decoded);
  } catch (error) {
    if (error.message.includes('Invalid character')) {
      throw new Error('Base64URL normalization failed. Check for invalid characters.');
    }
    if (error.message.includes('padding')) {
      throw new Error('Padding error. Ensure segment length is multiple of 4.');
    }
    throw error;
  }
}

Debugging workflow

When you encounter Base64URL decode errors:

  1. Inspect the segment - Use our JWT Encoder/Decoder to see if it decodes there
  2. Check character set - Look for - or _ that need conversion
  3. Verify padding - Ensure length is multiple of 4
  4. Normalize consistently - Apply same normalization to all segments
  5. Test incrementally - Decode header first, then payload

Tools that help

Browser tool

Our JWT Encoder/Decoder handles Base64URL normalization automatically:

  • No manual conversion needed - Automatically normalizes Base64URL to Base64
  • Instant decoding - See header and payload immediately with error highlighting
  • Client-side processing - Your tokens stay private, no server transmission
  • Visual feedback - See exactly how Base64URL segments are processed

Quick troubleshooting: If your token fails to decode elsewhere, paste it into our tool to verify the Base64URL format is correct. The tool will show you the normalized segments and decoded content instantly.

Command-line debugging

# Decode Base64URL segment manually
echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' | \
  sed 's/-/+/g; s/_/\//g' | \
  awk '{while(length($0)%4)$0=$0"="}1' | \
  base64 -d

Next steps

  1. Try decoding a token with our JWT Encoder/Decoder to see Base64URL in action
  2. Learn about safe JWT decoding practices before verification
  3. Read our troubleshooting guide for common decode errors
  4. Understand Base64 encoding basics for deeper context

Try JWT Encoder/Decoder Now

Ready to put this into practice? Use our free JWT Encoder/Decoder tool. It works entirely in your browser with no signup required.

Launch JWT Encoder/Decoder
Free Forever No Signup Browser-Based

Frequently Asked Questions

Q Why does adding padding fix decoding?

A

JWTs use Base64URL without padding; some decoders expect = padding. Normalizing fixes decoding.