Skip to main content

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 uses SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, and SUPABASE_SECRET_KEY.
  • Use a development or disposable test project. The register command creates a dedicated custom provider; run creates 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:4319 passes because that name resolves to a non-private address inside the container. Check your own runtime before relying on it.
  • Have openssl available to create a short-lived development CA, and permission to run docker against 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​

ResultNext action
Registration HTTP 400, URL must use HTTPSUse an HTTPS issuer on a non-loopback, non-private address.
Registration HTTP 400, discovery fetch failedCheck that the probe is serving and that the Auth container trusts its CA. Restart Auth after changing its trust bundle.
auth_connection_failedCheck 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 rejectedCheck 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_failedInspect or register the dedicated test provider before attempting session checks.
provider_issuer_mismatchEnsure the server's issuer exactly matches the registered provider.
provider_nonce_check_must_be_enabledRestore nonce validation before testing.
api_verifier_accepts_session failsCheck 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.

  1. Configure the API as in Environment variables, with CHANNEL_AUTH_ISSUER_URL=https://host.docker.internal:4319, CHANNEL_AUTH_ASSERTION_AUDIENCE=bookygram-channel-compatibility and CHANNEL_AUTH_SUPABASE_PROVIDER=custom:bookygram-compatibility, and start it with npm run dev --workspace api.

  2. Put a local-only TLS-terminating proxy on port 4319 in front of the API's HTTP port, using the issuer.pem and issuer.key created above. Do not run the harness serve command at the same time.

  3. Check discovery. Expected: 200 with Cache-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-configuration
    curl -sS --cacert /private/tmp/bookygram-compat-tls/ca.pem https://host.docker.internal:4319/.well-known/jwks.json
  4. With COMPAT_ISSUER_URL unchanged, run the harness register command, or inspect if the provider exists. Supabase Auth fetches discovery from the API during registration. Expected: both exit 0.

  5. Sign in through the API's public endpoints with launch data signed by a configured test bot, and check: the first exchange answers 200 with only accessToken, refreshToken and expiresAt; the same launch data from a new context answers 409 proof_consumed; the same Telegram user through the other bot gets the same Supabase subject; refresh rotates the refresh token; after logout, refresh answers 401 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.