ROUT

    ROUT Developers

    Login met ROUT

    Een standaard OpenID Connect-provider met PKCE, geverifieerde e-mail en optionele badges. Geen SDK nodig: elke OIDC-bibliotheek werkt.

    Snel starten

    1. Maak een app in de Developer Console: naam, website en terug-adressen.
    2. Bewaar de client_id en het eenmalig getoonde client_secret op je server.
    3. Wijs je OIDC-bibliotheek naar het discovery-adres hieronder.
    text
    https://rout.be/.well-known/openid-configuration

    Code-flow met PKCE (S256)

    PKCE is altijd verplicht, ook voor apps met een geheim. Terug-adressen moeten letterlijk overeenkomen met wat je registreerde (https, http enkel voor localhost, geen * en geen #).

    javascript
    // 1. Discovery ophalen (nooit endpoints hardcoden)
    const oidc = await fetch("https://rout.be/.well-known/openid-configuration").then((r) => r.json());
    
    // 2. Per poging: verifier, state en nonce — bewaar ze in een HttpOnly-cookie
    const b64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)))
      .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
    const verifier = b64(crypto.getRandomValues(new Uint8Array(32)));
    const challenge = b64(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
    const state = crypto.randomUUID();
    const nonce = crypto.randomUUID();
    
    // 3. Doorsturen
    const url = new URL(oidc.authorization_endpoint);
    url.search = new URLSearchParams({
      response_type: "code",
      client_id: process.env.ROUT_CLIENT_ID,
      redirect_uri: "https://jouwapp.be/auth/callback/rout",
      scope: "openid profile email",
      code_challenge: challenge,
      code_challenge_method: "S256",
      state, nonce,
    }).toString();
    javascript
    // Op je server, in /auth/callback/rout — na controle van state
    const res = await fetch(oidc.token_endpoint, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code, redirect_uri, code_verifier: verifier,
        client_id: process.env.ROUT_CLIENT_ID,
        client_secret: process.env.ROUT_CLIENT_SECRET, // nooit in de browser
      }),
    });
    const { id_token, access_token } = await res.json();
    // Controleer id_token via jwks_uri (ES256): iss, aud === client_id, exp en nonce.

    Codes zijn eenmalig en kortlevend. Het geheim stuur je mee als client_secret_post of via HTTP Basic; single-page en mobiele apps gebruiken geen geheim.

    Scopes en claims

    ScopeClaims
    openidsub — vaste, unieke ROUT-ID
    profilename, preferred_username, picture; plus rout_handle als de app Universal Handle Sync aanzet
    emailemail, email_verified
    linked_accountsGekoppelde sociale accounts die de gebruiker deelt
    Rich Identityrout_public_activity (lijst van kind, title, occurred_at) in het id_token — alleen als de app dit aanzet én de gebruiker het vakje aanvinkt

    Je eigen app bouwen

    Aanbevolen opbouw voor een app die "Login met ROUT" gebruikt:

    • Knop: het officiële ROUT-logo met lokale tekst ("Login met ROUT", "Se connecter avec ROUT", "Mit ROUT anmelden").
    • Popup of doorsturen: open een gecentreerde popup (500 × 650). Geblokkeerd? Stuur dan gewoon door; verifier, state en nonce blijven in een HttpOnly-, Secure-, SameSite=Lax-cookie.
    • Callback op /auth/callback/rout: controleer state, wissel de code in op de server, controleer het id_token (ES256 via JWKS; iss, aud, exp, nonce).
    • Sessie: maak je eigen sessiecookie. Tokens komen nooit in browser-JavaScript.
    • Koppelen: bestaat er al een gebruiker met hetzelfde geverifieerde e-mailadres, koppel ROUT dan aan dat account (in user_identities) in plaats van een dubbel account te maken. Anders: nieuw account met rout_id = sub.
    • Rich Identity: overschrijf rout_public_activity bij elke login vanuit het id_token en wis het als de claim ontbreekt. Vertrouw nooit invoer uit de browser.
    • Fouten: vertaal ?error= naar een vriendelijke melding (zie foutcodes).
    sql
    alter table users add column if not exists rout_id text unique;
    alter table users add column if not exists email_verified boolean not null default false;
    alter table users add column if not exists is_verified_person boolean not null default false;
    alter table users add column if not exists is_verified_business boolean not null default false;
    alter table users add column if not exists is_influencer boolean not null default false;
    
    create table if not exists user_identities (
      user_id uuid not null references users(id) on delete cascade,
      provider text not null,          -- 'rout'
      subject text not null,           -- sub-claim
      primary key (provider, subject)
    );

    Universal Handle Sync en webhooks

    Zet Universal Handle Sync aan in de console om ROUT de bron van gebruikersnamen te maken. Met de scope profile krijg je dan de claim rout_handle (in het id_token en via userinfo). Gebruik die als unieke username, maar koppel gebruikers altijd op sub: een handle kan veranderen.

    Verandert een lid zijn handle, dan stuurt ROUT een ondertekende POST user.handle.updated naar je webhook (voorbeeld; levering wordt binnenkort geactiveerd). Controleer de header x-rout-signature: t=…,v1=… (HMAC-SHA256 over t.body, maximaal 5 minuten oud) en sla verwerkte event-id's op.

    json
    {
      "id": "evt_01J9Z8K3QH4M2N",
      "type": "user.handle.updated",
      "created_at": "2026-10-10T10:40:00Z",
      "client_id": "rout_your_client_id",
      "data": {
        "sub": "8f0c2a4e-…",
        "previous_handle": "oldname",
        "rout_handle": "newname"
      }
    }
    javascript
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export function verifyRoutSignature(rawBody: string, header: string, secret: string) {
      const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
      const t = Number(parts.t);
      if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay window 5 min
      const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
      const got = Buffer.from(parts.v1 ?? "", "hex");
      return got.length === 32 && timingSafeEqual(got, Buffer.from(expected, "hex"));
    }

    Hoe verificaties werken

    Een badge betekent dat ROUT zelf iets heeft gecontroleerd — nooit enkel wat iemand intypte.

    Google / YouTube

    OAuth bij Google. ROUT krijgt een door Google bevestigd e-mailadres en, voor YouTube, het kanaal dat aan dat account hangt.

    GitHub

    OAuth bij GitHub. Bewijst dat de gebruiker die GitHub-account beheert; e-mail telt alleen als GitHub ze als geverifieerd markeert.

    GitLab

    OAuth/OIDC bij gitlab.com of een eigen GitLab-server. Zelfde regel: alleen bevestigde e-mail telt.

    Bluesky / AT Protocol

    OAuth bij de eigen PDS van de gebruiker. ROUT bewaart de DID (vaste identiteit), niet enkel de handle. Een eigen domein kan via /.well-known/atproto-did.

    Mastodon / fediverse

    OAuth bij de gekozen server. Omdat die meestal geen e-mail meegeeft, bevestigt de gebruiker zijn e-mail met een eenmalige code van 6 cijfers.

    Persoon, bedrijf, influencer

    Handmatig nagekeken door ROUT. Namen komen uit een goedgekeurde lijst; de gebruiker kiest er precies één.

    API-overzicht

    GET/.well-known/openid-configurationDiscovery-document (CORS open)
    GET/.well-known/jwks.jsonPublieke ES256-sleutels voor id_token (CORS open)
    GET/oauth/authorizeToestemmingsscherm; start de code-flow
    POST/api/public/oauth/tokenCode inwisselen (PKCE S256 verplicht)
    GET/api/public/oauth/userinfoClaims met Bearer access_token

    QR- en link-API-sleutels beheer je in de console.

    Foutcodes en limieten

    invalid_request400Verplichte velden ontbreken of zijn ongeldig.
    invalid_client401Onbekende app, fout geheim of IP niet toegelaten.
    invalid_grant400Code verlopen, al gebruikt, fout redirect_uri of PKCE klopt niet.
    unsupported_grant_type400Alleen authorization_code wordt ondersteund.
    access_denied—De gebruiker weigerde toestemming (komt terug als ?error=).
    rate_limited429Te veel pogingen. Wacht het aantal seconden uit Retry-After.
    • Token-uitwisseling: 30 per minuut per IP.
    • Aanmelden en wachtwoordherstel: 10 per minuut per IP.
    • Nieuwe apps: 5 per uur per account.
    • Sms-codes: 5 per uur per account, 3 per nummer.