AVnesterDevelopers
← Home

OAuth 2.1 walkthrough

OAuth + DCR + DPoP in three minutes

Anonymous calls work without any of this. OAuth 2.1 is the path to higher rate budget + request attribution — and it’s self-serve via Dynamic Client Registration (RFC 7591).

Step 1

Register a client via DCR (RFC 7591)

POST your client metadata to the registration endpoint. No human approval, no email thread. You get a client_id instantly. Use token_endpoint_auth_method: "none" — OAuth 2.1 + PKCE doesn’t need a client secret.

# register the client and get back a client_id
curl -X POST https://auth.avnester.com/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{
       "client_name": "My LLM Agent",
       "redirect_uris": ["https://my-app.example.com/callback"],
       "grant_types": ["authorization_code", "refresh_token"],
       "response_types": ["code"],
       "token_endpoint_auth_method": "none"
     }'
Response
{
  "client_id": "av_dcr_8f3d2c19a4b6e7d5",
  "client_id_issued_at": 1716700800,
  "registration_access_token": "...",
  "registration_client_uri": "https://auth.avnester.com/oauth/register/av_dcr_8f3d2c19a4b6e7d5",
  "grant_types": ["authorization_code", "refresh_token"],
  "redirect_uris": ["https://my-app.example.com/callback"],
  "token_endpoint_auth_method": "none"
}

Step 2

Run the authorization-code-with-PKCE flow

  1. Generate a PKCE code_verifier (~64 random URL-safe chars) and the matching code_challenge = SHA-256(verifier).
  2. Redirect the user to https://auth.avnester.com/oauth/authorize?response_type=code&client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&scope=listings:read:public%20locality:read:public%20calculators:use:public
  3. After consent, the user is redirected to your redirect_uri with ?code=....
  4. Exchange that code at the token endpoint for an access token + refresh token. DPoP is required — see Step 3 for code.

Discovery metadata (issuer, endpoints, supported scopes, JWKS URI) is at https://auth.avnester.com/.well-known/oauth-authorization-server (RFC 8414).

Step 3

Generate DPoP proofs and call tools

DPoP (RFC 9449) binds the access token to a keypair you generate. Every request carries a fresh DPoP proof JWT signed with that key — a stolen access token can’t be replayed from a different machine. Reuse the same keypair across requests; rotate annually.

Node.js (jose)

import { generateKeyPair, exportJWK, SignJWT } from 'jose';
import crypto from 'node:crypto';

// 1. Generate a DPoP keypair once per client (persist across requests).
const { privateKey, publicKey } = await generateKeyPair('ES256');
const publicJwk = await exportJWK(publicKey);

// 2. Build a DPoP proof JWT for each request.
async function dpopProof(method, url, accessToken) {
  const headers = {
    alg: 'ES256',
    typ: 'dpop+jwt',
    jwk: publicJwk,
  };
  const payload = {
    htm: method,
    htu: url,
    iat: Math.floor(Date.now() / 1000),
    jti: crypto.randomUUID(),
  };
  if (accessToken) {
    payload.ath = crypto
      .createHash('sha256')
      .update(accessToken)
      .digest('base64url');
  }
  return await new SignJWT(payload)
    .setProtectedHeader(headers)
    .sign(privateKey);
}

// 3. Token exchange — DPoP header on the token endpoint too.
const url = 'https://auth.avnester.com/oauth/token';
const proof = await dpopProof('POST', url);
const res = await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'DPoP': proof,
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: AUTH_CODE,
    code_verifier: PKCE_VERIFIER,
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
  }),
});
const tokens = await res.json(); // { access_token, refresh_token, ... }

// 4. Call a tool — fresh DPoP proof per request, bound to the access token.
const toolUrl = 'https://api.avnester.com/public/v1/search_properties';
const toolProof = await dpopProof('POST', toolUrl, tokens.access_token);
await fetch(toolUrl, {
  method: 'POST',
  headers: {
    'Authorization': `DPoP ${tokens.access_token}`,
    'DPoP': toolProof,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ city: 'Coimbatore' }),
});

