Back to Blog
QAuth: The Post-Quantum Authentication Protocol That Replaces OAuth 2.0 and JWT

QAuth: The Post-Quantum Authentication Protocol That Replaces OAuth 2.0 and JWT

January 30, 2026
14 min read
Tushar Agrawal

Deep dive into QuantumAuth (QAuth), a next-generation authentication protocol with dual signatures (Ed25519 + ML-DSA-65), encrypted payloads, mandatory proof-of-possession, and built-in revocation. Why OAuth 2.0 and JWT are fundamentally broken and how QAuth fixes everything.

The Problem: OAuth 2.0 and JWT Are Fundamentally Broken

Every security researcher knows it. Every production breach proves it. OAuth 2.0 and JWT have critical, unfixable vulnerabilities that have led to billions of compromised accounts. Let me be direct about what's wrong.

OAuth 2.0: Death by a Thousand Cuts

OAuth 2.0 Critical Vulnerabilities
==================================

1. Redirect URI Manipulation
   - Attackers can steal authorization codes
   - "Open redirect" vulnerabilities are trivial to exploit
   - Result: Full account takeover

2. Authorization Code Interception
   - Codes can be intercepted before exchange
   - PKCE helps but isn't mandatory
   - Many implementations skip it

3. Bearer Token Model
   - Tokens work from ANY device
   - Stolen token = full access
   - No sender verification

4. No Built-in Revocation
   - Tokens valid until expiry
   - Logout doesn't invalidate tokens
   - Breaches have extended impact

5. Cross-App Attacks
   - 16 of 18 major platforms vulnerable (2024 research)
   - Client impersonation attacks
   - Token theft across applications

JWT: The Algorithm Confusion Nightmare

JWT's design is fundamentally flawed. The header contains the algorithm, meaning the attacker chooses how to verify:

# Classic JWT Algorithm Confusion Attack

import jwt
import base64

# 1. "None" Algorithm Attack - Token forgery
malicious_token = base64.b64encode(
    '{"alg":"none","typ":"JWT"}'.encode()
).decode().rstrip('=') + '.' + base64.b64encode(
    '{"sub":"admin","role":"superuser"}'.encode()
).decode().rstrip('=') + '.'

# 2. RSA-to-HMAC Confusion
# Server uses RSA public key to verify
# Attacker signs with RSA public key as HMAC secret
# Server verifies successfully - complete bypass!

public_key = open('public.pem').read()
forged = jwt.encode(
    {"sub": "admin", "role": "superuser"},
    public_key,  # Using PUBLIC key as HMAC secret
    algorithm="HS256"  # Changed from RS256
)
# This VERIFIES on servers that don't validate algorithm!

# 3. Key ID (kid) Injection
# kid header can contain SQL injection, path traversal
# {"alg":"RS256","kid":"../../../../../../dev/null"}
# Results in verification with empty key

Real-world impact: These aren't theoretical - they've caused breaches at major companies. The Auth0 vulnerability (CVE-2022-23529), the PyJWT issues, countless custom implementations getting it wrong.

What's Missing From Current Standards

| Feature | OAuth 2.0 | JWT | Required | |---------|-----------|-----|----------| | Post-quantum crypto | No | No | Yes | | Encrypted payloads | No | No | Yes | | Proof of possession | Optional (DPoP) | No | Mandatory | | Built-in revocation | No | No | Yes | | Safe algorithm selection | No | No | Yes | | Device binding | No | No | Yes | | Fine-grained authorization | Scope explosion | Limited | Policy-based |

Introducing QAuth: Authentication Rebuilt from First Principles

QAuth is a complete replacement for OAuth 2.0 and JWT, designed with one goal: secure by default, not by configuration.

QAuth Design Principles
=======================

1. NO client-controlled algorithm selection
   - Server enforces all crypto parameters
   - Eliminates entire class of attacks

2. ENCRYPTED payloads, not encoded
   - Claims are private, not just signed
   - XChaCha20-Poly1305 encryption

3. MANDATORY proof of possession
   - Every request proves key ownership
   - Stolen tokens are useless

4. BUILT-IN revocation
   - 5-minute offline validity
   - Instant invalidation capability

5. POST-QUANTUM ready
   - Dual signatures: Ed25519 + ML-DSA-65
   - Harvest-now-decrypt-later resistant

6. POLICY references, not scope explosion
   - Fine-grained without token bloat
   - Server-side policy updates

