← Files Auth0ARCHIVED FILE

skills/auth0/references/feature-dpop/index.md

14.9 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

# Auth0 DPoP (Device-Bound Tokens)

Bind access tokens to the client's cryptographic key so stolen tokens cannot be replayed.

---

## Overview

### What is DPoP?

DPoP (Demonstrating Proof-of-Possession) is an OAuth 2.0 mechanism defined in
[RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) that cryptographically
binds access tokens to a client-held key pair. Each API request includes a
short-lived signed JWT (the DPoP proof) that proves the sender holds the private
key — a stolen token alone cannot be replayed by an attacker.

### When to Use DPoP

- Protecting high-value API calls against token theft and replay attacks
- Meeting security or compliance requirements that mandate sender-constrained tokens
- Any SPA or Vanilla JS app calling a protected Auth0 API with elevated security needs

### When NOT to Use DPoP

- **SSR / server-side environments** — DPoP relies on a private key held in the browser; it cannot be safely used server-side (Next.js, Nuxt, etc.)
- **APIs that don't support DPoP** — the resource server must be configured to accept DPoP token dialect; Bearer-only APIs will reject DPoP proofs
- **Flows requiring token sharing** — DPoP tokens are bound to a single key pair and cannot be forwarded to or reused by another client

### Requirements

- Auth0 tenant with DPoP-capable authorization server
- API resource server with DPoP token dialect enabled
- A browser SPA using one of: `@auth0/auth0-vue`, `@auth0/auth0-react`,
  `@auth0/auth0-angular`, or `@auth0/auth0-spa-js`
- HTTPS in production (required by Auth0 for DPoP)

### Key Concepts

| Concept | Description |
|---------|-------------|
| DPoP Proof | A short-lived signed JWT attached to each request proving key possession |
| DPoP Nonce | A server-issued value that must be included in the proof to prevent replay |
| `useDpop: true` | SDK option that enables automatic DPoP proof generation |
| `createFetcher()` | SDK helper that returns a `fetch`-compatible function handling proofs automatically |
| `UseDpopNonceError` | Error thrown when the server rotates its nonce mid-flight; retry with the new nonce |

---

## Step 1: Enable DPoP on Your API

### Via Auth0 Dashboard

1. Go to **Applications → APIs**
2. Select the API your SPA calls
3. Under the **Settings** tab, confirm the API identifier matches your `audience`
4. No additional toggle is needed in the dashboard — DPoP is enabled per-request
   by the client when the API resource server is configured to accept DPoP tokens

### Via Auth0 CLI

```bash
# Inspect current resource server settings
auth0 api get "resource-servers" | jq '.[] | select(.identifier == "https://your-api-identifier")'

# Enable DPoP token dialect on the API
auth0 api patch "resource-servers/{API_ID}" \
  --data '{"token_dialect": "access_token_authz"}'
```

> Replace `{API_ID}` with the ID returned from the GET call above.

### Same configuration in Terraform / MCP

Sender-constraining is set on the resource server (via a `proof_of_possession`
setting with `mechanism: "dpop"`) and, for a mandatory client, on the client.
There is no tenant-wide DPoP toggle. The loaded tooling reference has the exact
syntax:
- CLI: `auth0 api patch resource-servers/{API_ID}` with the `proof_of_possession` payload
- Terraform: `auth0_resource_server` `proof_of_possession` block + `auth0_client` `require_proof_of_possession`
- MCP: `auth0_update_resource_server` accepts `proof_of_possession`; the client-side toggle is **not** exposed by MCP — set it via CLI or Terraform.

---

## Step 2: Configure Your Application

### Common pattern across all frameworks

1. Add `useDpop: true` to your Auth0 client/provider configuration alongside your `audience`
2. Use `createFetcher()` instead of attaching tokens manually — the SDK handles
   proof generation, nonce management, and header injection for you
3. Handle `UseDpopNonceError` in cases where the server rotates its nonce

### Environment variables

Ensure your `.env` includes the API audience:

```bash
# Vite
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
VITE_AUTH0_AUDIENCE=https://your-api-identifier
```

---

## Framework Examples

### Vue.js

Uses `@auth0/auth0-vue`.

#### 1. Enable DPoP in plugin config

```typescript
// src/main.ts
import { createApp } from 'vue';
import { createAuth0 } from '@auth0/auth0-vue';
import App from './App.vue';

const app = createApp(App);

app.use(
  createAuth0({
    domain: import.meta.env.VITE_AUTH0_DOMAIN,
    clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
    authorizationParams: {
      redirect_uri: window.location.origin,
      audience: import.meta.env.VITE_AUTH0_AUDIENCE
    },
    useDpop: true
  })
);

app.mount('#app');
```

