# Authenticatie

> Authenticatie met de Tidal Control API. De OAuth 2.0 Device Authorization Grant voor scripts en CLI-tools, plus read-only serviceaccounts voor automatisering.

Source: https://tidalcontrol.com/nl/docs/api/authentication

De Tidal Control API gebruikt **Bearer-tokens** voor authenticatie. Tokens worden uitgegeven door de Keycloak-realm van je tenant via de **OAuth 2.0 Device Authorization Grant** (RFC 8628). Deze flow is ontworpen voor scripts en CLI-tools die zelf geen browservenster kunnen openen, en het token handelt namens jou, met jouw rechten.

Draait je integratie onbeheerd (een geplande export naar een BI-tool, een server-naar-server-synchronisatie), gebruik dan een read-only **serviceaccount**. Zie **Machine-to-machine-authenticatie** onderaan deze pagina.

## Vereisten

Je hebt je **tenantnaam** nodig: de Keycloak-realm-identifier van je organisatie. Dit is de organisatie-identifier in je portal-URL:

```
https://portal.tidalcontrol.com/{jouw-tenant}/
```

Als je portal-URL bijvoorbeeld `https://portal.tidalcontrol.com/demo/` is, is je tenantnaam `demo`.

Vervang in deze handleiding `{jouw-tenant}` door je eigen tenantnaam.

## De authenticatiestroom

De Device Authorization Grant werkt in drie stappen:

1. Je script vraagt een apparaatcode op bij Keycloak
2. Jij (of de gebruiker van het script) logt in via een browser-URL
3. Je script pollt op het toegangstoken zodra de inlog is voltooid

### Stap 1: Apparaatcode opvragen

```bash
curl -X POST \
  "https://auth.tidalcontrol.com/realms/{jouw-tenant}/protocol/openid-connect/auth/device" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=portal&scope=openid"
```

Antwoord:

```json
{
  "device_code": "...",
  "user_code": "ABCD-1234",
  "verification_uri": "https://auth.tidalcontrol.com/realms/{jouw-tenant}/device",
  "verification_uri_complete": "https://auth.tidalcontrol.com/realms/{jouw-tenant}/device?user_code=ABCD-1234",
  "expires_in": 600,
  "interval": 5
}
```

### Stap 2: Inloggen via browser

Open `verification_uri_complete` in een browser en log in met je Tidal Control-gegevens. Je script wacht terwijl je dit doet.

### Stap 3: Pollen op het token

Poll het token-endpoint elke `interval` seconden totdat de inlog is voltooid:

```bash
curl -X POST \
  "https://auth.tidalcontrol.com/realms/{jouw-tenant}/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=portal&device_code={device_code}"
```

Zodra de gebruiker is ingelogd, bevat het antwoord je toegangstoken:

```json
{
  "access_token": "eyJhbGci...",
  "expires_in": 300,
  "refresh_token": "eyJhbGci...",
  "refresh_expires_in": 1800,
  "token_type": "Bearer"
}
```

> **info:**

Toegangstokens verlopen na **5 minuten**. Gebruik de `refresh_token` om een nieuw toegangstoken op te halen zonder opnieuw in te loggen (zie hieronder).

## API-verzoeken versturen

Voeg het toegangstoken toe als `Bearer`-header aan elk GraphQL-verzoek:

```bash
curl -X POST https://portal.tidalcontrol.com/graphql \
  -H "Authorization: Bearer {toegangstoken}" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ tenant { name displayName } }"}'
```

## Volledig Python-voorbeeld

Dit script doorloopt de volledige authenticatiestroom en hergebruikt het token:

```python

TENANT = "{jouw-tenant}"
BASE_URL = f"https://auth.tidalcontrol.com/realms/{TENANT}/protocol/openid-connect"
CLIENT_ID = "portal"
GRAPHQL_URL = "https://portal.tidalcontrol.com/graphql"

def get_token():
    # Stap 1: Apparaatcode opvragen
    r = requests.post(
        f"{BASE_URL}/auth/device",
        data={"client_id": CLIENT_ID, "scope": "openid"},
    )
    r.raise_for_status()
    device = r.json()

    print(f"\nOpen deze URL in je browser om in te loggen:")
    print(f"  {device['verification_uri_complete']}\n")

    # Stap 2: Pollen op token
    interval = device.get("interval", 5)
    deadline = time.time() + device["expires_in"]

    while time.time() < deadline:
        time.sleep(interval)
        r = requests.post(
            f"{BASE_URL}/token",
            data={
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                "client_id": CLIENT_ID,
                "device_code": device["device_code"],
            },
        )
        if r.status_code == 200:
            print("Authenticatie geslaagd.")
            return r.json()
        body = r.json()
        if body.get("error") == "authorization_pending":
            continue
        if body.get("error") == "slow_down":
            interval += 5
            continue
        r.raise_for_status()

    raise RuntimeError("Authenticatie verlopen.")

def refresh_token(refresh_tok):
    r = requests.post(
        f"{BASE_URL}/token",
        data={
            "grant_type": "refresh_token",
            "client_id": CLIENT_ID,
            "refresh_token": refresh_tok,
        },
    )
    r.raise_for_status()
    return r.json()

def graphql(query, variables, access_token):
    r = requests.post(
        GRAPHQL_URL,
        json={"query": query, "variables": variables},
        headers={"Authorization": f"Bearer {access_token}"},
    )
    r.raise_for_status()
    result = r.json()
    if "errors" in result:
        raise RuntimeError(result["errors"])
    return result["data"]

if __name__ == "__main__":
    token = get_token()
    data = graphql("{ tenant { name displayName } }", {}, token["access_token"])
    print(json.dumps(data, indent=2))
```

