Secure API requests with OAuth DPoP

For the highest level of security when connecting to the Tango API, you can add DPoP (Demonstrating Proof of Possession, RFC 9449) on top of standard OAuth 2.0. DPoP cryptographically binds your access token to a key pair you control. A stolen token is useless without the corresponding private key.

DPoP is optional and can be adopted gradually. Platforms start in opportunistic mode (DPoP proofs are validated when present, but not required) before switching to strict mode (all requests must include a valid proof). Contact your Tango account team to enable DPoP for your platform.

📘

Prerequisite:

Use OAuth for a more secure connection first. You still need OAuth client credentials and a service account—DPoP adds a layer of security on top of this flow; it does not replace it.

How DPoP protects your application

DPoP helps protect against several common token-based security risks by ensuring tokens can only be used by the intended client:

ThreatDPoP protection
Token stolen from logs or memoryPrevents misuse because the attacker cannot generate a valid proof without the private key.
Man-in-the-middle token captureBinds the token to the client’s key pair, making intercepted tokens unusable.
Token replay across endpoints or methodsRequires a unique proof for each request, tied to a specific HTTP method and URI.
Clock manipulationValidates proof timestamps with a strict ±60 second tolerance window.
Lateral movement using leaked credentialsBlocks replay attempts through server-side deduplication within a 120-second window.

How DPoP works

DPoP adds a proof-of-possession layer to OAuth by binding tokens to a cryptographic key. The following flow shows how to generate proofs, obtain a token, and securely call the Tango API:

  1. Generate a key pair: Create an asymmetric key pair once per session or application lifecycle.
  2. Create a DPoP proof JWT: Generate a short-lived, signed assertion tied to the specific HTTP request you are about to make.
  3. Acquire a DPoP-bound token: Include the DPoP proof in your token request (POST /oauth/token) using the DPoP HTTP header. Tango's authorization server returns a DPoP-bound token containing a cnf.jkt claim (a thumbprint of your public key).
  4. Call the Tango API with DPoP: For each API request, include the following headers:
    • Authorization: DPoP <access_token>
    • DPoP:

Step 1: Generate a key pair

Generate a key pair once per session or application lifecycle. EC P-256 (ES256) is recommended for the best balance of security and performance. RSA (RS256, RS384, RS512, PS256, PS384, PS512) is also supported.

# Generate EC P-256 private key
openssl ecparam -name prime256v1 -genkey -noout -out dpop_private.pem

# Derive the public key
openssl ec -in dpop_private.pem -pubout -out dpop_public.pem
📘

Security note:

Securely store your private key in an HSM, secure enclave, or encrypted secrets manager (e.g., AWS Secrets Manager, Azure Key Vault, HashiCorp Vault). Do not transmit, log, or hard-code the key. The key pair can be reused across token requests. DPoP proofs must be generated per request.


Step 2: Create a DPoP Proof JWT

A DPoP Proof is a compact JSON Web Token (JWT) that you generate for each request. It proves that the client making the request controls the private key associated with the access token. A DPoP proof JWT follows the standard JWT structure:

<header>.<payload>.<signature>

A. Header

The header defines the token type (must be dpop+jwt), signing algorithm, and includes your public key (which the server uses to verify the signature):

{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "<base64url-encoded x coordinate>",
    "y": "<base64url-encoded y coordinate>"
  }
}


B1. Payload (token request proof)

The payload contains details about the specific HTTP request used to obtain an access token. Here's an example:

{
  "jti": "a-fresh-uuid-per-request",
  "htm": "POST",
  "htu": "https://sandbox-auth.tangocard.com/oauth/token",
  "iat": 1718000000
}


B2. Payload (API calls proof)

For API calls, the payload includes the same fields as above, with an additional ath claim that binds the proof to the access token. Here's an example:

{
  "jti": "another-fresh-uuid",
  "htm": "GET",
  "htu": "https://integration-api.tangocard.com/raas/v2/customers",
  "iat": 1718000050,
  "ath": "<base64url(SHA-256(access_token))>"
}


C. Signature

ES256 signature over the header and payload, using your private key.


📘

Notes:

  • The jwk header contains your public key only—never include private key material.
  • The htu value must include the scheme, host, and path with no query string.
  • Theath claim is required for API calls (but not for token requests). It equals base64url(SHA-256(access_token)), which binds the proof to the specific access token.
  • Generate a new jti (UUID) for every request—the server blocks reused values within a 120-second window.
  • Keep your system clock synchronized (NTP recommended). The server allows a maximum ±60 seconds skew on iat timestamp.

Step 3: Acquire a DPoP-bound token

Use the same POST {URI}/oauth/token endpoint as standard OAuth, adding the DPoP header:

curl --request POST \
  --url https://sandbox-auth.tangocard.com/oauth/token \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'DPoP: <dpop_proof_jwt>' \
  --data client_id=<your_client_id> \
  --data client_secret=<your_client_secret> \
  --data username=<service_account_username> \
  --data 'password=<service_account_password>' \
  --data scope=raas.all \
  --data audience=https://api.tangocard.com/ \
  --data grant_type=password

The token proof must include:

  • htm=POST

  • htu: the full token endpoint URL (/oauth/token)

    Do not include ath in the token request proof. ath is only required for API call proofs, not token requests.


Response

The following data is returned from the /oauth/token endpoint after you request a token:

{
  "access_token": "<string>",
  "scope": "raas.all",
  "expires_in": 86400,
  "token_type": "DPoP"
}

The token_type of DPoP confirms the token is bound to your key pair. The authorization server has embedded cnf.jkt (your public key thumbprint) in the token.


