Commit 79b3a57
fix(component): make loopback and multi-origin DCR clients refreshable in ha_auth mode (#2249)
* fix(component): make loopback and multi-origin DCR clients refreshable in ha_auth mode
Core binds a refresh token to the client_id the code leg presented; for
translated DCR/CIMD identities a redirect_uri-less refresh grant carried
nothing to re-derive it (ephemeral loopback ports, multi-origin
registrations), so the token view answered a local invalid_grant while
the code leg still handed out core's refresh token. Every
loopback-callback client re-authorized on each 30-minute access-token
expiry (#2248).
Record the identity at mint time instead: wrap the refresh_token of
every server-side-forwarded 200 in an HMAC-signed envelope carrying
core's token, the bound client_id, and a digest of the presenter; the
refresh leg unwraps it and proxies the exact pair to core. DCR in
ha_auth mode now advertises refresh_token for every registration.
Pre-envelope tokens keep the previous derivation path and migrate on a
single re-authorize.
Mirrored into the dev webhook proxy with the required dev version bump.
Fixes #2248
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
* fix(component): answer invalid envelopes locally, unwrap on revoke, wrap hybrid CIMD code exchanges
Review round on #2249:
- Split unwrap_refresh_token's failure into ABSENT (no prefix: a
pre-envelope token, falls through to the legacy derivation) and INVALID
(our prefix, bad MAC / presenter / shape / version): INVALID is logged
and answered with a local invalid_grant instead of 307'd into core's
failed-login accounting, and the pre-envelope message no longer claims
a cause it cannot know.
- Core's action=revoke on /auth/token answers 200 for unknown tokens, so
a client revoking the envelope would silently no-op; unwrap it there
(no presenter binding, RFC 7009 authorizes the bearer) and proxy.
- A hybrid CIMD identity presenting its same-origin redirect took the
code-leg fast path untranslated, got core's raw token, and stayed in
the re-auth loop; the code leg now pays the one CIMD fetch and proxies
an unreproducible identity so its token is wrapped.
- Log transport failures and half-initialised config on the core token
forward (with a 503 description), log non-JSON 200s, and replace the
bare assert on the anonymous refresh path with a guarded fallback.
- Mirror the component's str()-coerced MultiDict form into the proxy's
ha_auth token view (#2219 hardening had drifted).
- Tests: mirror-side MAC tamper/wrong-key/non-200/byte-identity cases,
disjointness tests that exercise the MAC via prefix relabelling,
refresh-leg re-wrap, payload-shape and keyless-ha_auth cases, revoke
cases, looser error_description assertions, one duplicate removed.
- Docs/comments: stale MIRROR header, DOCS.md migration clause, trimmed
repeated pre-envelope prose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
* fix(component): front RFC 7009 revocation so envelope-wrapped refresh tokens can be revoked
Core's /auth/revoke answers 200 for a token it does not recognise, so a
client revoking the signed envelope directly at core would get a silent
no-op and keep a live session. Add a scoped /revoke view in both trees:
an envelope is unwrapped (bearer-authorized, no presenter binding) and
forwarded server-side to core's /auth/revoke; a plain token 307s into
core so core observes the client's address; 503 carries Retry-After per
RFC 7009 section 2.2.1. Served and advertised (revocation_endpoint,
revocation_endpoint_auth_methods_supported: ["none"]) in ha_auth mode
only; none and legacy documents and behaviour are unchanged. The token
and revoke legs share one core-forward helper. SECURITY.md and the dev
proxy DOCS.md describe the fronted endpoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
* test: account for the scoped /revoke view in the registered-view pins
The embedded-entry prebind counts and the add-on proxy view-set tests
pin the number and URLs of the bound OAuth views; the scoped revocation
dispatcher (#2248) is one more. The add-on tests feature-detect it like
the other routes so the stable flavor keeps its expectation until
promotion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
* test: count the scoped /revoke view in the remaining registered-view pins
The component webhook registration tests and the add-on proxy's none /
ha_auth / reload / unload view-set tests pin the bound OAuth view count
and URLs; the scoped revocation dispatcher (#2248) binds with the
authorize/token pair in every mode. The add-on tests feature-detect it
so the stable flavor keeps its expectation until promotion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
* fix(component): recover the core token from an unverifiable envelope on revocation
Review by Patch76 on #2249: a token carrying the hamcp-rt- prefix whose
MAC does not verify took the same branch as a plain core token and was
307'd to core, whose revoke endpoint answers 200 for anything it cannot
resolve — so the client was told the session was revoked while core's
grant lived out its 90 days. The ordinary way in is a signing-key
rotation: removing and re-adding the integration mints a new key and
invalidates every outstanding envelope.
core_token_for_revocation() now parses the core token out of a prefixed
value's body even when the MAC fails, and only on the revocation legs.
Sound here and nowhere else: RFC 7009 authorizes the bearer of a token
rather than a client, and core's revoke endpoint is anonymous and
idempotent, so forwarding an unverified body grants a forger nothing
they could not get by POSTing to core directly. The refresh leg still
answers an INVALID envelope locally and never forwards it. The
unverified parse is length-capped before decoding and catches
RecursionError, which only it can meet.
Retry-After now rides every revocation 503, whether the request arrived
at the scoped /revoke view or as action=revoke on /token.
Tests pin both revocation surfaces in both trees: tampered and
rotated-key envelopes forwarded unwrapped, prefixed values carrying no
usable token 307'd, over-cap values not parsed, and the Retry-After
split between revocation and plain token failures. SECURITY.md and
DOCS.md narrow the no-outbound-request claim to the refresh path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BCYnuBnew5cdVoDs4Roc
---------
Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent d971ed5 commit 79b3a57
19 files changed
Lines changed: 3757 additions & 307 deletions
File tree
- custom_components/ha_mcp_tools
- homeassistant-addon-webhook-proxy-dev
- mcp_proxy_dev
- tests
- addon
- src/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
189 | 189 | | |
190 | 190 | | |
191 | 191 | | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
196 | 210 | | |
197 | 211 | | |
198 | 212 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
160 | 167 | | |
161 | 168 | | |
162 | 169 | | |
163 | 170 | | |
164 | 171 | | |
165 | 172 | | |
| 173 | + | |
| 174 | + | |
166 | 175 | | |
167 | 176 | | |
168 | 177 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
159 | | - | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
160 | 167 | | |
161 | 168 | | |
162 | 169 | | |
| |||
188 | 195 | | |
189 | 196 | | |
190 | 197 | | |
191 | | - | |
| 198 | + | |
192 | 199 | | |
193 | 200 | | |
194 | 201 | | |
195 | 202 | | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
200 | 209 | | |
201 | 210 | | |
202 | 211 | | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
| 212 | + | |
208 | 213 | | |
209 | 214 | | |
210 | 215 | | |
| |||
284 | 289 | | |
285 | 290 | | |
286 | 291 | | |
287 | | - | |
| 292 | + | |
288 | 293 | | |
289 | 294 | | |
290 | 295 | | |
| |||
0 commit comments