#### 2. Make DPoP-protected API calls

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useAuth0, UseDpopNonceError } from '@auth0/auth0-vue';

const { createFetcher } = useAuth0();
const data = ref(null);
const error = ref<string | null>(null);

// Create a DPoP-aware fetcher bound to your API base URL
const apiFetch = createFetcher({
  baseUrl: 'https://your-api.example.com'
});

const fetchData = async () => {
  error.value = null;
  try {
    const response = await apiFetch('/data');
    data.value = await response.json();
  } catch (err) {
    if (err instanceof UseDpopNonceError) {
      // Server rotated its nonce — retry once
      const response = await apiFetch('/data');
      data.value = await response.json();
    } else {
      error.value = (err as Error).message;
    }
  }
};
</script>

<template>
  <div>
    <button @click="fetchData">Fetch Data</button>
    <div v-if="error" class="error">{{ error }}</div>
    <pre v-if="data">{{ JSON.stringify(data, null, 2) }}</pre>
  </div>
</template>
```

#### 3. POST / PUT / DELETE requests

```typescript
// Pass standard fetch options as the second argument
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});
```

#### 4. Multiple APIs with separate fetchers

```typescript
const { createFetcher } = useAuth0();

const ordersApi = createFetcher({ baseUrl: 'https://orders.example.com' });
const inventoryApi = createFetcher({ baseUrl: 'https://inventory.example.com' });
```

#### 5. Manual DPoP management (advanced)

```typescript
import { useAuth0 } from '@auth0/auth0-vue';

const { generateDpopProof, getDpopNonce, setDpopNonce } = useAuth0();

// Generate a proof manually (e.g. for a custom fetch wrapper)
const proof = await generateDpopProof({
  url: 'https://your-api.example.com/data',
  method: 'GET',
  nonce: getDpopNonce()
});

const response = await fetch('https://your-api.example.com/data', {
  headers: {
    Authorization: `DPoP ${accessToken}`,
    DPoP: proof
  }
});

// Store a server-issued nonce for subsequent requests
const newNonce = response.headers.get('DPoP-Nonce');
if (newNonce) setDpopNonce(newNonce);
```

---

### React

Uses `@auth0/auth0-react`.

#### 1. Enable DPoP in provider config

```tsx
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Auth0Provider } from '@auth0/auth0-react';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Auth0Provider
      domain={import.meta.env.VITE_AUTH0_DOMAIN}
      clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}
      authorizationParams={{
        redirect_uri: window.location.origin,
        audience: import.meta.env.VITE_AUTH0_AUDIENCE
      }}
      useDpop={true}
    >
      <App />
    </Auth0Provider>
  </React.StrictMode>
);
```

#### 2. Make DPoP-protected API calls

```tsx
import { useAuth0, UseDpopNonceError } from '@auth0/auth0-react';
import { useState, useMemo } from 'react';

export function DataFetcher() {
  const { createFetcher } = useAuth0();
  const [data, setData] = useState(null);
  const [error, setError] = useState<string | null>(null);

  const apiFetch = useMemo(
    () => createFetcher({ baseUrl: 'https://your-api.example.com' }),
    [createFetcher]
  );

  const fetchData = async () => {
    setError(null);
    try {
      const response = await apiFetch('/data');
      setData(await response.json());
    } catch (err) {
      if (err instanceof UseDpopNonceError) {
        // Server rotated its nonce — retry once
        const response = await apiFetch('/data');
        setData(await response.json());
      } else {
        setError((err as Error).message);
      }
    }
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {error && <div className="error">{error}</div>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}
```

#### 3. Memoize the fetcher to avoid re-creation

```tsx
import { useAuth0 } from '@auth0/auth0-react';
import { useMemo } from 'react';

export function useApiClient() {
  const { createFetcher } = useAuth0();

  return useMemo(
    () => createFetcher({ baseUrl: 'https://your-api.example.com' }),
    [createFetcher]
  );
}
```

#### 4. POST / PUT / DELETE requests

```typescript
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});
```

---

### Angular

Uses `@auth0/auth0-angular`.

#### 1. Enable DPoP in provider config

**Standalone components (Angular 14+):**

```typescript
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAuth0 } from '@auth0/auth0-angular';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAuth0({
      domain: environment.auth0.domain,
      clientId: environment.auth0.clientId,
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: environment.auth0.audience
      },
      useDpop: true
    })
  ]
};
```

**NgModule-based apps:**

```typescript
// src/app/app.module.ts
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: environment.auth0.domain,
      clientId: environment.auth0.clientId,
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: environment.auth0.audience
      },
      useDpop: true
    })
  ]
})
export class AppModule {}
```

#### 2. Make DPoP-protected API calls

```typescript
import { Component, inject, signal } from '@angular/core';
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';

