I'm building an API application with Symfony 6.2 and trying to use Access Tokens for stateless authentication. As suggested in symfony docs I have implemented AccessTokenHandler class
<?php
# src\Security\AccessTokenHandler.php
namespace App\Security;
use App\Repository\AccessTokenRepository;
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 $repository
) {
}
public function getUserBadgeFrom( string $accessToken ): UserBadge {
// e.g. query the "access token" database to search for this token
$accessToken = $this->repository->findValidToken($accessToken);
if ( NULL === $accessToken || !$accessToken->isValid() ) {
throw new BadCredentialsException( 'Invalid credentials.' );
}
/* DEBUG: UNCOMMENT FOR DEBUG
dump($accessToken);die;
/* DEBUG /**/
// and return a UserBadge object containing the user identifier from the found token
return new UserBadge( $accessToken->getUserId() );
}
}
and configured my security.yaml file
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
access_token_provider:
entity:
class: App\Entity\AccessToken
property: tokenValue
firewalls:
main:
lazy: true
provider: access_token_provider
stateless: true
pattern: ^/api
access_token:
token_extractors: header
token_handler: App\Security\AccessTokenHandler
access_control:
- { path: ^/api, roles: ROLE_ADMIN }
In the database there is the only user in User table and related token in AccessToken table.
To test the above I'm using Postman to send a GET request to https://my-host/api/test-page with the following header:
Authorization: Bearer 59e4fb3f9c10e0fe570d486462ab417a
It gets into AccessTokenHandler::getUserBadgeFrom as expected and returns the correct token with user (see the commented debug code on AccessTokenHandler:23).
Yet, the request returns status code 401 Unauthorized with WWW-Authenticate: Bearer error="invalid_token",error_description="Invalid credentials." header.
It took me a while to figure out what the issue was. The above configuration is almost correct; however, a Symfony security component was missing a proper user provider to retrieve a User record even though it had the correct identifier (AccessToken).
So I had to do following steps to get the things working:
Step #1: Implement
AccessTokenUserProviderclass as follows:Step #2: Register user provider in
services.yamlStep #3: and finally configure its usage in
security.yaml:And this is it! Now Symfony uses our AccessTokenUserProvider to load corresponding user record from the database.
I hope this will save time for anyone who follows the same path! :)