# Recept: uw software koppelen voor FenoFin-gebruikers via OAuth 2.1

Voor softwareleveranciers: uw applicatie krijgt, met toestemming van een FenoFin-gebruiker,
toegang tot de administraties die die gebruiker aanwijst. De gebruiker hoeft geen sleutels uit
te wisselen en kan de toegang op elk moment intrekken.

- Dit document: `https://www.fenofin.nl/ontwikkelaars/oauth.md` (ruwe markdown, geen login)
- API-referentie (OpenAPI/ReDoc): `https://www.fenofin.nl/api/v1/docs/` — per operatie de
  vereiste scope, de verplichte headers en de mogelijke foutcodes
- OpenAPI-schema: `https://www.fenofin.nl/api/v1/schema/`

## Voor AI-assistenten

Dit document beschrijft de volledige OAuth-route. Kant-en-klare opdracht:

> Bouw met dit recept (https://www.fenofin.nl/ontwikkelaars/oauth.md) de OAuth-koppeling van
> **[applicatie]** met FenoFin: de autorisatie met PKCE S256, het inwisselen en vernieuwen van
> tokens met client_secret_basic, en de API-aanroepen uit §5. Lees client_id, client_secret en de
> callback-URL uit omgevingsvariabelen, bewaar tokens alleen op de server en behandel de foutcodes
> uit §8.

## 1. Eigen sleutel of OAuth: vier begrippen

| Begrip | Wat het is | Wie regelt het |
|---|---|---|
| **Eigen API-sleutel** | Voor software die u zelf op úw administraties aansluit: een test-sleutel voor de sandbox, een live-sleutel voor echte administraties. | U zelf, op **Mijn kantoor → Sandbox**. |
| **Leveranciersregistratie** | Voor software die veel FenoFin-gebruikers koppelen: uw applicatie krijgt een client_id en client_secret, een vaste lijst scopes en uw callback-URL(s). | FenoFin, na uw aanmelding via contact. |
| **Goedkeuring en schrijfvrijgave** | Alleen een goedgekeurde applicatie kan toestemming vragen. Schrijven in echte administraties vereist daarnaast een aparte vrijgave; tot dan geeft elke schrijfactie 403 `live_schrijven_niet_vrijgegeven`. | FenoFin. |
| **Toestemming per gebruiker** | De gebruiker kiest op het toestemmingsscherm welke administraties uw applicatie mag bereiken, met alleen de gevraagde rechten, en kan dat intrekken. | De gebruiker, onder **Mijn kantoor → Verleende API-toegang**. |

OAuth werkt alleen in de live-omgeving. Test uw koppellogica eerst met een eigen test-sleutel in
een sandbox (dezelfde endpoints, zie `https://www.fenofin.nl/ontwikkelaars/`); daarna gebruikt u
dezelfde aanroepen met een OAuth-token.

## 2. Aanmelden

Neem contact op met FenoFin en geef door: de naam van uw applicatie en uw bedrijf, een
contact-e-mailadres, uw website, de callback-URL(s) en de scopes die uw applicatie nodig heeft
(zo weinig mogelijk). Een callback-URL is altijd https. U ontvangt eenmalig:

- **client_id** — openbaar, staat in de autorisatielink;
- **client_secret** — geheim, alleen op uw server bewaren. FenoFin toont het één keer; kwijt?
  Neem contact op voor een nieuwe registratie.

Uw applicatie is een *confidential client*: bij het token-endpoint authenticeert zij zich met het
client_secret.

## 3. Toestemming vragen: `/o/authorize/`

Maak per poging een willekeurige `code_verifier` (43–128 tekens) en een `state`, en stuur de
gebruiker naar:

```
https://www.fenofin.nl/o/authorize/?response_type=code&client_id=<client_id>&redirect_uri=<callback-url>&scope=relaties:read%20relaties:write&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256
```

- `code_challenge` = base64url(SHA-256(`code_verifier`)) zonder `=`-opvulling; PKCE met **S256**
  is verplicht, ook voor een confidential client.
- `scope` is een deelverzameling van de scopes van uw registratie, gescheiden door een spatie.
- De gebruiker logt in (met tweestapsverificatie), ziet de naam van uw applicatie, de gevraagde
  rechten en het terugkeeradres, en kiest de administraties.

FenoFin stuurt de gebruiker terug naar uw callback met `?code=…&state=…`. Controleer dat
`state` gelijk is aan wat u meestuurde. Een autorisatiecode is **60 seconden** geldig en
eenmalig bruikbaar. Mogelijke fouten in de callback:

- `error=access_denied` — de gebruiker heeft geannuleerd;
- `error=invalid_scope` — een scope buiten uw registratie, of uw applicatie is (nog) niet
  goedgekeurd of geblokkeerd.

## 4. Code inwisselen: `/o/token/`

Het token-endpoint volgt OAuth: een **formulier** (`application/x-www-form-urlencoded`), niet de
JSON van de REST API. Authenticeer met HTTP Basic (`client_secret_basic`, de voorkeur); het
client_secret in de body (`client_secret_post`) werkt ook.

```bash
curl -X POST https://www.fenofin.nl/o/token/ \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "code_verifier=$CODE_VERIFIER"
```

Het antwoord bevat `access_token` (1 uur geldig, `expires_in: 3600`), `refresh_token` en
`scope`. De discovery-metadata op `/.well-known/oauth-authorization-server` noemt alleen de
methode `none`: die is voor de AI-connector (MCP-clients). Stel uw OAuth-bibliotheek daarom
expliciet in op `client_secret_basic`.

## 5. De API aanroepen

Stuur het token als Bearer. Controleer eerst welke administraties de gebruiker u gaf; het veld
`id` gebruikt u in de vervolgaanroepen.

```bash
curl https://www.fenofin.nl/api/v1/administraties/ \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Een schrijfactie stuurt JSON met `Content-Type: application/json` (een formulier geeft 415) en
een `Idempotency-Key`. Maak per handeling één key aan en bewaar hem; herhaalt u na een time-out
exact hetzelfde verzoek met dezelfde key, dan krijgt u het eerste antwoord terug
(`X-Idempotent-Replay: true`) en wordt niets dubbel geboekt. Bij OAuth hoort de key bij de
toestemming: hij blijft geldig als u het token vernieuwt.

```bash
KEY=$(uuidgen)
curl -X POST https://www.fenofin.nl/api/v1/administraties/$ADMINISTRATIE_ID/afnemers/ \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"naam": "Eerste Klant BV"}'
```

Schrijven vereist twee dingen: de schrijfvrijgave van uw applicatie door FenoFin én de
schrijfscope van het endpoint in de toestemming. `GET /api/v1/status/` toont met `mag_schrijven`
de vrijgave en met `scopes` de rechten.

## 6. Token vernieuwen

Het refresh-token roteert: elk vernieuwen geeft een nieuw refresh-token en het oude vervalt.
Bewaar dus altijd het nieuwste. Een refresh-token vervalt na 90 dagen zonder gebruik; daarna
doorloopt de gebruiker de toestemming opnieuw.

```bash
curl -X POST https://www.fenofin.nl/o/token/ \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d "refresh_token=$REFRESH_TOKEN"
```

## 7. Intrekken en blokkeren

- De gebruiker trekt de toestemming in onder **Mijn kantoor → Verleende API-toegang**. Het
  access-token werkt dan direct niet meer (401) en vernieuwen geeft 400 `invalid_grant`.
- Blokkeert FenoFin uw applicatie, dan geven bestaande tokens 401 en een nieuwe autorisatie
  `invalid_scope`.
- Uw applicatie kan zelf een token intrekken via `/o/revoke_token/`.

## 8. Fouten

De REST API geeft fouten volgens RFC 9457 (`application/problem+json`) met een stabiele `code`
op topniveau; stuur daarop. De OAuth-endpoints geven OAuth-fouten (`error`).

| Waar | Status en code | Betekenis en vervolg |
|---|---|---|
| callback | `access_denied` | De gebruiker annuleerde; vraag later opnieuw. |
| callback | `invalid_scope` | Scope buiten uw registratie, of applicatie niet (meer) goedgekeurd. |
| `/o/token/` | 400 `invalid_grant` | Code verlopen of al gebruikt, verkeerde `code_verifier` of `redirect_uri`, of refresh-token ingetrokken; start de autorisatie opnieuw. |
| `/o/token/` | 401 `invalid_client` | client_secret ontbreekt of klopt niet. |
| `/api/v1/` | 401 `authenticatie_ongeldig` | Token verlopen of ingetrokken; vernieuw het of vraag opnieuw toestemming. |
| `/api/v1/` | 403 `geen_toegang` | De toestemming of de API-toegang van de gebruiker is vervallen; vraag opnieuw toestemming. |
| `/api/v1/` | 403 `scope_ontbreekt` | Het endpoint eist een scope die de toestemming niet heeft. |
| `/api/v1/` | 403 `live_schrijven_niet_vrijgegeven` | FenoFin heeft schrijven voor uw applicatie nog niet vrijgegeven. |
| `/api/v1/` | 404 `niet_gevonden` | De administratie hoort niet bij de toestemming. |
| `/api/v1/` | 400 `ongeldige_json` / 415 `mediatype_niet_ondersteund` | De body is geen geldige JSON of mist `Content-Type: application/json`. |
| `/api/v1/` | 409 `idempotency_key_hergebruikt` | Dezelfde key met een andere body; controleer eerst of de eerste poging slaagde. |

De volledige lijst per operatie staat in de API-referentie.