QToken Format: The Anti-JWT

Unlike JWT's vulnerable header.payload.signature format with attacker-controlled algorithms, QToken uses a fixed, binary format:

QToken Structure
================

Header (42 bytes - FIXED):
├── Version:   1 byte  (0x01)
├── TokenType: 1 byte  (access=0x01, refresh=0x02, identity=0x03)
├── KeyID:     32 bytes (SHA-256 of issuer public keys)
└── Timestamp: 8 bytes (Unix milliseconds)

EncryptedPayload (XChaCha20-Poly1305):
├── Nonce:      24 bytes
├── Ciphertext: Variable
│   ├── Subject (sub): encrypted user ID
│   ├── Issuer (iss): encrypted issuer
│   ├── Audience (aud): encrypted audience
│   ├── Expiry (exp): encrypted timestamp
│   ├── PolicyRef: policy document reference
│   ├── RevocationID: unique 16-byte ID
│   └── Claims: custom encrypted data
└── AuthTag:    16 bytes

DualSignature (3373 bytes):
├── Ed25519:   64 bytes (classical)
└── ML-DSA-65: 3309 bytes (post-quantum)

ProofBinding (96 bytes):
├── DeviceKey:  32 bytes (SHA-256 of device public key)
├── ClientKey:  32 bytes (SHA-256 of client public key)
└── IPHash:     32 bytes (salted hash of client IP)

Why This Format Matters

1. No algorithm field - Server enforces Ed25519 + ML-DSA-65, period 2. Encrypted claims - Payloads are private, preventing information leakage 3. Dual signatures - Both classical and post-quantum must verify 4. Proof binding - Token bound to specific device and client keys

Dual Signature System: Belt and Suspenders

QAuth uses dual signatures for defense-in-depth:

/// QAuth Dual Signature Implementation
/// Both Ed25519 AND ML-DSA-65 must verify

pub struct DualSignature {
    pub ed25519: [u8; 64],      // Classical: 64 bytes
    pub mldsa: Vec<u8>,         // Post-quantum: 3309 bytes
}

impl IssuerSigningKeys {
    /// Sign with both algorithms - both must succeed
    pub fn sign(&self, message: &[u8]) -> DualSignature {
        let ed25519_sig = self.ed25519.sign(message);
        let mldsa_sig = self.mldsa.sign(message);
        DualSignature {
            ed25519: ed25519_sig,
            mldsa: mldsa_sig,
        }
    }
}

impl IssuerVerifyingKeys {
    /// Verify both signatures - both must pass
    pub fn verify(&self, message: &[u8], signature: &DualSignature) -> Result<()> {
        // Verify Ed25519 (classical)
        let ed25519_sig = Ed25519Signature::from_bytes(&signature.ed25519);
        self.ed25519.verify(message, &ed25519_sig)?;

        // Verify ML-DSA-65 (post-quantum)
        let mldsa_sig = MlDsaSignature::from_bytes(&signature.mldsa)?;
        dilithium3::verify_detached_signature(&mldsa_sig, message, &self.mldsa)?;

        Ok(())
    }
}

Why dual signatures?

  • If Ed25519 is broken by quantum computers, ML-DSA-65 remains secure
  • If ML-DSA-65 has an undiscovered flaw, Ed25519 is battle-tested
  • Both must verify - attacker must break both simultaneously

Mandatory Proof of Possession

Unlike OAuth's bearer tokens that work from any device, QAuth requires every request to prove key possession:

Proof of Possession Flow
========================

1. Client has ephemeral key pair (Ed25519)

2. Every API request includes:
   Authorization: QAuth <QToken>
   X-QAuth-Proof: <SignedProof>

3. SignedProof = Sign(
     timestamp ||
     request_method ||
     request_uri ||
     SHA256(body) ||
     SHA256(token),
     client_private_key
   )

4. Server verifies:
   - Proof signature valid
   - Timestamp within 60 seconds
   - Token's ProofBinding matches client public key
   - Nonce not reused

