Skip to content

fix(auth): stop the admin API from sharing the client's header dict - #1645

Open
grdsdev wants to merge 1 commit into
mainfrom
fix/auth-admin-headers-alias
Open

grdsdev wants to merge 1 commit into
mainfrom
fix/auth-admin-headers-alias

Conversation

@grdsdev

@grdsdev grdsdev commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Supersedes #1406. Same bug, fixed one layer down. Credit to @AdityaMedidala for finding and diagnosing it in #1404 / #1406.

The bug

GoTrueBaseAPI.__init__ stored the caller's dict by reference:

self._headers = headers

and GoTrueClient.__init__ hands the admin API its own _headers:

self.admin = AsyncGoTrueAdminAPI(url=..., headers=self._headers, ...)

So client._headers is client.admin._headers. Any in-place rewrite of the client's Authorization header rewrote the admin one too.

The supabase wrapper does exactly that on every auth event:

# supabase/_async/client.py, _listen_to_auth_events
self.auth._headers["Authorization"] = auth_header   # the user's token

Result: a client built with the service_role key loses admin rights the moment a user signs in.

Reproduced against a local Supabase instance

admin.list_users() before sign-in : OK
admin.list_users() after  sign-in : AuthApiError: User not allowed
admin/_client share one dict      : True

After this change:

admin.list_users() before sign-in : OK
admin.list_users() after  sign-in : OK
admin hdr is service_role         : True
admin/_client share one dict      : False

Why here and not in the wrapper

#1406 fixes it in supabase/_async/client.py by copying the admin dict and re-asserting the key on every auth event. That works, but it leaves the sharp edge in place: anyone constructing GoTrueClient or GoTrueAdminAPI directly still gets the aliased dict, and any future in-place header write reintroduces the bug.

Copying on the way in is one line, needs no wrapper changes at all, and makes in-place mutation safe by construction. I verified the wrapper needs no edit: _listen_to_auth_events is the only in-place _headers mutation in either package, and the wrapper already updates options.headers, postgrest and storage explicitly, so nothing relied on the sharing.

Tests

Four new tests in tests/_async/test_admin_headers.py, generated to _sync. Both cover the changed line: the client/admin aliasing, and a caller that keeps mutating the dict it passed in. All four fail without the fix and pass with it.

Verification

Every package, locally, against real infra (gotrue v2.187, PostgREST v11.2.2 and v16.2, storage-api v1.72.1):

package tests mypy
auth 168 passed (+4) clean
supabase 30 passed (unchanged) clean
functions 65 passed clean
postgrest 306 passed clean
storage 116 passed clean
realtime 31 passed clean

ruff check and ruff format --check both clean. _sync regenerated with make auth.build-sync.

`GoTrueBaseAPI.__init__` stored the caller's `headers` dict by reference, and
`GoTrueClient.__init__` hands the admin API its own `_headers`. That made
`client._headers is client.admin._headers`.

Anything that then rewrote the client's `Authorization` header in place also
rewrote the admin one. In the `supabase` wrapper, `_listen_to_auth_events`
does exactly that, so after `sign_in_with_password()` every
`auth.admin.*` call ran with the signed-in user's token instead of the
service key and failed with "User not allowed".

Copying the dict on the way in fixes it for every consumer, including code
that builds `GoTrueClient` or `GoTrueAdminAPI` directly, and makes in-place
header mutation safe by construction.

Fixes #1404
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Capability matrix drift detected

The following capabilities are marked implemented in the matrix but could not be found in python:

  • database.configuration.auto_retry → expected symbol: AsyncSelectRequestBuilder.retry
  • database.configuration.auto_retry → expected symbol: SyncSelectRequestBuilder.retry
  • realtime.client.connect → expected symbol: SyncRealtimeClient.connect
  • realtime.client.disconnect → expected symbol: AsyncRealtimeClient.disconnect
  • realtime.client.disconnect → expected symbol: SyncRealtimeClient.disconnect
  • realtime.channel.subscribe → expected symbol: SyncRealtimeChannel.subscribe
  • realtime.channel.unsubscribe → expected symbol: SyncRealtimeChannel.unsubscribe
  • realtime.channel.broadcast → expected symbol: SyncRealtimeChannel.send_broadcast
  • realtime.subscriptions.broadcast → expected symbol: SyncRealtimeChannel.on_broadcast
  • realtime.subscriptions.postgres_changes → expected symbol: SyncRealtimeChannel.on_postgres_changes
  • realtime.subscriptions.subscribe_presence → expected symbol: AsyncRealtimeChannel.on_presence_change
  • realtime.subscriptions.subscribe_presence → expected symbol: SyncRealtimeChannel.on_presence_change
  • realtime.presence.track → expected symbol: SyncRealtimeChannel.track
  • realtime.presence.untrack → expected symbol: SyncRealtimeChannel.untrack
  • realtime.presence.presence_state → expected symbol: AsyncRealtimeChannel.presences
  • realtime.presence.presence_state → expected symbol: SyncRealtimeChannel.presences

The following capabilities are marked implemented in python but have no registered symbols to verify:

  • auth.session.auto_refresh (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.cross_client_token_sync (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.oauth_flow_type (no symbols list — cannot confirm implementation exists)
  • client.session_management.custom_storage (no symbols list — cannot confirm implementation exists)
  • client.session_management.persist_session (no symbols list — cannot confirm implementation exists)
  • client.request_configuration.custom_http_client (no symbols list — cannot confirm implementation exists)
  • client.request_configuration.global_headers (no symbols list — cannot confirm implementation exists)
  • database.mutate.select_after_mutation (no symbols list — cannot confirm implementation exists)
  • database.using_filters.eq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.neq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.like (no symbols list — cannot confirm implementation exists)
  • database.using_filters.ilike (no symbols list — cannot confirm implementation exists)
  • database.using_filters.is (no symbols list — cannot confirm implementation exists)
  • database.using_filters.in (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contains (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contained_by (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_adjacent (no symbols list — cannot confirm implementation exists)
  • database.using_filters.overlaps (no symbols list — cannot confirm implementation exists)
  • database.using_filters.text_search (no symbols list — cannot confirm implementation exists)
  • database.using_filters.match (no symbols list — cannot confirm implementation exists)
  • database.using_filters.not (no symbols list — cannot confirm implementation exists)
  • database.using_filters.or (no symbols list — cannot confirm implementation exists)
  • database.using_filters.raw (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex_icase (no symbols list — cannot confirm implementation exists)
  • database.using_filters.not_in (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.order (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.limit (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.range (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.request_cancellation (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.relationship_embed (no symbols list — cannot confirm implementation exists)
  • database.configuration.request_timeout (no symbols list — cannot confirm implementation exists)
  • functions.invocation.region_selection (no symbols list — cannot confirm implementation exists)
  • functions.invocation.request_cancellation (no symbols list — cannot confirm implementation exists)
  • functions.invocation.timeout (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.postgres_changes_filter (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.private_channel (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_self (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_replay (no symbols list — cannot confirm implementation exists)
  • realtime.presence.presence_key (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.heartbeat_interval (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.access_bucket (no symbols list — cannot confirm implementation exists)

These may have been renamed, removed, or never registered. Please update the capability matrix.
See: https://github.com/supabase/sdk/blob/main/packages/capability-matrix/docs/capability-matrix.md

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.

2 participants