kyzegs / supabase-auth-bundle
A reusable Symfony bundle for stateless authentication via Supabase-issued JWTs.
Package info
github.com/Kyzegs/supabase-auth-bundle
Type:symfony-bundle
pkg:composer/kyzegs/supabase-auth-bundle
Requires
- php: ^8.3
- symfony/cache-contracts: ^3.4
- symfony/clock: ^7.2|^8.0
- symfony/config: ^7.2|^8.0
- symfony/dependency-injection: ^7.2|^8.0
- symfony/http-client-contracts: ^3.4
- symfony/http-kernel: ^7.2|^8.0
- symfony/security-core: ^7.2|^8.0
- symfony/security-http: ^7.2|^8.0
- web-token/jwt-library: ^4.1.4
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0|^12.0
- symfony/cache: ^7.2|^8.0
- symfony/framework-bundle: ^7.2|^8.0
- symfony/http-client: ^7.2|^8.0
- symfony/security-bundle: ^7.2|^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 12:19:02 UTC
README
Stateless Symfony authentication via Supabase-issued JWTs.
The bundle verifies a Supabase JWT (signature against the project JWKS, plus
exp and iss checks) and hands the verified claims to an application-provided
resolver that maps the Supabase identity to your own user. Verification logic
lives here once; the only application glue is the resolver.
Installation
composer require kyzegs/supabase-auth-bundle
If you don't use Symfony Flex, register the bundle manually in
config/bundles.php:
return [ // ... Kyzegs\SupabaseAuthBundle\SupabaseAuthBundle::class => ['all' => true], ];
Configuration
config/packages/supabase_auth.yaml:
supabase_auth: url: '%env(SUPABASE_URL)%' # required, absolute http(s) URL, no trailing slash jwks_ttl: 600 # optional, JWKS cache TTL in seconds leeway: 0 # optional, allowed clock skew in seconds audience: 'authenticated' # optional, required `aud` claim value algorithms: ['RS256', 'ES256'] # optional, accepted signature algorithms when@test: supabase_auth: test_mode: true # accept "test-user-{id}" tokens instead of real JWTs
| Option | Default | Description |
|---|---|---|
url |
(required) | Supabase project URL, e.g. https://xxxx.supabase.co. Must be an absolute http(s) URL. |
jwks_ttl |
600 |
Seconds to cache the JWKS (matches the Supabase edge cache). |
leeway |
0 |
Allowed clock skew (seconds) for the exp, iat and nbf checks. |
audience |
null |
When set, the token aud claim must contain this value (Supabase uses authenticated). |
algorithms |
[RS256, ES256] |
Signature algorithms accepted from the JWKS. Must be a subset of RS256, ES256. |
test_mode |
false |
Swap the handler for one accepting {prefix}{identifier} tokens. |
test_token_prefix |
test-user- |
Bearer-token prefix recognised in test mode. |
Invalid configuration (an unsupported algorithm, a negative jwks_ttl/leeway)
fails fast at container compile time. The url is validated at runtime instead,
so an %env(SUPABASE_URL)% value — which is unresolved during compilation — is
accepted; a non-http(s) URL fails on first token verification.
Failure responses
| Cause | Response |
|---|---|
| The token is malformed, has an invalid signature or fails claim checks | 401 |
| The JWKS cannot be fetched (5 s timeout) or parsed, or holds no keys | 503 |
url is empty or not an absolute http(s) URL, or no valid algorithms are configured |
503 |
A 503 is a ServiceUnavailableHttpException with a generic message and the
cause as its previous exception, so clients can tell it apart from a rejected
token, and Symfony logs it at critical level. These failures are not cached: once
the JWKS is available again, the next request succeeds.
Wiring the firewall
Point your firewall's access-token handler at the public service id
supabase_auth.token_handler:
# config/packages/security.yaml security: firewalls: api: stateless: true access_token: token_handler: supabase_auth.token_handler
Provide a user resolver
Implement SupabaseUserResolverInterface to map verified claims (typically the
sub claim) to your security user, and alias the interface to it:
namespace App\Security; use App\Repository\UserRepositoryInterface; use Kyzegs\SupabaseAuthBundle\Contract\SupabaseUserResolverInterface; use Symfony\Component\Security\Core\User\UserInterface; final class SupabaseUserResolver implements SupabaseUserResolverInterface { public function __construct(private UserRepositoryInterface $users) {} public function resolve(array $claims): ?UserInterface { return $this->users->find($claims['sub']); } }
# config/services.yaml services: Kyzegs\SupabaseAuthBundle\Contract\SupabaseUserResolverInterface: '@App\Security\SupabaseUserResolver'
Returning null from resolve() produces an authentication failure.
Testing your application
With test_mode: true, send Authorization: Bearer test-user-{identifier} and
the bundle calls your resolver with ['sub' => '{identifier}'] — no real JWT
required.
License
MIT