feat(http): HMAC-SHA256 request signing + token refresh on 401 - #84
Open
vietnguyentuan2019 wants to merge 1 commit into
Open
feat(http): HMAC-SHA256 request signing + token refresh on 401#84vietnguyentuan2019 wants to merge 1 commit into
vietnguyentuan2019 wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ships #81:
HttpRequestWorkergains optionalHmacSigningConfig(HMAC-SHA256 request signing) andTokenRefreshConfig(auto-refresh + one-shot retry on401) — 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.METHOD\nURL\nBODY\nTIMESTAMP→ HMAC-SHA256 via Okio's built-inByteString.hmacSha256— no platform-specific crypto/expect-actual needed. Hex-encoded, optional prefix (e.g.sha256=for GitHub-webhook style).signBody/includeTimestamptoggle what's covered.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)
refreshUrlnow passesSecurityValidator.validateURL()— checked inHttpRequestWorker.doWork()(fails fast, same as the primaryurl) and again insideTokenRefresh.refreshToken()itself, so any future caller adoptingTokenRefreshConfig(HttpSyncWorker,HttpUploadWorker, …) gets the SSRF gate for free instead of needing to remember it — the same drift classSecureRedirectFollowing.kt's own KDoc warns about.header()appends, not replaces — injecting the refreshedAuthorizationheader without removing the stale one first sent both values on retry.config.body(never written to the wire) is excluded from what gets signed — previously the signature covered bytes the server would never receive.Authorization: Bearer.Authorization) — it only coversMETHOD/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-blockedrefreshUrlat both thedoWork()andrefreshToken()layers, andextractByDotPathedge cases (missing key, non-object mid-path,null, array, numeric, empty path).