Check channel authentication compatibility
Use this guide to test whether a selected Supabase Auth instance can exchange a server-issued identity assertion for a real user session without email or phone. The compatibility harness and the API's channel authentication are both implemented. Both checks in this guide have passed against local Supabase Auth v2.197.0; no hosted project has been checked yet. Passing mocked tests does not prove interoperability with Supabase.
Prerequisites
- Install repository dependencies with
npm install. - Have a running Supabase instance and its public/publishable and administrative
keys in
apps/api/.env. The harness usesSUPABASE_URL,SUPABASE_PUBLISHABLE_KEY, andSUPABASE_SECRET_KEY. - Use a development or disposable test project. The
registercommand creates a dedicated custom provider;runcreates a disposable Auth identity and tests banning that identity. It does not migrate application accounts. - Choose an HTTPS issuer URL that the Auth service itself can reach and
trust. Auth v2.197.0 rejects HTTP issuers and loopback or private addresses
(
localhost,127.0.0.1, RFC 1918), with no configuration override. For local Supabase on OrbStack,https://host.docker.internal:4319passes because that name resolves to a non-private address inside the container. Check your own runtime before relying on it. - Have
opensslavailable to create a short-lived development CA, and permission to rundockeragainst the local Auth container.
The probe serves public discovery metadata and public signing keys over HTTPS. It exposes no signing endpoint and never prints private keys, access tokens, refresh tokens, raw assertions, or administrative credentials.
Configure the local probe
Run commands from the repository root. The npm script automatically reads
apps/api/.env; keep real secrets there rather than placing them in command
arguments. Review the sample configuration in
apps/api/scripts/channel-auth-compatibility/.env.example.
In each terminal that runs the harness, set these non-secret values:
export COMPAT_TARGET=local
export COMPAT_ISSUER_URL=https://host.docker.internal:4319
export COMPAT_PORT=4319
export COMPAT_PROVIDER=custom:bookygram-compatibility
export COMPAT_KEY_FILE=/private/tmp/bookygram-auth-compatibility.pem
export COMPAT_REPORT_FILE=../../agents-artifacts/channel-auth-compatibility.json
export COMPAT_TLS_CERT_FILE=/private/tmp/bookygram-compat-tls/issuer.pem
export COMPAT_TLS_KEY_FILE=/private/tmp/bookygram-compat-tls/issuer.key
serve refuses to start without both TLS files.
COMPAT_KEY_FILE must name a new file in an existing private directory. Key
creation refuses to overwrite an existing file and writes it with mode 0600.
The report path is relative to apps/api, the npm workspace's working directory.
Select another private path if /private/tmp is unavailable on your system.
Create a disposable signing key:
npm run auth:compatibility --workspace api -- prepare
Trust a development CA in local Auth
Create a short-lived CA and an issuer certificate for host.docker.internal in
a private directory:
mkdir -m 700 /private/tmp/bookygram-compat-tls
cd /private/tmp/bookygram-compat-tls
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem -days 7 -subj "/CN=Bookygram compatibility dev CA" -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -newkey rsa:2048 -nodes -keyout issuer.key -out issuer.csr -subj "/CN=host.docker.internal"
printf "subjectAltName=DNS:host.docker.internal\nextendedKeyUsage=serverAuth\n" > ext.cnf
openssl x509 -req -in issuer.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out issuer.pem -days 7 -extfile ext.cnf
Append the CA to the Auth container's trust bundle and restart it, because Auth
loads trusted CAs only at process start. The container name follows
supabase_auth_<project-id>:
docker exec -i -u root supabase_auth_bookygram-monorepo sh -c 'cat >> /etc/ssl/certs/ca-certificates.crt' < ca.pem
docker restart supabase_auth_bookygram-monorepo
This change only lasts until the container is recreated, for example by
supabase stop and supabase start. Repeat it afterwards. Never install this
CA in a hosted environment.
Serve the issuer
In the first terminal, serve the issuer:
npm run auth:compatibility --workspace api -- serve
Keep this process running. It listens with HTTPS on port 4319 on all host
interfaces so a local Auth runtime can fetch its public metadata and keys. Restrict access to
your development network. The process prints a readiness message without key
material. Stop it with Ctrl-C when testing is finished.
Register and inspect the test provider
In the second terminal, with the same configuration, register the dedicated provider:
npm run auth:compatibility --workspace api -- register
This uses a fixed test audience, bookygram-channel-compatibility, and enables
email-optional registration and nonce checking. It does not update or delete an
existing provider. For a previously registered provider using this key and
issuer, skip registration and inspect it:
npm run auth:compatibility --workspace api -- inspect
Auth requires authorization_endpoint, token_endpoint and jwks_uri in
discovery before it registers a provider. The probe lists
/oauth/authorize and /oauth/token. These endpoints exist but always answer
HTTP 400 with unsupported_response_type or unsupported_grant_type. They
never redirect, issue a code or issue a token. Discovery also advertises empty
response_types_supported and grant_types_supported. Never make these
endpoints succeed, and never disable nonce checking to force a pass.
Run the acceptance checks
npm run auth:compatibility --workspace api -- run
The harness checks provider configuration, invalid issuer/audience/nonce rejection, registration without contact data, repeated and concurrent identity reuse, the existing API token verifier, refresh rotation, current-session logout, and rejection of a banned user. Assertions are signed with RS256 and expire after 60 seconds; nonce claims contain the SHA-256 digest expected by the ID-token exchange.
The report is written to agents-artifacts/channel-auth-compatibility.json by
default. It contains origins, provider identifier, Auth version when available,
timestamps, and named checks. It omits user IDs and credentials. A zero exit code
and passed: true mean that all checks completed successfully against that
specific target. target: local is not evidence of hosted-project support.
The harness attempts to revoke all disposable sessions and restore any ban it applied, including after failed checks. The disposable Auth user and custom provider remain for operator inspection. It never automatically deletes Auth users or providers. If cleanup checks fail, resolve them in the test project before reusing it.
Diagnose a blocked result
| Result | Next action |
|---|---|
Registration HTTP 400, URL must use HTTPS | Use an HTTPS issuer on a non-loopback, non-private address. |
| Registration HTTP 400, discovery fetch failed | Check that the probe is serving and that the Auth container trusts its CA. Restart Auth after changing its trust bundle. |
auth_connection_failed | Check the configured Supabase origin and whether the command's runtime can reach it. A listening host port alone does not prove connectivity from an agent or container. |
| Registration rejected | Check Auth support for custom OIDC providers and inspect the Auth service's diagnostics privately. Do not copy raw error bodies into reports because they can contain credentials or claims. |
provider_preflight_failed | Inspect or register the dedicated test provider before attempting session checks. |
provider_issuer_mismatch | Ensure the server's issuer exactly matches the registered provider. |
provider_nonce_check_must_be_enabled | Restore nonce validation before testing. |
api_verifier_accepts_session fails | Check that Auth issues the configured project issuer and an ES256/RS256 access token, as required by the existing API. |
The probe sets bounded request timeouts, refuses redirects, and does not treat a server error or rate limit as proof of credential rejection. A failed or blocked check leaves the channel-authentication implementation gate unresolved.
Check the API as the issuer
The checks above use an issuer served by the harness. In deployment the API itself is the issuer, so repeat registration against it before trusting a target. This uses the same CA trust as above.
-
Configure the API as in Environment variables, with
CHANNEL_AUTH_ISSUER_URL=https://host.docker.internal:4319,CHANNEL_AUTH_ASSERTION_AUDIENCE=bookygram-channel-compatibilityandCHANNEL_AUTH_SUPABASE_PROVIDER=custom:bookygram-compatibility, and start it withnpm run dev --workspace api. -
Put a local-only TLS-terminating proxy on port
4319in front of the API's HTTP port, using theissuer.pemandissuer.keycreated above. Do not run the harnessservecommand at the same time. -
Check discovery. Expected:
200withCache-Control: no-store, and a JWKS with one key:curl -sS --cacert /private/tmp/bookygram-compat-tls/ca.pem -i https://host.docker.internal:4319/.well-known/openid-configurationcurl -sS --cacert /private/tmp/bookygram-compat-tls/ca.pem https://host.docker.internal:4319/.well-known/jwks.json -
With
COMPAT_ISSUER_URLunchanged, run the harnessregistercommand, orinspectif the provider exists. Supabase Auth fetches discovery from the API during registration. Expected: both exit0. -
Sign in through the API's public endpoints with launch data signed by a configured test bot, and check: the first exchange answers
200with onlyaccessToken,refreshTokenandexpiresAt; the same launch data from a new context answers409 proof_consumed; the same Telegram user through the other bot gets the same Supabase subject; refresh rotates the refresh token; after logout, refresh answers401 session_invalid.
Do not run the harness run command against the API-hosted issuer. It signs
assertions with the harness key, which the API's JWKS does not publish. Its
assertion-level checks, such as nonce and audience rejection and banning, do
not depend on where the issuer is hosted.
Hosted targets
For a hosted target, select COMPAT_TARGET=hosted, use HTTPS origins and a
separately reachable HTTPS issuer, and load that test project's configuration.
Never reuse a local pass as hosted production acceptance evidence.
Verify the harness locally
These checks use fake Auth responses and an injected issuer server; they do not need a running Supabase instance:
npm test --workspace api -- scripts/channel-auth-compatibility/compatibility.test.ts
npm run auth:compatibility:typecheck --workspace api
See Authentication for the business API's current token verification and Supabase for existing infrastructure.