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.
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"
}'{
"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
- Generate a PKCE
code_verifier(~64 random URL-safe chars) and the matchingcode_challenge = SHA-256(verifier). - 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 - After consent, the user is redirected to your
redirect_uriwith?code=.... - 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_idappears 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.