Result: Stolen token + stolen proof = USELESS
(Attacker can't generate new proofs without private key)

Implementation Example

// TypeScript SDK - Proof of Possession

interface ProofInput {
  timestamp: number;
  method: string;
  uri: string;
  bodyHash: string;
  tokenHash: string;
}

class QAuthClient {
  private privateKey: CryptoKeyPair;

  async createProof(request: Request, token: string): Promise<string> {
    const input: ProofInput = {
      timestamp: Date.now(),
      method: request.method,
      uri: new URL(request.url).pathname,
      bodyHash: await sha256(await request.text()),
      tokenHash: await sha256(token),
    };

    // Sign the proof
    const message = this.serializeProof(input);
    const signature = await this.sign(message);

    return base64url.encode(
      new Uint8Array([
        ...this.serializeProof(input),
        ...signature,
      ])
    );
  }

  async makeRequest(url: string, options: RequestInit = {}): Promise<Response> {
    const proof = await this.createProof(
      new Request(url, options),
      this.token
    );

    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `QAuth ${this.token}`,
        'X-QAuth-Proof': proof,
      },
    });
  }
}

Built-in Revocation System

JWT's stateless nature means tokens can't be revoked without external systems. QAuth solves this with built-in revocation:

Revocation Architecture
=======================

QToken contains:
├── revocation_id: 16-byte unique identifier
├── revocation_endpoint: URL to check
└── max_offline_validity: 5 minutes (default)

Verification Flow:
1. Verify dual signatures (Ed25519 + ML-DSA-65)
2. Verify proof of possession
3. Check local revocation cache
   - If fresh (< max_offline_validity): use cache
   - If stale: query revocation endpoint
4. Cache result with TTL

Revocation Methods:
├── Push: WebSocket/SSE to all validators
├── Pull: HTTP query to revocation endpoint
└── Bloom: Compact revocation list (space-efficient)

Timeline:
├── Token stolen at T=0
├── Admin revokes at T=1min
├── All validators notified by T=5min (worst case)
└── Attacker window: 5 minutes vs JWT's hours/days

Revocation Implementation

/// QAuth Revocation Checking

pub struct RevocationChecker {
    cache: Cache<[u8; 16], RevocationStatus>,
    endpoint: String,
}

impl RevocationChecker {
    pub async fn is_revoked(&self, revocation_id: &[u8; 16]) -> Result<bool> {
        // Check cache first
        if let Some(status) = self.cache.get(revocation_id) {
            if status.is_fresh() {
                return Ok(status.revoked);
            }
        }

        // Cache miss or stale - check endpoint
        let response = self.client
            .get(&format!("{}/check/{}", self.endpoint, hex::encode(revocation_id)))
            .send()
            .await?;

        let status: RevocationStatus = response.json().await?;

        // Update cache
        self.cache.insert(*revocation_id, status.clone());

        Ok(status.revoked)
    }
}

Policy-Based Authorization (No Scope Explosion)

OAuth's scope system doesn't scale. QAuth uses policy references instead:

OAuth Scopes vs QAuth Policies
==============================

OAuth (scope explosion):
"read:users write:users delete:users read:orders
 write:orders read:inventory write:inventory
 admin:reports read:analytics..."

QAuth (policy reference):
{
  "policy_ref": "urn:qauth:policy:user-manager",
  "policy_version": "2026-01-30"
}

The policy document (fetched/cached separately):
{
  "id": "urn:qauth:policy:user-manager",
  "rules": [
    {
      "resource": "users/**",
      "actions": ["read", "write"],
      "conditions": {
        "time": { "after": "09:00", "before": "18:00", "timezone": "America/New_York" },
        "ip_range": ["10.0.0.0/8", "192.168.0.0/16"],
        "mfa_verified": true
      }
    },
    {
      "resource": "users/*/sensitive",
      "actions": ["read"],
      "conditions": {
        "approval_required": true,
        "audit_log": true
      }
    }
  ]
}

Benefits of Policy References

1. Token stays small - Just a reference, not all permissions 2. Policies update without re-issuing tokens - Change server-side 3. Supports RBAC, ABAC, ReBAC - One unified system 4. Context-aware conditions - Time, location, device, MFA status 5. Audit-friendly - Clear policy documents

Complete QAuth Flow

QAuth Authorization Flow
========================