Step 4: Call the Tango API with DPoP

For every API call, generate a fresh DPoP proof for that specific request. Include the ath claim (SHA-256 hash of your access token) in every API proof.

Here's an example request:

curl \
  --header "Accept: application/json" \
  --header "Authorization: DPoP <your_access_token>" \
  --header "DPoP: <fresh_dpop_proof_jwt>" \
  https://integration-api.tangocard.com/raas/v2/customers
📘

Important:

Use Authorization: DPoP, not Authorization: Bearer. Bearer tokens and DPoP tokens are distinct.


Enforcement modes

DPoP enforcement is configured per-platform by your Tango account team. It determines whether proofs are optional or required.

ModeBehavior
Opportunistic (default during migration)DPoP proofs are validated when present; requests without a DPoP header are still allowed. Use this to test your implementation before full enforcement.
StrictAll API requests must include a valid DPoP proof. Requests without a proof are rejected with dpop_required.

During migration, use opportunistic mode to validate your DPoP flow while non-DPoP clients continue to work. Coordinate the switch to strict mode with your Tango account team once all clients are migrated.

Error responses

When DPoP validation fails, the server returns a 401 Unauthorized with structured error information per RFC 9449 §7.2:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="<error_code>", error_description="<reason>"
  X-Pop-Decision-Stage: pop-validation

The following table outlines common errors and how to resolve them:

Error CodeCauseResolution
dpop_requiredNo proof provided in strict modeAdd the DPoP header with a valid proof JWT
missing_dpop_proofBound token sent without a proofInclude a DPoP header matching the bound key
malformed_proofProof is not a valid JWT or jwk header is missingVerify compact JWT serialization and include jwk in the header
missing_required_claimProof is missing jti, htm, htu, or iatEnsure all four claims are present in the payload
missing_athath claim missing from API call proofInclude ath: base64url(SHA-256(access_token)) in API proofs
invalid_signatureProof signature does not verify against embedded JWKEnsure signing key matches the jwk in the header
invalid_typtyp header is not dpop+jwtSet JWT typ to exactly dpop+jwt
htm_mismatchhtm ≠ actual HTTP methodEnsure htm matches the request method (case-insensitive)
htu_mismatchhtu ≠ actual request URIUse scheme + host + path only — no query string
iat_out_of_rangeiat is outside ±60 seconds of server timeSynchronize clock via NTP; generate proofs immediately before sending
ath_mismatchath ≠ SHA-256 of the access tokenRecompute ath from the exact access token string
cnf_jkt_mismatchProof signing key ≠ key bound in the tokenUse the same key pair that was used at token issuance
replayed_dpop_proofSame jti reused within 120 secondsGenerate a fresh UUID for every request

Migrate to DPoP

You can migrate from your existing integration to use DPoP instead of (or in addition to) standard OAuth behavior (Bearer tokens). With OAuth, tokens can potentially be reused if stolen, but with DPoP, tokens are tied to your app and therefore much more secure.

To migrate to DPoP:

  1. Generate a stable key pair for your application. Store the private key securely.
  2. Update your token request to include a DPoP proof in the DPoP header. The response will have token_type: "DPoP".
  3. Update all API calls:
    1. To use Authorization: DPoP {token} instead of Authorization: Bearer {token}
    2. To include a fresh DPoP header proof with every request (with ath)
  4. Test in opportunistic mode so nothing breaks yet. Your platform will validate proofs while still allowing non-DPoP requests.
  5. Turn in on fully: coordinate the activation of strict mode with your Tango account team once all clients have been migrated.

FAQs

Here are the most frequently questions asked:

Q: Can I reuse the same key pair across multiple tokens?

Yes. The key pair is long-lived (per session or application). You request new tokens bound to the same key. Only the DPoP proofs are single-use.

Q: What happens if my clock is out of sync?

Proofs with iat more than 60 seconds from the server's time are rejected with iat_out_of_range. Use NTP to keep clocks synchronized and generate proofs immediately before sending.

Q: Do I need a DPoP proof when refreshing a token?

Yes. When refreshing a token, send a DPoP proof to the token endpoint (htm=POST, htu=<token_endpoint>). The new token will be bound to the same key.

Q: What algorithms are supported?

ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512. EC (ES256) is recommended for the best balance of security and performance.

Q: Is the DPoP header required on every single API request?

Yes. Once strict mode is enabled for your platform, every API request must include a valid, fresh DPoP proof.



Suggested pseudocode

Here's a simplified example of how the flow works; an easy-to-read, language-like example showing the logic of the integration. It's not the exact code that you copy and run:

# 1. Generate key pair (once per session or application lifecycle)
private_key = generate_ec_key(P-256)

# 2. Acquire DPoP-bound token
token_proof = sign_dpop_jwt(
    private_key,
    htm="POST",
    htu="https://sandbox-auth.tangocard.com/oauth/token",
    jti=uuid(),
    iat=now()
    # Note: no 'ath' claim on token request proofs
)
access_token = request_token(auth_server, headers={"DPoP": token_proof})
# Response: { "access_token": "...", "token_type": "DPoP" }

# 3. Call API (generate a fresh proof per request, including 'ath')
api_proof = sign_dpop_jwt(
    private_key,
    htm="GET",
    htu="https://integration-api.tangocard.com/raas/v2/customers",
    ath=base64url(sha256(access_token)),
    jti=uuid(),
    iat=now()
)

response = http_get(
    "https://integration-api.tangocard.com/raas/v2/customers",
    headers={
        "Authorization": f"DPoP {access_token}",
        "DPoP": api_proof
    }
)

© 2026 Tango API are provided by Tango, a division of BHN, Inc.