API tokens
A mobile app or another service does not log in through a form or keep a session. It sends a token — a long random string — with every request. In Symfony the access_token setting turns on token checking.
A separate firewall
An API and a site are protected differently, so the /api addresses get a section of their own:
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
access_token:
token_handler: App\Security\AccessTokenHandler
main:
# the regular form login
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: ROLE_USER }stateless: true turns the session off: every request is checked afresh.
The token entity
#[ORM\Entity(repositoryClass: AccessTokenRepository::class)]
class AccessToken
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 64, unique: true)]
private string $hash;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private User $user;
#[ORM\Column]
private \DateTimeImmutable $expiresAt;
public function isValid(): bool
{
return $this->expiresAt > new \DateTimeImmutable();
}
// the constructor and the get methods
}The database stores not the token itself but its hash.
The token handler
namespace App\Security;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
class AccessTokenHandler implements AccessTokenHandlerInterface
{
public function __construct(private AccessTokenRepository $tokens) {}
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
{
$token = $this->tokens->findOneBy(['hash' => hash('sha256', $accessToken)]);
if (!$token || !$token->isValid()) {
throw new BadCredentialsException('Invalid token');
}
return new UserBadge($token->getUser()->getUserIdentifier());
}
}Symfony takes the token out of the Authorization: Bearer … header by itself and passes it to the method.
Issuing a token
#[Route('/api/login', name: 'api_login', methods: ['POST'])]
public function login(
#[MapRequestPayload] LoginInput $input,
UserRepository $users,
UserPasswordHasherInterface $hasher,
EntityManagerInterface $em,
): JsonResponse {
$user = $users->findOneBy(['email' => $input->email]);
if (!$user || !$hasher->isPasswordValid($user, $input->password)) {
return $this->json(['error' => 'Wrong email or password'], 401);
}
$plain = bin2hex(random_bytes(32));
$em->persist(new AccessToken(hash('sha256', $plain), $user, new \DateTimeImmutable('+30 days')));
$em->flush();
return $this->json(['token' => $plain]);
}The current user
use Symfony\Component\Security\Http\Attribute\CurrentUser;
#[Route('/api/me', name: 'api_me')]
public function me(#[CurrentUser] User $user): JsonResponse
{
return $this->json(['email' => $user->getEmail()]);
}A client request
curl https://example.com/api/me -H "Authorization: Bearer 3f9a1c..."Without a token, or with an expired one, the response is a 401 code.
JWT
Another widespread kind of token is JWT: the user data is kept in the token itself and signed with a key, so the database need not be queried on every request. The lexik/jwt-authentication-bundle package adds it. The downside: an issued JWT cannot be revoked before it expires.
Common mistake
A token stored in the database in plain form is the same as a plain password: a database leak hands over access to every account. Store a hash and show the token itself to the client once. And do not pass a token in the request address: addresses end up in server logs and browser history.