jonston / symfony-sanctum-bundle
Simple token-based authentication for Symfony, inspired by Laravel Sanctum. Provides easy authentication via API tokens without the complexity of OAuth.
Installs: 13
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.1
- doctrine/doctrine-bundle: ^2.0
- doctrine/orm: ^2.0
- symfony/framework-bundle: ^6.0|^7.0
- symfony/security-bundle: ^6.0|^7.0
Requires (Dev)
- phpunit/phpunit: ^9.0
- symfony/test-pack: ^1.0
This package is auto-updated.
Last update: 2025-09-04 15:48:00 UTC
README
Simple token-based authentication for Symfony, inspired by Laravel Sanctum. Provides easy API token authentication without OAuth complexity.
Features
- ✅ Token-based API authentication
- ✅ Personal access token creation and management
- ✅ Automatic token verification from HTTP headers
- ✅ Token expiration support
- ✅ Last usage tracking
- ✅ Simple Symfony Security integration
Requirements
- PHP 8.1+
- Symfony 6.0+ or 7.0+
- Doctrine ORM 2.0+
Installation
1. Install via Composer
composer require jonston/symfony-sanctum-bundle
2. Register the Bundle
In config/bundles.php
:
return [ // ... Jonston\SanctumBundle\SanctumBundle::class => ['all' => true], ];
3. Create Database Table
# Create migration based on Entity php bin/console doctrine:migrations:diff # Run migration php bin/console doctrine:migrations:migrate # Or update schema directly (for development) php bin/console doctrine:schema:update --force
4. Configure Security
In config/packages/security.yaml
:
security: firewalls: api: pattern: ^/api stateless: true custom_authenticators: - Jonston\SanctumBundle\Security\TokenAuthenticator main: # Your main configuration
Usage
Creating Tokens
<?php use Jonston\SanctumBundle\Service\TokenManager; use Symfony\Component\Security\Core\User\UserInterface; class ApiController extends AbstractController { public function __construct( private readonly TokenManager $tokenManager ) {} #[Route('/api/tokens', methods: ['POST'])] public function createToken(UserInterface $user): JsonResponse { // Create token without expiration $result = $this->tokenManager->createToken($user, 'Mobile App'); // Create token with expiration $expiresAt = new \DateTimeImmutable('+30 days'); $result = $this->tokenManager->createToken($user, 'Web App', $expiresAt); return new JsonResponse([ 'token' => $result['token'], // Give this token to client 'name' => $result['entity']->getName(), 'expires_at' => $result['entity']->getExpiresAt()?->format('Y-m-d H:i:s') ]); } }
Using Tokens
Client should send token in Authorization
header:
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
http://localhost:8000/api/user
Managing Tokens
<?php class TokenController extends AbstractController { public function __construct( private readonly TokenManager $tokenManager ) {} // Get all tokens for current user #[Route('/api/tokens', methods: ['GET'])] public function getUserTokens(): JsonResponse { $user = $this->getUser(); $tokens = $this->tokenManager->getUserTokens($user); return new JsonResponse(array_map(function($token) { return [ 'id' => $token->getId(), 'name' => $token->getName(), 'last_used_at' => $token->getLastUsedAt()?->format('Y-m-d H:i:s'), 'expires_at' => $token->getExpiresAt()?->format('Y-m-d H:i:s'), ]; }, $tokens)); } // Revoke specific token #[Route('/api/tokens/{id}', methods: ['DELETE'])] public function revokeToken(int $id, PersonalAccessTokenRepository $repository): JsonResponse { $token = $repository->find($id); if (!$token || $token->getUserId() !== (int) $this->getUser()->getUserIdentifier()) { throw $this->createNotFoundException(); } $this->tokenManager->revokeToken($token); return new JsonResponse(['message' => 'Token revoked']); } // Revoke all user tokens #[Route('/api/tokens', methods: ['DELETE'])] public function revokeAllTokens(): JsonResponse { $this->tokenManager->revokeAllTokensForUser($this->getUser()); return new JsonResponse(['message' => 'All tokens revoked']); } }
Cleaning Expired Tokens
Create a command for regular cleanup:
<?php // src/Command/CleanupTokensCommand.php use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Jonston\SanctumBundle\Service\TokenManager; class CleanupTokensCommand extends Command { protected static $defaultName = 'sanctum:cleanup'; public function __construct(private readonly TokenManager $tokenManager) { parent::__construct(); } protected function execute(InputInterface $input, OutputInterface $output): int { $count = $this->tokenManager->cleanupExpiredTokens(); $output->writeln("Removed {$count} expired tokens"); return Command::SUCCESS; } }
Run on schedule:
# Manually php bin/console sanctum:cleanup # Or add to crontab 0 2 * * * /path/to/your/app/bin/console sanctum:cleanup
Protecting Routes
Basic Protection
#[Route('/api/user', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED')] public function getUser(): JsonResponse { return new JsonResponse([ 'id' => $this->getUser()->getUserIdentifier(), 'email' => $this->getUser()->getEmail(), ]); }
Configuration-based
# config/packages/security.yaml security: access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } - { path: ^/api, roles: IS_AUTHENTICATED }
API Reference
TokenManager
createToken(UserInterface $user, string $name, ?\DateTimeImmutable $expiresAt = null): array
Creates a new token for user.
Parameters:
$user
- user to create token for$name
- token name (e.g., "Mobile App")$expiresAt
- expiration date (optional)
Returns: array with token
(string for client) and entity
(database object) keys
revokeToken(PersonalAccessToken $token): void
Removes specified token.
revokeAllTokensForUser(UserInterface $user): void
Removes all user tokens.
getUserTokens(UserInterface $user): array
Returns all active user tokens.
cleanupExpiredTokens(): int
Removes all expired tokens. Returns count of deleted records.
PersonalAccessToken Entity
Main methods:
getName(): ?string
- token namegetCreatedAt(): ?\DateTimeImmutable
- creation dategetExpiresAt(): ?\DateTimeImmutable
- expiration dategetLastUsedAt(): ?\DateTimeImmutable
- last usagegetUserId(): ?int
- user IDisExpired(): bool
- expiration check
Security
Token Hashing
Tokens are stored in database as SHA-256 hash. Original token is only visible at creation time.
Protection from Attacks
- Use HTTPS in production
- Regularly clean expired tokens
- Set reasonable token expiration times
- Monitor suspicious activity
Best Practices
Token Naming
Give tokens meaningful names:
$tokenManager->createToken($user, 'iPhone App - John'); $tokenManager->createToken($user, 'CI/CD Pipeline'); $tokenManager->createToken($user, 'Postman Testing');
Expiration Management
// Short tokens for automated systems $shortTerm = new \DateTimeImmutable('+1 hour'); $tokenManager->createToken($user, 'CI Build', $shortTerm); // Long tokens for mobile apps $longTerm = new \DateTimeImmutable('+90 days'); $tokenManager->createToken($user, 'Mobile App', $longTerm);
Troubleshooting
Token Not Accepted
- Check header format:
Authorization: Bearer YOUR_TOKEN
- Verify token hasn't expired
- Check firewall configuration in security.yaml
User Not Loading
- Ensure UserProvider is configured correctly
- Check that user_id in token matches actual user ID
- Verify user exists in system
Testing
Run the test suite:
# Install dev dependencies composer install --dev # Run tests ./vendor/bin/phpunit # Run tests with coverage ./vendor/bin/phpunit --coverage-html coverage
Contributing
- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature
) - Commit changes (
git commit -m 'Add amazing feature'
) - Push to branch (
git push origin feature/amazing-feature
) - Open Pull Request
License
MIT
Support
- GitHub Issues: https://github.com/jonston/symfony-sanctum-bundle/issues
- Documentation: https://github.com/jonston/symfony-sanctum-bundle/wiki