Skip to content

feat: add DPoP support with fetcher API - #500

Merged
yogeshchoudhary147 merged 3 commits into
mainfrom
feat/dpop-support
Dec 11, 2025
Merged

yogeshchoudhary147 merged 3 commits into
mainfrom
feat/dpop-support

Conversation

@yogeshchoudhary147

@yogeshchoudhary147 yogeshchoudhary147 commented Dec 3, 2025 •

Copy link
Copy Markdown
Contributor

Description

Adds DPoP (Demonstrating Proof-of-Possession) support by exposing new authentication methods from @auth0/auth0-spa-js v2.10.0. DPoP cryptographically binds access tokens to clients, preventing token theft and replay attacks.

What is DPoP?

DPoP prevents security vulnerabilities by:

  • Token Theft Protection - Stolen tokens are cryptographically bound and unusable
  • Replay Attack Prevention - Tokens are tied to specific HTTP requests
  • Token Exfiltration Mitigation - Tokens require the client's private key

Changes

1. Dependency & Compatibility Updates

  • ⬆️ Upgrade @auth0/auth0-spa-js to v2.10.0

    • Adds DPoP (Demonstrating Proof-of-Possession) support
    • Includes new authentication methods for secure API calls
  • 🔧 Update handleRedirectCallback return type

    • Added ConnectAccountRedirectResult to support account linking flows
    • Fully backward compatible - existing code works unchanged
  • 🧪 Add TextEncoder/TextDecoder polyfills

    • auth0-spa-js v2.10.0 uses Web Crypto APIs for DPoP operations
    • These APIs aren't available globally in Node.js test environments
    • Polyfills provide TextEncoder/TextDecoder globals for Jest tests
    • See jest.setup.js - only affects test environment, not production

2. New DPoP Methods

Method Description
getDpopNonce(id?) Retrieve DPoP nonce for an API identifier
setDpopNonce(nonce, id?) Store DPoP nonce for future requests
generateDpopProof(params) Generate cryptographic DPoP proof JWT
createFetcher(config?) Create authenticated fetcher with automatic DPoP handling ⭐

3. New Exports

Class:

  • UseDpopNonceError - DPoP nonce error class

Types:

  • FetcherConfig - Fetcher configuration options
  • Fetcher - Fetcher instance type
  • CustomFetchMinimalOutput - Custom response type constraint

4. Documentation & Testing

  • ✅ Comprehensive JSDoc documentation for all methods
  • ✅ Unit tests for all 4 new DPoP methods
  • ✅ All existing tests pass

Usage

Basic Setup

import { createAuth0 } from '@auth0/auth0-vue';

const auth0 = createAuth0({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
    audience: 'https://api.example.com'
  },
  useDpop: true  // Enable DPoP
});

app.use(auth0);

Recommended: Using createFetcher() ⭐

The simplest way to make authenticated API calls with DPoP:

<script setup>
import { useAuth0 } from '@auth0/auth0-vue';

const auth0 = useAuth0();

async function fetchData() {
  // Create fetcher - handles tokens, DPoP proofs, and nonces automatically
  const fetcher = auth0.createFetcher({ 
    dpopNonceId: 'my-api',
    baseUrl: 'https://api.example.com'
  });

  const response = await fetcher.fetchWithAuth('/protected-data');
  return response.json();
}
</script>

What createFetcher() does automatically:

  • ✅ Retrieves access tokens via getAccessTokenSilently()
  • ✅ Adds proper Authorization headers (DPoP <token> or Bearer <token>)
  • ✅ Generates and includes DPoP proofs in the DPoP header
  • ✅ Manages DPoP nonces per API endpoint
  • ✅ Automatically retries on nonce errors
  • ✅ Handles token refreshing
  • ✅ Works with both DPoP and Bearer tokens

Multiple APIs

// Each fetcher manages its own nonces independently
const api1 = auth0.createFetcher({
  dpopNonceId: 'internal-api',
  baseUrl: 'https://internal.example.com'
});

const api2 = auth0.createFetcher({
  dpopNonceId: 'partner-api',
  baseUrl: 'https://partner.example.com'
});

const data1 = await api1.fetchWithAuth('/data');
const data2 = await api2.fetchWithAuth('/resources');

Advanced: Manual DPoP Management

For scenarios requiring full control:

import { useAuth0, UseDpopNonceError } from '@auth0/auth0-vue';

const auth0 = useAuth0();

