## Getting Started
### Minimal Steps
1. **Generate Keys**
Run the provided OpenSSL commands in `docs/generate_keys.md` to create `private.pem` and `public.pem` files. Store them securely (e.g., `config/jwt/`).
Example for `ES256`:
```bash
openssl genpkey -out config/jwt/private.pem -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -aes-256-cbc -pass pass:${APP_SECRET}
openssl pkey -in config/jwt/private.pem -passin pass:${APP_SECRET} -out config/jwt/public.pem -pubout
Install & Configure
composer require atlance/jwt-auth ^7.0
Copy the default config to config/packages/atlance_jwt_auth.yaml and update .env with:
ATLANCE_JWT_AUTH_PRIVATE_KEY_PATH=%kernel.project_dir%/config/jwt/private.pem
ATLANCE_JWT_AUTH_PUBLIC_KEY_PATH=%kernel.project_dir%/config/jwt/public.pem
ATLANCE_JWT_AUTH_ALGORITHM=ES256 # or RS256, ES384, etc.
First Use Case: Login Endpoint
Create a controller using the Create\Token\Handler to issue JWTs:
#[Route('/login', methods: ['POST'])]
public function login(
Request $request,
UserProviderInterface $userProvider,
UserPasswordHasherInterface $passwordHasher,
HandlerInterface $tokenHandler
): JsonResponse {
$data = json_decode($request->getContent(), true);
$user = $userProvider->loadUserByIdentifier($data['username']);
if (!$passwordHasher->isPasswordValid($user, $data['password'])) {
return new JsonResponse(['error' => 'Invalid credentials'], 401);
}
return new JsonResponse(['token' => $tokenHandler->handle($user)]);
}
Protect Routes
Update security.yaml to enable JWT access:
security:
firewalls:
main:
access_token:
token_handler: Atlance\JwtAuth\Security\Factory\UserBadgeFactory
Now, annotate controllers with [IsGranted] to enforce authentication:
#[IsGranted('ROLE_USER')]
#[Route('/profile')]
public function profile(#[CurrentUser] UserInterface $user): JsonResponse {
return new JsonResponse(['username' => $user->getUserIdentifier()]);
}
Token Issuance
Create\Token\HandlerInterface to generate JWTs for authenticated users.exp, iat, or custom claims):
$handler->handle($user, [
'exp' => (new \DateTimeImmutable('+1 hour'))->getTimestamp(),
'custom_claim' => 'value',
]);
Token Validation
AccessTokenAuthenticator, so validation is automatic.[CurrentUser] attribute or $this->getUser() in controllers.Role-Based Access
[IsGranted] attributes:
#[IsGranted('ROLE_ADMIN')]
public function adminDashboard(): JsonResponse { ... }
Refresh Tokens
HandlerInterface with a longer expiry:
$refreshTokenHandler->handle($user, ['exp' => (new \DateTimeImmutable('+7 days'))->getTimestamp()]);
Custom User Providers
Extend Symfony’s UserProviderInterface to fetch users from databases/APIs:
class CustomUserProvider implements UserProviderInterface {
public function loadUserByIdentifier(string $identifier): UserInterface {
return $this->userRepository->findByUsername($identifier);
}
// ... other methods
}
Token Storage
Store JWTs in HTTP-only cookies for better security (use Symfony’s Response helpers):
$response = new JsonResponse();
$response->headers->setCookie(new Cookie('jwt', $token, [
'httponly' => true,
'secure' => true,
'samesite' => 'Strict',
'expires' => (new \DateTimeImmutable('+1 hour'))->format('D, d M Y H:i:s T'),
]));
Testing
Mock the HandlerInterface or use Symfony’s TestClient with pre-authenticated requests:
$client = static::createClient();
$client->request('POST', '/login', [], [], [
'HTTP_Authorization' => 'Bearer ' . $validToken,
]);
Key Management
ALGORITHM in .env matches the key type (e.g., ES256 for EC keys, RS256 for RSA).Token Expiry
atlance_jwt_auth:
ttl: 3600 # 1 hour in seconds
User Provider Quirks
loadUserByIdentifier must return a UserInterface. Custom users must implement this interface.Symfony Integration
access_token to a firewall will break JWT auth.Authorization headers:
# config/packages/nelmio_cors.yaml
path_patterns:
- ^/api
allow_credentials: true
allow_headers: ['Authorization']
Invalid Tokens
public.pem path in .env and ensure it’s readable.Bearer <token>).Permission Denied
[IsGranted] roles match the user’s roles (e.g., ROLE_USER).dd($user->getRoles()) to debug user roles.Performance
UserProvider is optimized (e.g., indexed usernames).Custom Claims
Extend the HandlerInterface to add dynamic claims:
class CustomTokenHandler implements HandlerInterface {
public function handle(UserInterface $user, array $customClaims = []): string {
$claims = array_merge($customClaims, [
'user_id' => $user->getId(),
'scopes' => $user->getRoles(),
]);
return $this->jwtBuilder->build($claims);
}
}
Token Revocation Implement a blacklist or short-lived tokens with refresh logic:
// Store revoked tokens in a database table
$revokedTokens = $this->revokedTokenRepository->findBy(['token' => $token]);
if ($revokedTokens->count() > 0) {
throw new \Symfony\Component\Security\Core\Exception\AuthenticationException();
}
Multi-Tenancy Add tenant ID to the JWT payload:
$handler->handle($user, ['tenant_id' => $user->getTenantId()]);
Then validate in middleware:
$tenantId = $token->getClaim('tenant_id');
if ($tenantId !== $request->get('tenant_id')) {
throw new AccessDeniedException();
}
Logging Log token issuance/rejection for auditing:
$this->logger->info('JWT issued', [
'user_id' => $user->getId(),
'ip' => $request->getClientIp(),
]);
ttl in config is in seconds, not minutes/hours.ClockInterface. Override for testing:
services:
Symfony\Component\Clock\ClockInterface: '@custom_clock'
How can I help you explore Laravel packages today?