Koppelmij Implementation Guide
0.1.0 - ci-build
Koppelmij Implementation Guide - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
Deze walkthrough beschrijft hoe een PGO een module-launch voorbereidt door een launch_code op te halen bij de DVA. De flow is gebaseerd op Option 3a - Token Exchange Launch Token met Gebruikersidentificatie en gebruikt PAR (RFC 9126), Authorization Code flow met PKCE (RFC 6749 / RFC 7636) en Token Exchange (RFC 8693). Voorwaarde is dat het PGO in de verzamelfase al een DVA access_token (MEDMIJ_VERZAMELEN_TOKEN) heeft verkregen.
De PGO doorloopt vier stappen om een launch_code op te halen bij de DVA:
POST /par — Pushed Authorization Request met subject_token, PKCE en state (backchannel).GET /authorize — front-channel redirect met request_uri. De DVA zet de Module DVA Authentication cookie en redirect direct terug naar de PGO-callback (géén DigiD in dit pad).GET /callback_stepup — PGO ontvangt een code (opaque, kortlevende authorization code) en state.POST /token — Token Exchange waarbij de authorization code als actor_token wordt meegegeven en de response de launch_code bevat.MEDMIJ_VERZAMELEN_TOKEN, het DVA access_token uit de verzamelfase.client_id bij de DVA.redirect_uri https://pgo.example.nl/callback_stepup is geregistreerd bij de DVA.state (opaque, CSRF-bescherming);code_verifier + afgeleide code_challenge (method S256).De PGO duwt de authorization request naar de DVA voordat de browser wordt geredirect.
baseUrl, de DVA Authorization Server, bijvoorbeeld https://dva.example.nl.subjectToken, de MEDMIJ_VERZAMELEN_TOKEN uit de verzamelfase.moduleClientId, de client_id van de module die gelaunched wordt (audience).pgoClientId, de client_id van het PGO bij de DVA.redirectUri, bijvoorbeeld https://pgo.example.nl/callback_stepup.state, opaque anti-CSRF waarde.codeChallenge, PKCE code_challenge, methode S256.scope, door de DVA voorgeschreven scope-set. Typisch combinatie van SMART-on-FHIR scopes (bv. launch openid fhirUser patient/*.read); exacte waarde is per DVA configureerbaar.async function pushAuthorizationRequest(
baseUrl: string,
subjectToken: string,
moduleClientId: string,
pgoClientId: string,
redirectUri: string,
state: string,
codeChallenge: string,
scope: string,
): Promise<{ request_uri: string; expires_in: number }> {
const url = `${baseUrl}/par`;
const body = new URLSearchParams({
response_type: "code",
client_id: pgoClientId,
redirect_uri: redirectUri,
audience: moduleClientId,
subject_token: subjectToken,
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
acr_values: "urn:medmij:oauth2:acr-value:launch-authorization",
state: state,
scope: scope,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: body.toString(),
});
if (!resp.ok) {
throw new Error(`PAR failed: ${resp.status} ${resp.statusText}`);
}
return await resp.json();
}
201 Created met een JSON map die het request_uri bevat, te gebruiken in Stap 2. De expires_in geeft de geldigheid van dit request_uri in seconden.{
"request_uri": "urn:ietf:params:oauth:request_uri:example123",
"expires_in": 90
}
/authorize (front-channel)De PGO stuurt de browser naar de DVA /authorize met het request_uri uit Stap 1.
client_id, de PGO client_id (zelfde als in PAR).request_uri, de waarde uit de PAR-response.state, zelfde waarde als in PAR (optioneel — het request_uri bevat ook state, maar het meesturen vergemakkelijkt browser-side correlatie).GET /authorize?client_id={pgo-clientid}&request_uri=urn:ietf:params:oauth:request_uri:example123&state=example456 HTTP/1.1
Host: dva.example.nl
Wat er aan DVA-zijde gebeurt:
request_uri en correleert met de parameters uit Stap 1.MEDMIJ_VERZAMELEN_TOKEN uit Stap 1 fungeert als bewijs van eerdere authenticatie.sub (claim uit MEDMIJ_VERZAMELEN_TOKEN), client_id van de module, state en een kortlevende authorization code. TTL: 3 minuten.redirect_uri van het PGO met de authorization code.Het PGO ontvangt de authorization code op zijn redirect_uri.
code, opaque authorization code uitgegeven door de DVA (kortlevend, eenmalig bruikbaar in Stap 4).state, moet exact matchen met de state uit Stap 1.GET /callback_stepup?code={authorization-code}&state=example456 HTTP/1.1
Host: pgo.example.nl
state overeenkomt met de waarde die in Stap 1 is gegenereerd. Bij mismatch: flow afbreken.code met de bijbehorende PKCE code_verifier; die is nodig in Stap 4.launch_codeHet PGO wisselt de MEDMIJ_VERZAMELEN_TOKEN + authorization code in voor een launch_code via Token Exchange.
baseUrl, de DVA Authorization Server, bijvoorbeeld https://dva.example.nl.subjectToken, dezelfde MEDMIJ_VERZAMELEN_TOKEN als in Stap 1.authorizationCode, de code uit Stap 3.codeVerifier, de PKCE code_verifier die in Stap 1 als code_challenge is gecommitteerd.pgoClientId en moduleClientId, zelfde waarden als in Stap 1.resources, één of meer FHIR resource-referenties die de launch-context vormen (bijv. Task/456, Observation/789). De DVA valideert dat het PGO toegang heeft tot deze resources en neemt ze op in de launch-context. Zie Option 3a §Context meegeven.async function exchangeForLaunchCode(
baseUrl: string,
subjectToken: string,
authorizationCode: string,
codeVerifier: string,
pgoClientId: string,
moduleClientId: string,
resources: string[],
): Promise<{
access_token: string;
expires_in: number;
token_type: string;
issued_token_type: string;
}> {
const url = `${baseUrl}/token`;
const body = new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token: subjectToken,
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
actor_token: authorizationCode,
actor_token_type: "urn:medmij:token-type:launch-authorization-code",
requested_token_type: "urn:medmij:token-type:launch-code",
client_id: pgoClientId,
audience: moduleClientId,
code_verifier: codeVerifier,
});
// resource is multi-valued; URLSearchParams append respecteert dat bij serialisatie.
for (const resource of resources) {
body.append("resource", resource);
}
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: body.toString(),
});
if (!resp.ok) {
throw new Error(`Token Exchange failed: ${resp.status} ${resp.statusText}`);
}
return await resp.json();
}
200 OK met een JSON map. De access_token bevat de launch_code.Let op: de
launch_codeis géén access_token. Hij wordt conform RFC 8693 in het veldaccess_tokengeretourneerd, maar is bedoeld als opaque launch-code voor de SMART-on-FHIR launch van de module en kan niet gebruikt worden tegen een FHIR endpoint. Het veldtoken_typeis bewustN_A. Delaunch_codeis kortlevend (doorgaans 180 s) en éénmalig bruikbaar.
{
"access_token": "{launch_code}",
"expires_in": 180,
"token_type": "N_A",
"issued_token_type": "urn:medmij:token-type:launch-code"
}
Met de launch_code kan het PGO de module launchen via SMART-on-FHIR. Zie Het uitvoeren van de launch als PGO.
Openstaand: de gekozen waarde voor acr_values in Stap 1. In eerdere ontwerpversies werd urn:medmij:oauth2:acr-value:step-up gebruikt als marker voor de step-up flow. Nu step-up is komen te vervallen, is de huidige aanname urn:medmij:oauth2:acr-value:launch-authorization als marker voor "dit is een launch-voorbereiding, geen gewone verzamelen-authorize". Openstaande vragen:
acr_values überhaupt meegegeven worden, of is audience={module-clientid} + requested_token_type=launch-code in Stap 4 voldoende signaal voor de DVA?acr_values blijft: is urn:medmij:oauth2:acr-value:launch-authorization de juiste naamgeving, of hoort dit bij een andere MedMij-urn-namespace?