┌─────────┐                              ┌─────────────┐
│ Client  │                              │ Auth Server │
└────┬────┘                              └──────┬──────┘
     │                                          │
     │ 1. Generate ephemeral keypair            │
     │    (Ed25519 for proof-of-possession)     │
     │                                          │
     │ 2. Authorization Request                 │
     │    + Client public key                   │
     │    + PKCE challenge (mandatory)          │
     │    + Device attestation                  │
     │ ──────────────────────────────────────►  │
     │                                          │
     │ 3. User authenticates + consents         │
     │                                          │
     │ 4. Authorization Response                │
     │    + Encrypted auth code                 │
     │    + Bound to client public key          │
     │ ◄──────────────────────────────────────  │
     │                                          │
     │ 5. Token Request                         │
     │    + Auth code                           │
     │    + PKCE verifier                       │
     │    + Proof of possession                 │
     │ ──────────────────────────────────────►  │
     │                                          │
     │ 6. QToken Response                       │
     │    + Access QToken (bound to client)     │
     │    + Refresh QToken                      │
     │ ◄──────────────────────────────────────  │
     │                                          │
     │ 7. API Request                           │
     │    + QToken                              │
     │    + Fresh proof of possession           │
     │ ──────────────────────────────────────►  │
     │                                          │

Every subsequent request requires NEW proof.
Stolen token without private key = useless.

Security Analysis: QAuth vs Known Attacks

JWT Attacks - All Mitigated

| Attack | JWT Vulnerable | QAuth Protected | How | |--------|----------------|-----------------|-----| | Algorithm confusion | Yes | N/A | No algorithm field | | "None" algorithm | Yes | N/A | Algorithm not configurable | | Key ID injection | Yes | No | KeyID is hash of public key | | Signature bypass | Yes | No | Server enforces verification | | Token replay | Yes | No | Proof of possession | | Token theft | Yes | No | Bound to device/client key | | Payload inspection | Yes | No | Encrypted, not encoded | | Expiry extension | Yes | No | Encrypted expiry |

OAuth 2.0 Attacks - All Mitigated

| Attack | OAuth Vulnerable | QAuth Protected | How | |--------|------------------|-----------------|-----| | Redirect URI manipulation | Yes | No | Cryptographic binding | | Authorization code interception | Yes | No | Bound to client public key | | PKCE downgrade | Yes | N/A | PKCE mandatory | | Bearer token theft | Yes | No | Proof of possession required | | Cross-app attacks | Yes | No | Client key binding | | Token reuse | Yes | No | Request-specific proofs |

Post-Quantum Security

| Threat | Current Status | QAuth Protection | |--------|----------------|------------------| | Harvest-now-decrypt-later | Active threat | ML-DSA-65 signatures | | Quantum signature forgery | Future threat | Dual Ed25519 + ML-DSA-65 | | Quantum key compromise | Future threat | Keys derived with SHA3-512 |

SDK Availability

QAuth is available in multiple languages:

Rust (Reference Implementation)

use qauth::{Issuer, Token, Validator, PolicyEngine};

// Generate issuer keys (dual: Ed25519 + ML-DSA-65)
let issuer = Issuer::generate();

// Create a token
let claims = Claims::new()
    .subject("user123")
    .audience("api.example.com")
    .policy_ref("urn:qauth:policy:standard-user")
    .expires_in(Duration::hours(1));

let token = issuer.create_token(claims)?;
let qtoken_string = token.encode();

// Validate a token
let validator = Validator::new(issuer.verifying_keys());
let verified = validator.validate(&qtoken_string)?;

TypeScript/JavaScript

import { QAuthIssuer, QAuthValidator, Claims } from 'qauth';

// Generate issuer
const issuer = await QAuthIssuer.generate();

// Create token
const claims = new Claims()
  .subject('user123')
  .audience('api.example.com')
  .policyRef('urn:qauth:policy:standard-user')
  .expiresIn(3600);

const token = await issuer.createToken(claims);

// Validate
const validator = new QAuthValidator(issuer.verifyingKeys());
const verified = await validator.validate(token);

Python

from qauth import QAuthIssuer, QAuthValidator, Claims

# Generate issuer
issuer = QAuthIssuer.generate()

# Create token
claims = Claims() \
    .subject("user123") \
    .audience("api.example.com") \
    .policy_ref("urn:qauth:policy:standard-user") \
    .expires_in(3600)

token = issuer.create_token(claims)

# Validate
validator = QAuthValidator(issuer.verifying_keys())
verified = validator.validate(token)

Go

import "github.com/tushar010402/qauth-go"

// Generate issuer
issuer := qauth.GenerateIssuer()

// Create token
claims := qauth.NewClaims().
    Subject("user123").
    Audience("api.example.com").
    PolicyRef("urn:qauth:policy:standard-user").
    ExpiresIn(3600)

token, err := issuer.CreateToken(claims)

// Validate
validator := qauth.NewValidator(issuer.VerifyingKeys())
verified, err := validator.Validate(token)

