Skip to content

feat(http): HMAC-SHA256 request signing + token refresh on 401 - #84

Open
vietnguyentuan2019 wants to merge 1 commit into
mainfrom
feat/http-token-refresh-hmac-signing
Open

feat(http): HMAC-SHA256 request signing + token refresh on 401#84
vietnguyentuan2019 wants to merge 1 commit into
mainfrom
feat/http-token-refresh-hmac-signing

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

What

Ships #81: HttpRequestWorker gains optional HmacSigningConfig (HMAC-SHA256 request signing) and TokenRefreshConfig (auto-refresh + one-shot retry on 401) — Flutter parity Group 2.

Design

Both are config-scoped, not installed on HttpClientProvider's shared singleton client (which is process-wide across all workers) — so different workers/calls can carry different secrets and refresh endpoints without stepping on each other.

  • HMAC signing: canonical METHOD\nURL\nBODY\nTIMESTAMP → HMAC-SHA256 via Okio's built-in ByteString.hmacSha256 — no platform-specific crypto/expect-actual needed. Hex-encoded, optional prefix (e.g. sha256= for GitHub-webhook style). signBody/includeTimestamp toggle what's covered.
  • Token refresh: on 401, POST a configurable refresh endpoint, extract the new token via a dot-notation JSON path (e.g. auth.access_token), retry the original request exactly once with the refreshed token. Never loops.

Security review (folded in before this PR, not left as follow-up)

  • refreshUrl now passes SecurityValidator.validateURL() — checked in HttpRequestWorker.doWork() (fails fast, same as the primary url) and again inside TokenRefresh.refreshToken() itself, so any future caller adopting TokenRefreshConfig (HttpSyncWorker, HttpUploadWorker, …) gets the SSRF gate for free instead of needing to remember it — the same drift class SecureRedirectFollowing.kt's own KDoc warns about.
  • Real bug caught by the tests: Ktor's header() appends, not replaces — injecting the refreshed Authorization header without removing the stale one first sent both values on retry.
  • A GET/DELETE's config.body (never written to the wire) is excluded from what gets signed — previously the signature covered bytes the server would never receive.
  • A blank token in the refresh response is treated as a failed refresh, not retried with Authorization: Bearer .
  • Corrected KDoc that incorrectly claimed the HMAC signature covers headers (including a refreshed Authorization) — it only covers METHOD/URL/BODY/TIMESTAMP.

Testing

./gradlew :kmpworker-http:iosSimulatorArm64Test :kmpworker-http:testDebugUnitTest :kmpworker:testDebugUnitTest — 64/64 on iOS, Android green. New coverage: known-vector HMAC signature test, GET-with-body exclusion, append-vs-replace header regression, token-refresh happy path / still-401 / refresh-endpoint-failure / blank-token / no-config-passthrough, SSRF-blocked refreshUrl at both the doWork() and refreshToken() layers, and extractByDotPath edge cases (missing key, non-object mid-path, null, array, numeric, empty path).

Ships the two Flutter-parity features tracked in #81: HttpRequestWorker
gains optional HmacSigningConfig and TokenRefreshConfig.

- HmacSigningConfig: canonical METHOD\nURL\nBODY\nTIMESTAMP ->
  HMAC-SHA256 (Okio's built-in ByteString.hmacSha256 -- no
  platform-specific crypto needed), hex-encoded, optional prefix
  (GitHub-webhook-style sha256=). Known-vector test pins the exact
  canonical format against an independently computed digest.
- TokenRefreshConfig: on 401, POST a configurable refresh endpoint,
  extract the new token via dot-notation JSON path, retry the original
  request exactly once with the refreshed Authorization header. Never
  loops -- a still-401 retry, a failed refresh call, or a blank/missing
  token in the refresh response all fall through to the original 401
  rather than retrying again.

Both config-scoped (not installed on the shared HttpClientProvider
singleton, which is process-wide) so different workers/calls can use
different secrets and refresh endpoints.

Security-review fixes folded in before this landed:
- refreshUrl now passes SecurityValidator.validateURL() -- both in
  HttpRequestWorker.doWork() (fails fast, matches the existing config.url
  check) and inside TokenRefresh.refreshToken() itself, so any future
  caller of TokenRefreshConfig (HttpSyncWorker, HttpUploadWorker, ...)
  gets the SSRF gate for free rather than needing to remember it -- same
  drift class SecureRedirectFollowing.kt's KDoc warns about.
- Fixed a real bug the tests caught: Ktor's header() APPENDS, so
  injecting a refreshed Authorization header without first removing the
  stale one sent both values on retry.
- A GET/DELETE request's config.body (never written to the wire) is now
  excluded from what gets signed -- previously the signature covered
  bytes the server would never receive.
- A blank token in the refresh response is treated as a failed refresh
  instead of retrying with 'Authorization: Bearer '.
- Corrected KDoc that incorrectly claimed the HMAC signature covers
  headers (including a refreshed Authorization header) -- it only covers
  METHOD/URL/BODY/TIMESTAMP.

Fixes #81.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant