# Verify a user owns a domain (DomainAttest)

If you run a marketplace, or any service that needs **instant proof a Porkbun user controls a domain** — with no TXT records, no DNS propagation, and no stale verifications — Porkbun is a [DomainAttest](https://domainattest.org) issuer. The user logs into Porkbun, consents, and you receive a short-lived, registrar-signed proof you can verify in milliseconds.

> **This is not the API-key JSON API.** DomainAttest is a separate, standards-based flow: OAuth 2.0 authorization code + PKCE, with the result delivered as an ES256-signed JWT. There is no `apikey`/`secretapikey` here — you act as a **relying party (RP)**, and the domain owner authenticates in their own browser. Full spec: https://github.com/domainattest/spec

## 1. Register as a relying party (one time)

DomainAttest runs in `direct` mode: contact Porkbun support to register your **`client_id`** and the exact **`redirect_uri`(s)** you'll use. They're allowlisted server-side and matched exactly (no wildcards), so a leaked authorization code can't be redirected anywhere you didn't pre-register.

## 2. Discover the issuer

```bash
curl https://porkbun.com/.well-known/domainattest-configuration
```

```json
{
  "issuer": "https://porkbun.com",
  "authorization_endpoint": "https://porkbun.com/domainattest/authorize",
  "token_endpoint": "https://porkbun.com/domainattest/token",
  "jwks_uri": "https://porkbun.com/domainattest/jwks",
  "attest_versions_supported": ["1"],
  "modes_supported": ["direct"],
  "code_challenge_methods_supported": ["S256"],
  "attestation_signing_alg_values_supported": ["ES256"]
}
```

## 3. Send the user to authorize (PKCE)

Generate a PKCE pair, then redirect the user's browser to the `authorization_endpoint`:

```
GET https://porkbun.com/domainattest/authorize
  ?response_type=code
  &client_id=<your-client-id>
  &redirect_uri=<your-registered-callback>
  &domain=<domain-to-verify>          # lowercase, punycode for IDNs
  &code_challenge=<base64url(SHA256(verifier))>
  &code_challenge_method=S256         # S256 only
  &state=<opaque-csrf-token>
```

The user logs into Porkbun (with their own 2FA) and confirms ownership on Porkbun's consent screen. Porkbun then redirects the browser back to your `redirect_uri` with `?code=…&state=…` (or `?error=…` if they decline). Verify `state`, and note the `code` is single-use and expires within ~60 seconds.

## 4. Exchange the code for an attestation

Server-to-server, present the code plus your PKCE verifier:

```bash
curl -X POST https://porkbun.com/domainattest/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=<code-from-redirect>' \
  -d 'code_verifier=<your-pkce-verifier>' \
  -d 'client_id=<your-client-id>' \
  -d 'redirect_uri=<your-registered-callback>'
```

Response: `{ "attestation": "<compact-JWS>", "token_type": "domainattest" }`. (Send a normal `User-Agent` header — empty-UA requests may be blocked.)

## 5. Verify the attestation

Validate the JWS signature against the published JWKS and check the claims. The signature proves it came from Porkbun; the claims tell you what was attested. Using [`jose`](https://github.com/panva/jose):

```javascript
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://porkbun.com/domainattest/jwks'));

const { payload } = await jwtVerify(attestation, JWKS, {
  issuer:   'https://porkbun.com',   // must equal the discovered issuer
  audience: '<your-client-id>',      // attestation is bound to you only
});

if (payload.sub !== requestedDomain) throw new Error('domain mismatch');
// Also reject if payload.jti was already seen within its exp window (replay).
```

Claims you'll receive:

| Claim | Meaning |
|---|---|
| `iss` | Porkbun's issuer URL — must match the one you discovered |
| `sub` | the verified domain (lowercase, punycode) |
| `aud` | your `client_id` — the attestation is valid for you and no one else |
| `iat` / `exp` | issued-at / expiry (Porkbun issues ~5-minute tokens; spec max is 15) |
| `jti` | unique id — track it within the validity window to reject replays |
| `attest_ver` | `"1"` |
| `attest_iana_id` | Porkbun's IANA registrar id (`1861`) |

An attestation proves **control, not identity** — it carries no registrant name, email, address, or any WHOIS data. Because it's short-lived, there's nothing to revoke: to re-confirm control later, just run the flow again.

## Related

- DomainAttest spec & protocol: https://domainattest.org · https://github.com/domainattest/spec
- Issuer discovery: https://porkbun.com/.well-known/domainattest-configuration
- Public keys (JWKS): https://porkbun.com/domainattest/jwks


---

## More

- Guides (how-tos): https://porkbun.com/llms/guides
- Topic index: https://porkbun.com/llms
- Full reference (one file): https://porkbun.com/llms-full.txt
- OpenAPI spec (full schemas): https://porkbun.com/api/json/v3/spec
- Short overview: https://porkbun.com/llms.txt
- Official MCP server: https://porkbun.com/mcp (`npx -y @porkbunllc/mcp-server`)
- Create API keys: https://porkbun.com/account/api