Performance Benchmarks

QAuth performance on modern hardware:

Benchmark Results (Apple M3, single thread)
==========================================

Key Generation:
├── Ed25519 keypair:    45 µs
├── ML-DSA-65 keypair:  892 µs
└── Combined issuer:    937 µs

Token Creation:
├── Payload encryption:  12 µs
├── Ed25519 signature:   42 µs
├── ML-DSA-65 signature: 1.2 ms
└── Total token create:  1.3 ms

Token Validation:
├── Signature verify:    450 µs
├── Payload decrypt:     8 µs
├── Policy check:        15 µs
└── Total validation:    473 µs

Proof of Possession:
├── Proof creation:      48 µs
├── Proof verification:  52 µs

Throughput (single thread):
├── Token creation:      ~770 tokens/sec
├── Token validation:    ~2,100 validations/sec
├── With caching:        ~15,000+ validations/sec

Migration Path from OAuth 2.0

Phase 1: QAuth Gateway

Deploy QAuth as a proxy in front of existing OAuth:

┌──────────┐      ┌──────────────┐      ┌──────────────┐
│  Client  │─────►│ QAuth Gateway│─────►│ OAuth Server │
│          │◄─────│(Proxy Mode)  │◄─────│  (Legacy)    │
└──────────┘      └──────────────┘      └──────────────┘

- Gateway translates OAuth tokens to QTokens
- Adds proof-of-possession layer
- Gradual client migration

Phase 2: Hybrid Mode

Auth server issues both OAuth and QAuth tokens:

Client A (legacy) ──► OAuth token
Client B (migrated) ──► QToken + Proof
Client C (new) ──► QToken only

Phase 3: Native QAuth

Full QAuth implementation, OAuth deprecated:

All clients ──► QToken + Proof of Possession
OAuth endpoints ──► Removed or redirect to QAuth

Why Use QAuth?

For Security Teams

  • Eliminates entire attack classes - No algorithm confusion, no bearer token theft
  • Post-quantum ready - Dual signatures protect against future quantum attacks
  • Built-in revocation - 5-minute worst-case revocation window
  • Audit-friendly - Clear policy documents, encrypted-by-default

For Developers

  • Simpler mental model - No complex security configurations
  • Type-safe SDKs - Rust, TypeScript, Python, Go
  • Policy updates without re-deployment - Change server-side policies
  • Better error messages - Clear validation failures

For Compliance

  • NIST FIPS 204 compliant - ML-DSA-65 signatures
  • Encrypted at rest and in transit - Payloads never exposed
  • Fine-grained access control - RBAC, ABAC, ReBAC supported
  • Complete audit trail - Every access logged with proof

Getting Started

Installation

# Rust
cargo add qauth

# TypeScript/JavaScript
npm install @quantumshield/qauth

# Python
pip install qauth

# Go
go get github.com/tushar010402/qauth-go

Quick Start

use qauth::{Issuer, Claims, Validator};
use std::time::Duration;

fn main() -> Result<(), qauth::Error> {
    // 1. Generate issuer keys
    let issuer = Issuer::generate();

    // 2. Create a token
    let claims = Claims::new()
        .subject("user-12345")
        .audience("https://api.example.com")
        .policy_ref("urn:qauth:policy:standard")
        .expires_in(Duration::from_secs(3600));

    let token = issuer.create_token(claims)?;
    println!("QToken: {} bytes", token.encode().len());

    // 3. Validate the token
    let validator = Validator::new(issuer.verifying_keys());
    let verified = validator.validate(&token.encode())?;

    println!("Subject: {}", verified.subject());
    println!("Expires: {}", verified.expires_at());

    Ok(())
}

Conclusion

OAuth 2.0 and JWT served their purpose, but their fundamental design flaws can't be fixed with patches. QAuth is built from first principles:

1. Secure by default - No configuration required for security 2. Post-quantum ready - Dual signatures protect against future threats 3. Proof of possession - Stolen tokens are useless 4. Built-in revocation - Instant invalidation 5. Encrypted payloads - Privacy by design 6. Policy-based authorization - No scope explosion

The quantum computing threat isn't hypothetical. Nation-states are harvesting encrypted data today. QAuth ensures your authentication system will remain secure when quantum computers arrive.

---

Resources

QAuth is part of the QuantumShield project, bringing post-quantum cryptography to authentication.

Share this article

Related Articles