@Component({
  selector: 'app-data',
  template: `
    <button (click)="fetchData()">Fetch Data</button>
    <div *ngIf="error()">{{ error() }}</div>
    <pre *ngIf="data()">{{ data() | json }}</pre>
  `
})
export class DataComponent {
  private auth = inject(AuthService);
  data = signal<unknown>(null);
  error = signal<string | null>(null);

  // Created once — reused across all fetchData() calls so nonce state is preserved
  private apiFetch = this.auth.createFetcher({
    baseUrl: 'https://your-api.example.com'
  });

  async fetchData() {
    this.error.set(null);
    try {
      const response = await this.apiFetch('/data');
      this.data.set(await response.json());
    } catch (err) {
      if (err instanceof UseDpopNonceError) {
        // Server rotated its nonce — retry once
        const response = await this.apiFetch('/data');
        this.data.set(await response.json());
      } else {
        this.error.set((err as Error).message);
      }
    }
  }
}
```

#### 3. POST / PUT / DELETE requests

```typescript
// Uses the same class-field fetcher — pass standard fetch options as the second argument
const response = await this.apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});
```

---

### auth0-spa-js (Vanilla JS)

Uses `@auth0/auth0-spa-js` directly — suitable for Vanilla JS, Svelte, SolidJS,
or any SPA not using a framework wrapper.

#### 1. Initialize client with DPoP enabled

```typescript
import { createAuth0Client, UseDpopNonceError } from '@auth0/auth0-spa-js';

const auth0 = await createAuth0Client({
  domain: import.meta.env.VITE_AUTH0_DOMAIN,
  clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
  authorizationParams: {
    redirect_uri: window.location.origin,
    audience: import.meta.env.VITE_AUTH0_AUDIENCE
  },
  useDpop: true
});

// Handle redirect callback after login
const query = new URLSearchParams(window.location.search);
if ((query.has('code') || query.has('error')) && query.has('state')) {
  await auth0.handleRedirectCallback();
  window.history.replaceState({}, document.title, window.location.pathname);
}
```

#### 2. Make DPoP-protected API calls

```typescript
// Create a DPoP-aware fetcher
const apiFetch = auth0.createFetcher({
  baseUrl: 'https://your-api.example.com'
});

async function fetchData() {
  try {
    const response = await apiFetch('/data');
    return await response.json();
  } catch (err) {
    if (err instanceof UseDpopNonceError) {
      // Server rotated its nonce — retry once
      const response = await apiFetch('/data');
      return await response.json();
    }
    throw err;
  }
}
```

#### 3. POST / PUT / DELETE requests

```typescript
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});
```

#### 4. Multiple APIs with separate fetchers

```typescript
const ordersApi = auth0.createFetcher({ baseUrl: 'https://orders.example.com' });
const inventoryApi = auth0.createFetcher({ baseUrl: 'https://inventory.example.com' });

const [orders, inventory] = await Promise.all([
  ordersApi('/list').then(r => r.json()),
  inventoryApi('/stock').then(r => r.json())
]);
```

---

## Error Handling

### `UseDpopNonceError`

Servers may rotate their DPoP nonce. When this happens the SDK throws
`UseDpopNonceError`. Retry the request once — the SDK will have updated the
stored nonce automatically:

```typescript
// Import from your framework SDK:
// @auth0/auth0-vue | @auth0/auth0-react | @auth0/auth0-angular | @auth0/auth0-spa-js
import { UseDpopNonceError } from '@auth0/auth0-vue';

try {
  const response = await apiFetch('/data');
  const data = await response.json();
} catch (err) {
  if (err instanceof UseDpopNonceError) {
    // Nonce was stale — retry once; SDK has already stored the new nonce
    const response = await apiFetch('/data');
    const data = await response.json();
  } else {
    throw err;
  }
}
```

---

## Common Issues

| Issue | Cause | Fix |
|-------|-------|-----|
| API returns `401` with `error: use_dpop_nonce` | Server issued a new nonce | Catch `UseDpopNonceError` and retry |
| API returns `401` with `invalid_dpop_proof` | Clock skew or wrong `htm`/`htu` values | Ensure system clock is accurate; verify `baseUrl` matches API URL exactly |
| Token still issued as Bearer instead of DPoP | `useDpop: true` missing or `audience` not set | Confirm both options are present in client config |
| `createFetcher` is undefined | SDK version too old | Upgrade to `@auth0/auth0-spa-js` ≥ 2.1 (or framework SDK wrapping it) |

SHA-256: eb074b8ebb0b7dc0603ed3cc636c86c2c9cea14d0ec1b88e05533c8007d02cfb