SMART 那一層改過四次
day12 那個 launch context,火線超人裡面是一支叫 Smart::LaunchContextService 的服務在處理。2025-11-16 寫完,到今天大約九個月,只有 commit 一次。
處理 OAuth 授權的是另外兩支服務。同樣這九個月,加起來卻改了十五次。
我從那十五次裡挑四次來看看。前三次改的都是我當初沒照規範做的地方,而第四次有點不大一樣。
第一次:端點寫死且寫錯
2026-01-24,commit 815fd78。原本 token 端點是這樣寫的:
token_endpoint = "#{@fhir_server_url}/auth/token"
看起來沒什麼問題對吧,但問題是 @fhir_server_url 後面本來就有帶 /fhir,再接上去就變成 /fhir/auth/token。那個位址上什麼都沒有。
commit 訊息寫得很清楚:Fix token endpoint path (was using /fhir/auth/token instead of /auth/token)。
後來才改成不自己亂拼湊,先問 FHIR 伺服器再說。
day06 講過,問端點有兩條路。一條是 .well-known/smart-configuration,SMART App Launch 2.0 的作法。另一條更早,從 /metadata 的 CapabilityStatement 裡挖,那個擴充叫 oauth-uris。我當時走的是後面那條。
哪一條都行,反正都比自己亂拼字串強多了。壞就壞在我根本沒問。
同一個 commit 還補了另一樣東西。原本那個換 token 的方法簽章是 exchange_code_for_token(code, redirect_uri),改成了 exchange_code_for_token(code, redirect_uri, code_verifier: nil)。
也就是說,在還沒換之前 PKCE 的 code_verifier 根本沒送給授權伺服器。day08 講過那組亂數怎麼用。授權時送的 code_challenge 是拿 code_verifier 算出來的,換 token 才送原本那組亂數。伺服器兩邊對得起來,才知道是同一個程式。而我當時只做了前面那一步。
第二次:EHR Launch 少帶兩個參數
2026-03-23,commit f6cf536,補的是 launch 跟 aud。
commit 內文寫得很完整:The SMART on FHIR EHR Launch flow requires the launch token and aud (FHIR server URL) to be forwarded in the authorization request. Without these, the SMART Launcher returns "Invalid launch options".
Invalid launch options 這句話你在 day06 看過。當時那張圖講的是自己拼授權網址,會把 sim/ 那一段拼掉。拼出來的網址是合法的,但就是錯的,按下去就跳這句。
這次我拿到同一句話,成因卻不一樣。網址本身沒拼錯,是該帶的兩個參數沒帶。
aud 在 day05 有整整一節,重點是兩種模式都要送。它回答的是「這張 token 要拿去打哪一台 FHIR 伺服器」。launch 就是 EHR Launch 專屬的那根棒子。day12 用 EHR 模式跑的時候,那個值有 124 字元。
規範講過,我自己的系列也講過,程式碼還是漏了。
第三次:secret 送錯地方
2026-03-27,commit 7f7f587。我原本把 client secret 放在 POST 的表單內容裡送出去:
body[:client_secret] = @client_secret if @client_secret.present?
後來從表單內容裡刪掉,搬到 Authorization 標頭:
body.delete(:client_secret)
headers['Authorization'] = "Basic #{Base64.strict_encode64("#{@client_id}:#{@client_secret}")}"
理由 commit 訊息裡有:Use HTTP Basic Authentication header for confidential client token exchange per SMART on FHIR spec。同一句還註明它修掉的錯誤是 401 Basic authentication is required。
這條你的 app 遇不到。day07 講過,瀏覽器裡的檔案人家打得開。所以純前端一律是 public client,手上根本沒有 secret 可送。
火線超人有後端,是另外那一種。手上有 secret 的話就多一個問題要回答:這東西送在哪裡。不是隨便找個欄位塞進去就算數。
第四次:授權網址太長
2026-03-27,跟上一次同一天,commit ce6af18。
commit 內文是這樣寫的:LINE URI action has a 1000 character limit. SMART Launcher's authorization URLs exceed this due to long sim paths.
這次的狀況是這樣。火線超人把授權網址掛在 LINE 訊息的按鈕上,使用者點下去才開始授權。而 LINE 對那種按鈕的網址有硬性上限,一千個字元。sandbox 那串 sim/ 路徑本來就長,湊一湊就超過去,那則訊息根本送不出去。
後來改成把完整的授權網址存進資料表,讓使用者點一個短的位址,再由後端導過去。
這一次跟前三次不一樣。前三次是規範白紙黑字寫著,而我沒照做。這次規範根本沒寫,是通路自己的限制撞上了 sandbox 的網址長度。
分得出這兩種差別,遇到問題你才知道要去哪裡查。前三次翻 SMART App Launch 就有,第四次翻到爛也翻不到。
快取那件事不是設計出來的
第三次那個 commit 還順手做了一件事,跟 day22 有關。
它把抓回來的 metadata 快取起來,存活時間一小時。同時給那支請求設了 3 秒的 timeout,在那之前程式碼裡根本沒設過。
理由 commit 訊息裡寫著,這是為了避免重複抓太慢、把反向代理拖到逾時。火線超人是 LINE bot。使用者每傳一則訊息,LINE 就打一次 webhook 進來。那支請求裡要用到授權端點,於是每次都去抓一輪 metadata。抓到後來,反向代理等不下去。
所以那個快取根本不是設計階段想到的,是被逾時逼出來的。隔天的 494e0ad 才把端點從「每次抓」改成存進資料表欄位,那次 o_auth2_service.rb 一口氣少了 99 行。
day22 帶你做的那份快取設 24 小時。我這個一小時是當場止血訂的,兩個數字都不是標準規定的,是各自環境裡挑出來的。

