QAuth: The Post-Quantum Authentication Protocol That Replaces OAuth 2.0 and JWT
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 Specification
- Rust Implementation
- TypeScript SDK -
npm install @quantumshield/qauth - Python SDK -
pip install qauth - Go SDK
- QuantumShield Crypto Library