Python (Authlib + cryptography)

from authlib.jose import jwt
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
import hashlib, base64, time, uuid, requests

# 1. Generate DPoP keypair once.
private_key = ec.generate_private_key(ec.SECP256R1())
public_jwk = {  # serialize public key as JWK
    "kty": "EC", "crv": "P-256",
    "x": base64.urlsafe_b64encode(
        private_key.public_key().public_numbers().x.to_bytes(32, "big")
    ).rstrip(b"=").decode(),
    "y": base64.urlsafe_b64encode(
        private_key.public_key().public_numbers().y.to_bytes(32, "big")
    ).rstrip(b"=").decode(),
}

# 2. Build a DPoP proof per request.
def dpop_proof(method, url, access_token=None):
    payload = {
        "htm": method, "htu": url,
        "iat": int(time.time()), "jti": str(uuid.uuid4()),
    }
    if access_token:
        ath = hashlib.sha256(access_token.encode()).digest()
        payload["ath"] = base64.urlsafe_b64encode(ath).rstrip(b"=").decode()
    header = {"alg": "ES256", "typ": "dpop+jwt", "jwk": public_jwk}
    return jwt.encode(header, payload, private_key).decode()

# 3. Token exchange.
token_url = "https://auth.avnester.com/oauth/token"
r = requests.post(
    token_url,
    headers={"DPoP": dpop_proof("POST", token_url)},
    data={
        "grant_type": "authorization_code",
        "code": AUTH_CODE, "code_verifier": PKCE_VERIFIER,
        "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
    },
)
tokens = r.json()

# 4. Call a tool.
tool_url = "https://api.avnester.com/public/v1/search_properties"
requests.post(
    tool_url,
    headers={
        "Authorization": f"DPoP {tokens['access_token']}",
        "DPoP": dpop_proof("POST", tool_url, tokens["access_token"]),
    },
    json={"city": "Coimbatore"},
)

Go (sketch)

// Sketch only — see github.com/lestrrat-go/jwx for production-grade DPoP.
package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "encoding/json"
    "net/http"
    "strings"
)

// 1. Generate ES256 keypair once.
privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

// 2. Build dpop_proof(method, url, accessToken) returning a JWT signed with privKey.
//    Use github.com/lestrrat-go/jwx/jwt + jws.

// 3. Token exchange.
form := strings.NewReader(`grant_type=authorization_code&code=...&code_verifier=...&client_id=...`)
req, _ := http.NewRequest("POST", "https://auth.avnester.com/oauth/token", form)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("DPoP", dpopProof("POST", req.URL.String(), ""))
resp, _ := http.DefaultClient.Do(req)
var tokens map[string]any
json.NewDecoder(resp.Body).Decode(&tokens)

// 4. Tool call.
toolReq, _ := http.NewRequest("POST", "https://api.avnester.com/public/v1/search_properties",
    strings.NewReader(`{"city":"Coimbatore"}`))
toolReq.Header.Set("Content-Type", "application/json")
toolReq.Header.Set("Authorization", "DPoP "+tokens["access_token"].(string))
toolReq.Header.Set("DPoP", dpopProof("POST", toolReq.URL.String(), tokens["access_token"].(string)))
http.DefaultClient.Do(toolReq)

What OAuth gets you

The 10× tier (and what's coming)

  • 10× per-tool rate budget — 300–600 requests/min depending on the tool (the per-IP aggregate cap is removed).
  • Request attribution — your client_id appears in our abuse logs so we can reach out before rate-limiting you for spikes.
  • Future: usage analytics — per-client tool usage breakdown lands with the v3 dashboard.
  • Future: Business tier upgrade — when v4 ships, your OAuth client is what gets upgraded. No code changes; the next access token after upgrade carries tier: "business" automatically.

Further reading