沒改過的那一邊
四次講完,來看另一邊。
開頭那支 Smart::LaunchContextService,89 行,九個月一個 commit。中間經過多租戶重構、跨院整合、兩次主機遷移,它一行都沒被動到。
再看開頭說的那兩支。它們不是並排的,是疊起來的。
裡面那層叫 Fhir::OAuth2Service,121 行,貼著協定走。它負責組授權網址跟換 token,總共 7 個 commit。最後一次改是 2026-03-28,到盤點日大概五個月沒動。
外面那層叫 FhirOauthService,348 行,處理綁定跟使用者狀態。它一路改到 2026-06-16 的多租戶隔離,總共 8 個 commit。
這兩支不是新舊版本的關係,是包覆關係。外層那支的第 297 行就在呼叫裡面那支。

規格文件那邊也對得起來。我這個專案的每一項功能都有一份規格檔,檔頭記著最後更新日期。SMART 授權相關的有四份,更新日期都停在 2026 年 3 月,最晚那份是 3 月 28 日。
那四份講的東西,剛好就是第二幕到第三幕教你的:
- 每台伺服器各自的 credentials
- 每台各自的 scope
fhirUser身分- 端點快取
講白一點就是這樣。最貼著規範的那一支九個月沒動,包著它的業務層改到今年六月。而我挑出來的前三次,改的都是當初沒照規範做的地方。
這不是說規範比較高明。是說規範已經被很多人踩過坑了,而我的判斷只經過我一個人。
你在鐵人賽這幾天學到的,哪些會過期
照規範做的耐久,這件事也可以拿來看你自己。
你在鐵人賽這幾天學到的東西其實分兩部分。一部分是規範講死的,另一部分是我在這個系列裡幫你選的。
規範講死的那些,你都走過了:
- 端點要跟伺服器問,不能自己拼
code_challenge跟code_verifier要成對送出去aud要送,兩種 launch 模式都要- scope 怎麼組,
fhirUser是誰 - 分頁要照著
next走
這些東西不會變。換一台伺服器還在,換一個函式庫還在,換一種語言也還在。火線超人是 Rails,你的是瀏覽器原生 JS,兩邊要對的是同一份規範。
另一部分是我幫你選的,規範完全沒規定:
- token 存
sessionStorage - discovery 快取 24 小時
- 合併排序拿來源代號當次要鍵
- 整個系列用 fhirclient
我選的那些你可以挑別的,而且你大概真的會挑別的。day22 那個 24 小時跟火線超人那個一小時,剛才講過了,兩個都不是標準規定的。
會過期的是我選的那些。真正要記住的是規範講死的,還有它們要去哪裡查。
跟著做:開著 Network 跑最後一次授權
最後一次動手。這次不用改程式碼,就是看它在做什麼。
打開你第四幕做出來的專案,開 DevTools 的 Network。記得勾 Preserve log,不然導轉之後那些紀錄會被清掉。然後按下連線,跑一次完整授權。
有四件事你會親眼看到,前三件正是我當初漏掉的。
第一,按下連線之後的第一支請求。 它會是 .well-known/smart-configuration。端點是問來的,不是拼出來的。我這次跑第一支還逾時了一次,重試才回 304。
第二,導去授權伺服器那一支的網址。 裡面有 aud、code_challenge 跟 code_challenge_method=S256。但沒有 launch。你的 app 走的是 Standalone Launch,那個值只有 EHR 啟動時才有。
第三,導回來之後那支 POST。 打去 /auth/token,表單內容裡有 code_verifier。
第二件跟第三件對著看。授權網址帶出去的是 code_challenge,換 token 才帶 code_verifier。前者是拿後者算出來的,伺服器兩個都收到才對得起來。我第一次修正補的就是後面那一步。
第四,同一支 POST 裡沒有的東西。 沒有 client_secret,Request Headers 裡也沒有 Authorization。因為你是 public client,手上根本沒有 secret 要送。

四件事看完,你會發現一個共同點。前三件都不是 fhirclient 發明的,是規範規定的,它只是替你照做。第四件更單純,你是 public client,本來就沒有 secret。所以哪天你不用這個函式庫了,要對的還是同一份規範。
commit 訊息沒說的
誠實列一下。
四次修正裡,只有第二次跟第四次的 commit 內文提到 SMART Launcher。另外兩次是在哪一台伺服器上發現的,我當時沒寫。
修正的間隔為什麼是那樣,1 月一次、3 月三次,commit 訊息裡也看不出來。
還有第一次那個寫死的端點,在改掉之前流程到底走不走得完。commit 訊息只說路徑錯了,沒說在那之前換 token 成功過沒有。
這三件事我不補,因為補了就是編。
小結
我改過的四次,括號裡是系列講過它的那幾篇。
- 端點寫死而且寫錯(day06),同一個 commit 才把
code_verifier送進 token 交換(day08) - EHR Launch 漏帶
launch跟aud,拿到的錯誤跟 day06 那張圖同一句,成因不同(day05、day12) - client secret 放在 POST 內容裡,而不是 Authorization 標頭(day07)
- 授權網址超過通路的一千字元上限,這條規範沒寫(day26)
前三條翻規範查得到,第四條查不到。而九個月沒被動過的那一支,做的正是規範講死了的事。
火線超人得過獎,但它的 SMART 那一層不是一開始就對的,是我改了四次才補齊的。你第四幕那個 app 一開始就沒踩到前三個坑。不是因為你比較厲害,是因為 fhirclient 照規範做了。
所以明天那篇不會教你新東西。它給的是規範的清單,也就是今天講的那些不會過期的要去哪裡查。