fix(sync): refresh access token when expired

This commit is contained in:
Tienson Qin
2026-03-03 12:28:24 +08:00
parent 8150230772
commit dcb26353ec
3 changed files with 123 additions and 16 deletions

View File

@@ -8,6 +8,7 @@
[clojure.set :as set]
[clojure.string :as string]
[frontend.common.missionary :as c.m]
[frontend.common.thread-api :refer [def-thread-api]]
[frontend.config :as config]
[frontend.debug :as debug]
[frontend.flows :as flows]
@@ -17,7 +18,8 @@
[goog.crypt.Hmac]
[goog.crypt.Sha256]
[goog.crypt.base64 :as base64]
[missionary.core :as m]))
[missionary.core :as m]
[promesa.core :as p]))
;;; userinfo, token, login/logout, ...
@@ -301,6 +303,11 @@
(-> (state/get-auth-id-token) parse-jwt expired?))
(throw (ex-info "empty or expired token and refresh failed" {:type :expired-token})))))))
(def-thread-api :thread-api/ensure-id&access-token
[]
(p/let [_ (js/Promise. task--ensure-id&access-token)]
{:id-token (state/get-auth-id-token)}))
;;; user groups
(defn rtc-group?

View File

@@ -134,6 +134,28 @@
(defn- auth-token []
(worker-state/get-id-token))
(defn- id-token-expired?
[token]
(if-not (string? token)
true
(try
(let [exp-ms (some-> token worker-util/parse-jwt :exp (* 1000))]
(or (not (number? exp-ms))
(<= exp-ms (common-util/time-ms))))
(catch :default _
true))))
(defn- <resolve-ws-token
[]
(let [token (auth-token)]
(if (id-token-expired? token)
(p/let [resp (worker-state/<invoke-main-thread :thread-api/ensure-id&access-token)
refreshed-token (:id-token resp)]
(when (string? refreshed-token)
(worker-state/set-new-state! {:auth/id-token refreshed-token})
refreshed-token))
(p/resolved token))))
(defn- get-user-uuid []
(some-> (worker-state/get-id-token)
worker-util/parse-jwt
@@ -1376,8 +1398,12 @@
(when-let [current @worker-state/*db-sync-client]
(when (and (= (:repo current) repo)
(= (:graph-id current) (:graph-id client)))
(let [updated (connect! repo current url)]
(reset! worker-state/*db-sync-client updated)))))
(-> (p/let [token (<resolve-ws-token)
updated (connect! repo current url token)]
(reset! worker-state/*db-sync-client updated))
(p/catch (fn [error]
(log/error :db-sync/ws-reconnect-failed {:repo repo :error error})
(schedule-reconnect! repo current url :connect-failed)))))))
delay)]
(swap! reconnect assoc :timer timeout-id :attempt (inc attempt))
(log/info :db-sync/ws-reconnect-scheduled
@@ -1443,20 +1469,21 @@
(catch :default _
nil))))
(defn- connect! [repo client url]
(defn- connect! [repo client url token]
(when (:ws client)
(stop-client! client))
(let [ws (js/WebSocket. (append-token url (auth-token)))
updated (assoc client :ws ws)]
(attach-ws-handlers! repo updated ws url)
(set! (.-onopen ws)
(fn [_]
(reset-reconnect! updated)
(touch-last-ws-message! updated)
(set-ws-state! updated :open)
(send! ws {:type "hello" :client repo})
(enqueue-asset-sync! repo updated)))
(close-stale-ws-loop updated ws)))
(when token
(let [ws (js/WebSocket. (append-token url token))
updated (assoc client :ws ws)]
(attach-ws-handlers! repo updated ws url)
(set! (.-onopen ws)
(fn [_]
(reset-reconnect! updated)
(touch-last-ws-message! updated)
(set-ws-state! updated :open)
(send! ws {:type "hello" :client repo})
(enqueue-asset-sync! repo updated)))
(close-stale-ws-loop updated ws))))
(defn stop!
[]
@@ -1505,7 +1532,8 @@
url (format-ws-url base graph-id)
_ (ensure-client-graph-uuid! repo graph-id)
connected (assoc client :graph-id graph-id)
connected (connect! repo connected url)]
token (<resolve-ws-token)
connected (connect! repo connected url token)]
(reset! worker-state/*db-sync-client connected)
nil))
(p/finally

View File

@@ -61,6 +61,78 @@
:child2 child2
:child3 child3}))
(deftest resolve-ws-token-refreshes-when-token-expired-test
(async done
(let [refresh-calls (atom 0)
main-thread-prev @worker-state/*main-thread
worker-state-prev @worker-state/*state]
(reset! worker-state/*state (assoc worker-state-prev :auth/id-token "expired-token"))
(reset! worker-state/*main-thread
(fn [qkw _direct-pass? _args-list]
(if (= qkw :thread-api/ensure-id&access-token)
(do
(swap! refresh-calls inc)
(p/resolved {:id-token "fresh-token"}))
(p/resolved nil))))
(with-redefs [db-sync/auth-token (fn [] "expired-token")
db-sync/id-token-expired? (fn [_token] true)]
(-> (#'db-sync/<resolve-ws-token)
(p/then (fn [token]
(is (= 1 @refresh-calls))
(is (= "fresh-token" token))
(is (= "fresh-token" (worker-state/get-id-token)))
(reset! worker-state/*main-thread main-thread-prev)
(reset! worker-state/*state worker-state-prev)
(done)))
(p/catch (fn [error]
(reset! worker-state/*main-thread main-thread-prev)
(reset! worker-state/*state worker-state-prev)
(is nil (str error))
(done))))))))
(deftest resolve-ws-token-skips-refresh-when-token-not-expired-test
(async done
(let [refresh-calls (atom 0)
main-thread-prev @worker-state/*main-thread]
(reset! worker-state/*main-thread
(fn [qkw _direct-pass? _args-list]
(when (= qkw :thread-api/ensure-id&access-token)
(swap! refresh-calls inc))
(p/resolved {:id-token "fresh-token"})))
(with-redefs [db-sync/auth-token (fn [] "valid-token")
db-sync/id-token-expired? (fn [_token] false)]
(-> (#'db-sync/<resolve-ws-token)
(p/then (fn [token]
(reset! worker-state/*main-thread main-thread-prev)
(is (= 0 @refresh-calls))
(is (= "valid-token" token))
(done)))
(p/catch (fn [error]
(reset! worker-state/*main-thread main-thread-prev)
(is nil (str error))
(done))))))))
(deftest resolve-ws-token-refreshes-when-token-missing-test
(async done
(let [refresh-calls (atom 0)
main-thread-prev @worker-state/*main-thread]
(reset! worker-state/*main-thread
(fn [qkw _direct-pass? _args-list]
(when (= qkw :thread-api/ensure-id&access-token)
(swap! refresh-calls inc))
(p/resolved {:id-token "fresh-token"})))
(with-redefs [db-sync/auth-token (fn [] nil)]
(-> (#'db-sync/<resolve-ws-token)
(p/then (fn [token]
(reset! worker-state/*main-thread main-thread-prev)
(is (= 1 @refresh-calls))
(is (= "fresh-token" token))
(done)))
(p/catch (fn [error]
(reset! worker-state/*main-thread main-thread-prev)
(is nil (str error))
(done))))))))
(deftest update-online-users-dedupes-identical-messages-test
(let [client {:repo test-repo
:online-users (atom [])