Authorization code + PKCE: kenapa React SPA tidak punya secret
Public client, PKCE S256, state dan aud, lalu token response dengan launch context
code_verifier dan code_challenge (S256) melindungi authorization code, apa fungsi state dan aud,
dan menyusun alur EHR launch dari klik di EHR sampai data tampil.
Sebelumnya, di lesson 0017: app diluncurkan (EHR atau standalone) lalu menemukan authorization_endpoint dan token_endpoint lewat discovery. Lesson ini melanjutkan dari titik itu.
3. Authorization code + PKCE: kenapa React SPA tidak punya secret
SMART membedakan confidential dan public app berdasarkan satu pertanyaan: "is your app able to protect a secret?"
HTML5/JS in-browser app, termasuk SPA, adalah public app karena secret apa pun di dalamnya "can potentially be extracted". [1]
Sebagai gantinya: "All SMART apps SHALL support Proof Key for Code Exchange (PKCE)", server SHALL mendukung S256 dan SHALL NOT mendukung plain. [1]
Masalah yang diselesaikan PKCE: public client rentan terhadap authorization code interception; penyerang yang mendapat code bisa menukarnya menjadi access token.
Dengan PKCE, app membuat code_verifier acak (43–128 karakter), mengirim code_challenge = BASE64URL(SHA256(code_verifier)) saat authorize,
dan baru mengirim code_verifier aslinya saat menukar code. Penyerang yang hanya punya code tidak bisa menukarnya. [2]
Di sisi server: authorization server menyimpan code_challenge dan code_challenge_method bersama authorization code yang diterbitkannya. [2]
Saat token request datang, server menghitung ulang BASE64URL(SHA256(code_verifier)) dari verifier yang diterima dan membandingkannya dengan challenge tadi;
kalau tidak sama, server menjawab error invalid_grant dan tidak ada token yang terbit. [2]
// Hanya untuk dibaca: inilah yang dikerjakan fhirclient untuk Anda (Web Crypto, browser)
const b64url = (b: Uint8Array) =>
btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32))); // 43 karakter (RFC 7636 §4.1)
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
const challenge = b64url(new Uint8Array(digest)); // dikirim di authorize
// verifier disimpan sementara di sessionStorage, dikirim nanti ke token endpoint
Authorize request (browser di-redirect ke authorization_endpoint), contoh dari IG ditambah dua parameter PKCE: [1]
https://ehr/authorize?
response_type=code&
client_id=app-client-id&
redirect_uri=https%3A%2F%2Fapp%2Fafter-auth&
launch=xyz123& // hanya EHR launch
scope=launch+patient%2FObservation.rs+patient%2FPatient.rs+openid+fhirUser&
state=98wrghuwuogerg97&
aud=https://ehr/fhir& // FHIR base URL; di EHR launch = iss
code_challenge=...&code_challenge_method=S256
state: wajib, tidak bisa ditebak (≥ 122 bit entropi, mis. UUID acak), mencegah CSRF/session fixation; app SHALL memvalidasinya diredirect_uri. [1]aud: wajib, URL server FHIR tujuan; mencegah token sah bocor ke resource server palsu. [1]- Authorization code berumur pendek, "usually expiring within around one minute". [1]
Lalu app POST ke token_endpoint (form-urlencoded) dengan grant_type=authorization_code, code, redirect_uri, code_verifier,
dan client_id (wajib untuk public app; public app tidak melakukan client authentication). [1] Responsnya:
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch patient/Observation.rs patient/Patient.rs openid fhirUser", // bisa beda dari yang diminta!
"id_token": "eyJ0eX...", // karena openid fhirUser
"patient": "123", // launch context: Patient/123
"need_patient_banner": true
}
scope di respons "can be different from the scopes requested by the app"; launch context seperti "patient": "123" datang bersama token.
Respons token SHALL memakai header Cache-Control: no-store. Access token SHOULD berumur tidak lebih dari satu jam. [1]
Setelah itu setiap request FHIR membawa Authorization: Bearer {{access_token}}; resource server memvalidasi masa berlaku, scope, dan aud. [1]
Latihan 2 — Susun alur EHR launch
Susun dari klik di EHR sampai data tampil. Ucapkan setiap langkah dengan suara keras saat memindahkannya.
- EHR membuka launch URL app dengan
issdanlaunch - App
GET [iss]/.well-known/smart-configuration - App membuat
code_verifier,code_challenge, danstate - Redirect ke
authorization_endpointdengan scopelaunch patient/Observation.rs,launch,aud - EHR memutuskan (kebijakan, mungkin bertanya ke user), redirect ke
redirect_uridengancode+state - App memvalidasi
state, laluPOSTketoken_endpointdengancode+code_verifier - Token response:
access_token,scopeyang diberikan,patient GET Observation?patient=123denganAuthorization: Bearer
Discovery harus sebelum authorize karena dari sanalah app tahu URL authorize dan token.
PKCE dan state dibuat sebelum redirect karena challenge dikirim di authorize, sedangkan verifier baru dikirim saat menukar code.
Konteks pasien baru diketahui app dari token response, bukan dari launch URL (launch hanya handle opaque).
Latihan 3 — PKCE dan state
Latihan lisan — Jawab lisan
Pasang timer, jawab dengan suara keras seolah sedang interview, baru buka jawaban contoh. Soal ini juga ada di bank soal dan bisa ditarik review 0027.
"React SPA kami tidak punya client secret. Bagaimana PKCE tetap melindungi authorization code?" (60 detik)
Bandingkan dengan jawaban contoh dan checklist-nya
"SPA adalah public client: secret apa pun di bundle JS bisa diekstrak, jadi tidak ada client authentication. Risikonya authorization code interception: siapa pun yang mencegat code bisa menukarnya menjadi token.
Dengan PKCE, app membuat code_verifier acak 43–128 karakter dan menyimpannya sementara, lalu mengirim code_challenge = BASE64URL(SHA256(verifier)) dengan code_challenge_method=S256 di authorize request; SMART mewajibkan S256 dan melarang plain.
Server menyimpan challenge itu bersama code. Saat menukar code di token endpoint, app mengirim verifier aslinya; server menghitung ulang hash-nya dan membandingkan, kalau beda dijawab invalid_grant. Penyerang yang hanya punya code tidak punya verifier.
PKCE tidak menggantikan state: state acak tetap dikirim dan divalidasi di redirect_uri untuk mencegah CSRF, dan aud diisi FHIR base URL."
Checklist: apakah Anda menyebut…
- SPA = public client: secret di bundle JS bisa diekstrak, tanpa client authentication?
- ancaman: authorization code interception (code dicegat lalu ditukar penyerang)?
code_verifieracak 43–128 karakter, disimpan sementara di app?code_challenge= BASE64URL(SHA256(verifier)), method S256 di authorize;plaintidak didukung?- verifier dikirim di token request; server hitung ulang dan bandingkan, beda =
invalid_grant? statetetap dipakai melawan CSRF dan divalidasi diredirect_uri?
Lulus kalau minimal 4 dari 6.
Berikutnya, lesson 0019: token response membawa scope yang diberikan. Apa arti patient/Observation.rs, dan bagaimana job tanpa user mendapat token?
Istilah: GLOSSARY.md (bagian "Keamanan dan SMART on FHIR") · Sumber: RESOURCES.md
Sumber: [1] App Launch: Launch and Authorization · [2] RFC 7636 PKCE