## Token vernieuwen

Wanneer het toegangstoken verloopt, gebruik je het refresh-token in plaats van de volledige inlogstroom opnieuw te doorlopen:

```python
token = refresh_token(token["refresh_token"])
access_token = token["access_token"]
```

> **tip:**

Refresh-tokens verlopen ook (doorgaans na 30 minuten inactiviteit). Als een refresh mislukt, doorloop dan opnieuw de volledige `get_token()`-stroom.

## Machine-to-machine-authenticatie

Voor onbeheerde integraties die zonder iemand erbij draaien (een nachtelijke export naar een BI-tool, een geplande server-naar-server-synchronisatie), gebruik je een **serviceaccount** in plaats van de device flow. Een serviceaccount is een OAuth 2.0 **Client Credentials**-client: een `client_id` en `client_secret` die je script rechtstreeks inwisselt voor een toegangstoken, zonder browserstap.

Serviceaccounts zijn **read-only**. Ze kunnen queries uitvoeren, maar geen mutaties.

> **info:**

Serviceaccounts worden per organisatie ingeschakeld. Zie je de pagina **Serviceaccounts** (tot oktober 2026 **API-inloggegevens** genoemd) niet in je instellingen, mail dan support@tidalcontrol.com om deze te laten inschakelen.

### Een serviceaccount aanmaken

Voor het aanmaken van serviceaccounts heb je de rol **Super User** nodig.

1. Ga naar **Instellingen → Serviceaccounts**.
2. Klik op **Serviceaccount aanmaken**, geef het een label en stel eventueel een vervaldatum in.
3. Kopieer de **client secret** direct. Deze wordt **één keer** getoond en kan daarna niet meer worden opgehaald. Bewaar de secret in een secret manager of vault, nooit in platte tekst in je configuratie.

Je beschikt nu over een `client_id` en een `client_secret`. Samen vormen deze de inloggegevens van je serviceaccount.

### Een token opvragen

Wissel de client-ID en secret in voor een toegangstoken via de `client_credentials`-grant. Er is geen browserstap en geen `scope`-parameter:

```bash
curl -X POST \
  "https://auth.tidalcontrol.com/realms/{jouw-tenant}/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id={client_id}" \
  -d "client_secret={client_secret}"
```

Antwoord:

```json
{
  "access_token": "eyJhbGci...",
  "expires_in": 300,
  "token_type": "Bearer"
}
```

> **info:**

Deze flow heeft **geen refresh-token**. Wanneer het toegangstoken verloopt (na ongeveer 5 minuten), vraag je een nieuw token op met dezelfde `client_credentials`-aanroep.

Voeg het toegangstoken aan elk GraphQL-verzoek toe als `Bearer`-header, precies zoals bij **API-verzoeken versturen** hierboven.

### Volledig shell-voorbeeld

Zet `CLIENT_SECRET` op de secret die je hebt gekopieerd, vraag vervolgens een token op en roep de API aan:

```bash
export CLIENT_SECRET='<de gekopieerde secret>'

ACCESS_TOKEN=$(curl -s -X POST \
  "https://auth.tidalcontrol.com/realms/{jouw-tenant}/protocol/openid-connect/token" \
  -d "grant_type=client_credentials" \
  -d "client_id={client_id}" \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r .access_token)

curl -s https://portal.tidalcontrol.com/graphql \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ currentTenant { name } }"}'
```

### Wat een serviceaccount kan lezen

Een read-only serviceaccount kan de **volledige dataset** van je organisatie lezen: maatregelen, bewijs, personeel, KPI's, rapporten en de gebruikerslijst (namen en e-mailadressen). Behandel een gelekte secret als volledige blootstelling van je organisatiedata en roteer deze direct.

### Roteren en intrekken

Gebruik op **Instellingen → Serviceaccounts** de rijacties (kebabmenu, drie verticale puntjes) om te:

- **Geheim roteren**: een nieuwe secret genereren en de oude ongeldig maken. Werk je integratie bij met de nieuwe secret.
- **Intrekken**: het serviceaccount permanent uitschakelen.

> **tip:**

Toegangstokens blijven geldig tot ze verlopen (ongeveer 5 minuten). Een rotatie, intrekking of verloop wordt dus binnen één tokenlevensduur van kracht. Houd rekening met een venster van maximaal 5 minuten voordat de wijziging volledig is doorgevoerd.
