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
- Maak een app in de Developer Console: naam, website en terug-adressen.
- Bewaar de
client_iden het eenmalig getoondeclient_secretop je server. - Wijs je OIDC-bibliotheek naar het discovery-adres hieronder.
https://rout.be/.well-known/openid-configurationCode-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 #).
// 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();// 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
| Scope | Claims |
|---|---|
| openid | sub — vaste, unieke ROUT-ID |
| profile | name, preferred_username, picture; plus rout_handle als de app Universal Handle Sync aanzet |
email, email_verified | |
| linked_accounts | Gekoppelde sociale accounts die de gebruiker deelt |
| Rich Identity | rout_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 metrout_id = sub. - Rich Identity: overschrijf
rout_public_activitybij 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).
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.
{
"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"
}
}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-configuration | Discovery-document (CORS open) |
| GET | /.well-known/jwks.json | Publieke ES256-sleutels voor id_token (CORS open) |
| GET | /oauth/authorize | Toestemmingsscherm; start de code-flow |
| POST | /api/public/oauth/token | Code inwisselen (PKCE S256 verplicht) |
| GET | /api/public/oauth/userinfo | Claims met Bearer access_token |
QR- en link-API-sleutels beheer je in de console.
Foutcodes en limieten
| invalid_request | 400 | Verplichte velden ontbreken of zijn ongeldig. |
| invalid_client | 401 | Onbekende app, fout geheim of IP niet toegelaten. |
| invalid_grant | 400 | Code verlopen, al gebruikt, fout redirect_uri of PKCE klopt niet. |
| unsupported_grant_type | 400 | Alleen authorization_code wordt ondersteund. |
| access_denied | — | De gebruiker weigerde toestemming (komt terug als ?error=). |
| rate_limited | 429 | Te 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.