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 module een PGO-launch ontvangt en doorloopt tot FHIR resource-toegang. De module fungeert hier als standaard SMART App Launch client tegen de DVA als Authorization Server en Resource Server. Voorwaarde is dat de PGO de module heeft gelauncht zoals beschreven in Uitvoeren van een launch als PGO.
De module doorloopt vijf stappen:
GET /launch?iss=&launch=) en haal de SMART configuration op bij de iss./authorize bij de DVA met de launch-parameter, PKCE en state.redirect_uri.client_id, client_secret, redirect_uri).https://module.example.nl/launch).state (anti-CSRF, gebonden aan sessie) en een PKCE code_verifier + code_challenge (methode S256).De module ontvangt een SMART standalone/EHR launch request met iss en launch als query-parameters. Met de iss wordt de SMART configuration opgehaald; daaruit komen de authorization_endpoint en token_endpoint van de DVA.
iss, URL-gecodeerde DVA FHIR base URL, bijvoorbeeld https://dva.example.nl/fhir.launch, de launch_code die het PGO via Token Exchange heeft verkregen.De binnenkomende request op de module:
GET /launch?iss=https%3A%2F%2Fdva.example.nl%2Ffhir&launch={launch_code} HTTP/1.1
Host: module.example.nl
type SmartConfiguration = {
authorization_endpoint: string;
token_endpoint: string;
capabilities: string[];
code_challenge_methods_supported?: string[];
scopes_supported?: string[];
};
async function getSmartConfiguration(iss: string): Promise<SmartConfiguration> {
const url = `${iss}/.well-known/smart-configuration`;
const resp = await fetch(url, {
headers: { Accept: "application/json" },
});
if (!resp.ok) {
throw new Error(`SMART configuration fetch failed: ${resp.status}`);
}
return resp.json();
}
authorization_endpoint en token_endpoint, gebruikt in Stap 2 en Stap 4.{
"authorization_endpoint": "https://dva.example.nl/authorize",
"token_endpoint": "https://dva.example.nl/token",
"capabilities": [
"launch-standalone",
"client-confidential-symmetric",
"context-standalone-patient",
"permission-patient"
],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["launch", "openid", "fhirUser", "patient/*.read"]
}
/authorizeDe module redirect de browser naar het authorization_endpoint met de ontvangen launch-parameter, PKCE en state. De module slaat state, code_verifier en iss server-side op, gekoppeld aan de browser-sessie.
authorizationEndpoint, uit Stap 1.clientId, de client_id van de module bij de DVA.redirectUri, de geregistreerde module callback URL.scope, de gewenste scopes. Typisch launch openid fhirUser patient/*.read. De openid/fhirUser-combinatie is een bilaterale afspraak — zie DVA als Identity Provider en de Discussie.state, opaque CSRF-waarde.launch, de launch_code uit Stap 1.codeChallenge, PKCE code_challenge (method S256).aud, de DVA FHIR base URL (iss uit Stap 1). SMART vereist dat de module de beoogde audience expliciet meestuurt.function buildAuthorizeRedirect(
authorizationEndpoint: string,
clientId: string,
redirectUri: string,
scope: string,
state: string,
launchCode: string,
codeChallenge: string,
aud: string,
): string {
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
scope: scope,
state: state,
aud: aud,
launch: launchCode,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
return `${authorizationEndpoint}?${params.toString()}`;
}
De browser volgt de redirect:
GET /authorize?response_type=code&client_id={module-clientid}&redirect_uri=https%3A%2F%2Fmodule.example.nl%2Fcallback&scope=launch+openid+fhirUser+patient%2F%2A.read&state=example789&aud=https%3A%2F%2Fdva.example.nl%2Ffhir&launch={launch_code}&code_challenge=XYZ&code_challenge_method=S256 HTTP/1.1
Host: dva.example.nl
Wat er aan DVA-zijde gebeurt:
launch_code en koppelt die aan de in Stap 4 van walkthrough #1 opgebouwde context (sub, module client_id, resources).redirect_uri met code en state.De DVA stuurt de browser terug naar de module callback met een authorization code.
code, opaque authorization code (kortlevend, eenmalig bruikbaar in Stap 4).state, moet exact matchen met de waarde uit Stap 2.GET /callback?code={authorization-code}&state=example789 HTTP/1.1
Host: module.example.nl
state tegen de waarde uit de sessie. Bij mismatch: afbreken.code_verifier, iss en redirect_uri uit de sessie-context voor Stap 4.De module ruilt de authorization code in voor een access_token bij het token_endpoint.
tokenEndpoint, uit Stap 1.code, uit Stap 3.codeVerifier, de PKCE code_verifier uit Stap 2.redirectUri, zelfde als in Stap 2.clientId / clientSecret, module client credentials.type TokenResponse = {
access_token: string;
token_type: "Bearer";
expires_in: number;
scope: string;
patient?: string;
fhirUser?: string;
id_token?: string;
refresh_token?: string;
};
async function exchangeCodeForAccessToken(
tokenEndpoint: string,
code: string,
codeVerifier: string,
redirectUri: string,
clientId: string,
clientSecret: string,
): Promise<TokenResponse> {
const body = new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
});
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
const resp = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basic}`,
},
body: body.toString(),
});
if (!resp.ok) {
throw new Error(`Token exchange failed: ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
200 OK met JSON map die het access_token bevat, plus launch-context (patient, fhirUser, eventueel id_token als openid scope is gebruikt).openid fhirUser scope){
"access_token": "{access_token}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch openid fhirUser patient/*.read",
"patient": "Patient/789",
"fhirUser": "Patient/789",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
openid scope){
"access_token": "{access_token}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch patient/*.read",
"patient": "Patient/789"
}
De module gebruikt het access_token om FHIR resources op te halen bij de DVA Resource Server.
iss, de DVA FHIR base URL uit Stap 1.accessToken, uit Stap 4.GET /fhir/Task/456 HTTP/1.1
Host: dva.example.nl
Authorization: Bearer {access_token}
Accept: application/fhir+json
async function fetchTask(
iss: string,
taskId: string,
accessToken: string,
): Promise<unknown> {
const resp = await fetch(`${iss}/Task/${taskId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/fhir+json",
},
});
if (!resp.ok) {
throw new Error(`FHIR GET /Task/${taskId} failed: ${resp.status}`);
}
return resp.json();
}
De module kan vervolgens zijn eigen functionaliteit aanbieden (bijvoorbeeld een vragenlijst tonen) op basis van de opgehaalde FHIR-context.
Openstaand: scope-keuze en id_token.
openid fhirUser leidt ertoe dat de DVA een id_token uitgeeft met de gebruikersidentiteit als FHIR referentie. Dit is de verwachte praktijk voor modules met meervoudige toegang (PGO én portalen). Zie DVA als Identity Provider.openid is toegestaan; de module krijgt dan geen id_token en moet gebruikersidentiteit afleiden uit launch-context (patient, fhirUser via scope zonder openid is technisch niet mogelijk in SMART v2).Openstaand: client-authenticatiemethode op /token. Dit voorbeeld gebruikt client_secret_basic. Een DVA mag ook client_secret_post, private_key_jwt of andere methoden ondersteunen; zie het veld token_endpoint_auth_methods_supported in de SMART configuration.