try {
  const token = await auth0.getAccessTokenSilently();
  const nonce = await auth0.getDpopNonce('my-api');
  
  const proof = await auth0.generateDpopProof({
    url: 'https://api.example.com/data',
    method: 'POST',
    accessToken: token,
    nonce
  });

  const response = await fetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
      'Authorization': `DPoP ${token}`,
      'DPoP': proof,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ data: 'example' })
  });

  // Update nonce if server provides new one
  const newNonce = response.headers.get('DPoP-Nonce');
  if (newNonce) {
    await auth0.setDpopNonce(newNonce, 'my-api');
  }

} catch (error) {
  if (error instanceof UseDpopNonceError) {
    console.error('DPoP nonce error:', error.message);
  }
}

Error Handling

import { UseDpopNonceError } from '@auth0/auth0-vue';

try {
  const response = await fetcher.fetchWithAuth('/data');
} catch (error) {
  if (error instanceof UseDpopNonceError) {
    // DPoP nonce validation failed
    console.error('Nonce error:', error.message);
  }
}

Why Use createFetcher()?

Feature createFetcher() Manual
Token retrieval ✅ Automatic ❌ Manual
DPoP proof generation ✅ Automatic ❌ Manual
Nonce management ✅ Automatic ❌ Manual
Error retry ✅ Automatic ❌ Manual
Code complexity ✅ Low ❌ High

Breaking Changes

None - Fully backward compatible:

  • ✅ New methods are additive
  • ✅ handleRedirectCallback return type is a union (includes old type)
  • ✅ All existing code works without modification
  • ✅ DPoP is opt-in via useDpop: true

Migration Guide

Enable DPoP

// Add useDpop: true to your configuration
const auth0 = createAuth0({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
  useDpop: true  // Add this
});

Update API Calls (Recommended)

// Before
const token = await auth0.getAccessTokenSilently();
const response = await fetch('https://api.example.com/data', {
  headers: { 'Authorization': `Bearer ${token}` }
});

// After - Use fetcher
const fetcher = auth0.createFetcher({
  dpopNonceId: 'my-api',
  baseUrl: 'https://api.example.com'
});
const response = await fetcher.fetchWithAuth('/data');

Testing

  • ✅ Unit tests for getDpopNonce() with/without ID
  • ✅ Unit tests for setDpopNonce() with/without ID
  • ✅ Unit test for generateDpopProof()
  • ✅ Unit tests for createFetcher() with/without config
  • ✅ All existing tests pass

Related

Checklist

  • Tests pass
  • Types exported
  • Documentation added
  • No breaking changes
  • Backward compatible

- Update @auth0/auth0-spa-js dependency to v2.10.0
- Fix handleRedirectCallback return type to include ConnectAccountRedirectResult
- Import ConnectAccountRedirectResult type from spa-js
- Add TextEncoder/TextDecoder polyfills in jest.setup.js for Node.js test environment
- Configure Jest to use setup file for polyfills

The handleRedirectCallback method now returns a union type that includes
ConnectAccountRedirectResult to support the new connect account flow
introduced in spa-js v2.10.0.

The TextEncoder/TextDecoder polyfills are required because spa-js v2.10.0
uses these Web APIs for DPoP functionality, which are not available in
Node.js test environment by default.
- Add getDpopNonce() method to retrieve DPoP nonces
- Add setDpopNonce() method to store DPoP nonces
- Add generateDpopProof() method to generate DPoP proof JWTs
- Add createFetcher() method for automatic authenticated API requests
- Export FetcherConfig, Fetcher, CustomFetchMinimalOutput types
- Export UseDpopNonceError for error handling
- Add comprehensive JSDoc documentation for all new methods
- Add unit tests for all new DPoP methods

DPoP (Demonstrating Proof-of-Possession) cryptographically binds access
tokens to a specific client, preventing token theft and replay attacks.

The createFetcher() method is the recommended way to make authenticated
API calls, as it automatically handles token retrieval, DPoP proof
generation, nonce management, and retry logic.
Comment thread src/interfaces/auth0-vue-client.ts
Comment thread src/plugin.ts
);
}

async getDpopNonce(id?: string): Promise<string | undefined> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @yogeshchoudhary147 shouldn't the new async dpop related methods use the proxy pattern.
I see It is used to wrap every single asynchronous method that interacts with the Auth0 Client.

async getDpopNonce(id?: string) {
  return this.__proxy(() => 
    this._client.getDpopNonce(id)
  );
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I've added the __proxy wrapper.

- Wrap getDpopNonce, setDpopNonce, and generateDpopProof with __proxy
- Add error tracking tests for all three async DPoP methods
- Add error clearing test to verify successful calls clear errors
- Ensures consistent error handling and loading state management
- Maintains consistency with existing async methods pattern
@yogeshchoudhary147
yogeshchoudhary147 merged commit 6dede2d into main Dec 11, 2025
17 checks passed
@yogeshchoudhary147
yogeshchoudhary147 deleted the feat/dpop-support branch December 11, 2025 11:04
This was referenced Dec 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants