{"id":27261407,"url":"https://github.com/rakotomandimby/symfony-enable-database-authentication","last_synced_at":"2026-05-10T19:04:56.730Z","repository":{"id":287148473,"uuid":"963756720","full_name":"rakotomandimby/symfony-enable-database-authentication","owner":"rakotomandimby","description":"How to enable authentication against a PostgreSQL database after a minimal Symfony installation","archived":false,"fork":false,"pushed_at":"2025-04-10T08:15:21.000Z","size":8,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-10T08:21:15.663Z","etag":null,"topics":["jwt","php","postgresql","symfony"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rakotomandimby.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-04-10T06:56:14.000Z","updated_at":"2025-04-10T08:15:24.000Z","dependencies_parsed_at":"2025-04-10T08:21:17.037Z","dependency_job_id":"8542b040-ecbe-4f82-a62c-df0154995acf","html_url":"https://github.com/rakotomandimby/symfony-enable-database-authentication","commit_stats":null,"previous_names":["rakotomandimby/symfony-enable-database-authentication"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rakotomandimby%2Fsymfony-enable-database-authentication","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rakotomandimby%2Fsymfony-enable-database-authentication/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rakotomandimby%2Fsymfony-enable-database-authentication/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rakotomandimby%2Fsymfony-enable-database-authentication/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rakotomandimby","download_url":"https://codeload.github.com/rakotomandimby/symfony-enable-database-authentication/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248347355,"owners_count":21088628,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jwt","php","postgresql","symfony"],"created_at":"2025-04-11T05:30:42.935Z","updated_at":"2026-05-10T19:04:56.724Z","avatar_url":"https://github.com/rakotomandimby.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Enable Symfony authentication against database\n\n**Note**: Database here is a PostgreSQL database.\n\nSetup the database and create a user in the database.\n\n```sql\nCREATE TABLE public.users (\n    id SERIAL NOT NULL,\n    email CHARACTER VARYING(255) NOT NULL,\n    password CHARACTER VARYING(255) NOT NULL,\n    first_name CHARACTER VARYING(512),\n    last_name CHARACTER VARYING(512),\n    roles CHARACTER VARYING(1024)\n);\n```\n\nCreate the user:\n```sql\nINSERT INTO public.users (\n    email, \n    password, \n    first_name, \n    last_name, \n    roles) \nVALUES (\n    'mr@rktmb.org', \n    '$2a$12$AbZtZJCEB8qnu2ZCAcQIE.4wOlO1RM4H7eec8y4Fmaehtvrwu9SaW', \n    'Miha', \n    'RKTMB', \n    'ROLE_ADMIN,ROLE_USER');\n```\n\nThe password is `mihamina` hashed with any [online bcrypt generator](https://www.google.com/search?client=firefox-b-d\u0026q=bcrypt+online)\n\nCreate a Symfony project:\n\n```bash\nsymfony new my_project_name --no-git\n```\n\nInstall the security and JWT bundles:\n\n```bash\ncomposer require symfony/security-bundle lexik/jwt-authentication-bundle\n```\n\n\nDefine `User` entity in `src/Entity/User.php`:\n\n```php\n\u003c?php\nnamespace App\\Entity;\n\n\nuse Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\n\nclass User implements UserInterface, PasswordAuthenticatedUserInterface\n{\n  private ?int $id;\n  private ?string $email;\n  private ?string $password;\n  private ?string $firstName;\n  private ?string $lastName;\n  // roles\n  private ?string $roles;\n\n  public function __construct(\n    ?int $id = null, \n    ?string $email = null, \n    ?string $password = null,\n    ?string $firstName = null,\n    ?string $lastName = null,\n    ?string $roles = null\n  )\n  {\n    $this-\u003eid = $id;\n    $this-\u003eemail = $email;\n    $this-\u003epassword = $password;\n    $this-\u003efirstName = $firstName;\n    $this-\u003elastName = $lastName;\n    $this-\u003eroles = $roles; \n  }\n\n  public function getRoles(): array\n  {\n    // Roles are coma separated in the database\n    // we need to explode them\n    return explode(',', $this-\u003eroles);\n  }\n\n  public function setRoles(?array $roles): self\n  {\n    $this-\u003eroles = implode(',', $roles);\n    return $this;\n  }\n\n  public function eraseCredentials():void\n  {\n  }\n\n  public function getUserIdentifier(): string\n  {\n    return $this-\u003eemail;\n  }\n\n  public function getId(): ?int\n  {\n    return $this-\u003eid;\n  }\n\n  public function getEmail(): ?string\n  {\n    return $this-\u003eemail;\n  }\n\n  public function setEmail(?string $email): self\n  {\n    $this-\u003eemail = $email;\n    return $this;\n  }\n  \n  public function getUsername(): ?string\n  {\n    return $this-\u003eemail;\n  }\n\n  public function setUsername(?string $email): self\n  {\n    $this-\u003eemail = $email;\n    return $this;\n  }\n\n  public function getPassword(): ?string\n  {\n    return $this-\u003epassword;\n  }\n\n  public function setPassword(?string $password): self\n  {\n    $this-\u003epassword = $password;\n    return $this;\n  }\n\n  public function getFirstName(): ?string\n  {\n    return $this-\u003efirstName;\n  }\n\n  public function setFirstName(?string $firstName): self\n  {\n    $this-\u003efirstName = $firstName;\n    return $this;\n  }\n\n  public function getLastName(): ?string\n  {\n    return $this-\u003elastName;\n  }\n\n  public function setLastName(?string $lastName): self\n  {\n    $this-\u003elastName = $lastName;\n    return $this;\n  }\n}\n```\n\nDefine the `UserProvider` in `src/Security/UserProvider.php`:\n\n```php\n\u003c?php\n\nnamespace App\\Security;\n\nuse App\\Repository\\UserRepository;\nuse Symfony\\Component\\Security\\Core\\Exception\\UserNotFoundException;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserProviderInterface;\n\nclass UserProvider implements UserProviderInterface\n{\n  private UserRepository $userRepository;\n\n  public function __construct(UserRepository $userRepository)\n  {\n    $this-\u003euserRepository = $userRepository;\n  }\n\n  public function loadUserByIdentifier(string $identifier): UserInterface\n  {\n    $user = $this-\u003euserRepository-\u003efindOneByEmail($identifier);\n\n    if (!$user) {\n      throw new UserNotFoundException();\n    }\n\n    return $user;\n  }\n\n  public function refreshUser(UserInterface $user): UserInterface\n  {\n    /** @var App\\Entity\\User|null $user */\n    return $this-\u003eloadUserByIdentifier($user-\u003egetEmail());\n  }\n\n  public function supportsClass(string $class): bool\n  {\n    return $class === 'App\\Entity\\User';\n  }\n}\n```\nDefine the `UserRepository` in `src/Repository/UserRepository.php`:\n\n```php\n\u003c?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\User;\n\nclass UserRepository\n{\n  private \\PDO $conn;\n\n  /**\n   * Maps standard URL schemes to PHP's PDO driver names.\n   */\n  private const SCHEME_TO_DRIVER_MAP = [\n      'postgresql' =\u003e 'pgsql',\n      'mysql' =\u003e 'mysql',\n  ];\n\n  public function __construct()\n  {\n      $dbUrl = getenv('DATABASE_URL');\n      if ($dbUrl === false) {\n          throw new \\RuntimeException('DATABASE_URL environment variable is not set.');\n      }\n\n      $db = parse_url($dbUrl);\n      if ($db === false || !isset($db['scheme'])) {\n          throw new \\RuntimeException('Could not parse DATABASE_URL.');\n      }\n\n      $driver = self::SCHEME_TO_DRIVER_MAP[$db['scheme']] ?? null;\n      if ($driver === null) {\n          throw new \\RuntimeException(\"Unsupported database driver: {$db['scheme']}\");\n      }\n\n      $host = $db['host'];\n      $port = $db['port'];\n      $user = $db['user'];\n      $pass = $db['pass'];\n      $dbname = ltrim($db['path'], '/');\n\n      $dsn = \"{$driver}:host={$host};port={$port};dbname={$dbname}\";\n\n      $this-\u003econn = new \\PDO($dsn, $user, $pass);\n      $this-\u003econn-\u003esetAttribute(\\PDO::ATTR_ERRMODE, \\PDO::ERRMODE_EXCEPTION);\n  }\n\n  public function findOneByEmail(string $email): ?User\n  {\n    $stmt = $this-\u003econn-\u003eprepare('SELECT id, email, password, first_name, last_name, roles FROM users WHERE email = :email');\n    $stmt-\u003eexecute([':email' =\u003e $email]);\n\n    if ($row = $stmt-\u003efetch(\\PDO::FETCH_ASSOC)) {\n      return new User(\n        $row['id'],\n        $row['email'],\n        $row['password'],\n        $row['first_name'],\n        $row['last_name'],\n        $row['roles']\n      );\n    }\n\n    return null;\n  }\n\n  // findOneByUsername(), which is the same as findOneByEmail(), so just wrap it\n  public function findOneByUsername(string $username): ?User\n  {\n    return $this-\u003efindOneByEmail($username);\n  }\n}\n```\n\nConfigure dependency injection for `UserRepository` in `config/services.yaml`:\n\n**Note** : Here, we just append the new content to the existing `services.yaml` file.\n\n```yaml\n### existing services.yaml content ###\nservices:\n    ### existing service definitions ###\n    App\\Repository\\UserRepository:\n        arguments: ['%env(DATABASE_URL)%']\n```\n\nConfigure the security in `config/packages/security.yaml`:\n\n**Note** : Here, we replace the whole file with the following content.\n\n```yaml\nsecurity:\n    password_hashers:\n        Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface: 'auto'\n    providers:\n        database:\n            id: App\\Security\\UserProvider\n    firewalls:\n        dev:\n            pattern: ^/(_(profiler|wdt)|css|images|js)/\n            security: false\n        api:\n            pattern: ^/api\n            stateless: true\n            provider: database\n            jwt: ~\n            json_login:\n                check_path: /api/login\n                success_handler: lexik_jwt_authentication.handler.authentication_success\n                failure_handler: lexik_jwt_authentication.handler.authentication_failure\n                username_path: email \n                password_path: password   \n\n    access_control:\n        - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }\n        - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }   # Easy way to control access for large sections of your site\n\nwhen@test:\n    security:\n        password_hashers:\n            # By default, password hashers are resource intensive and take time. This is\n            # important to generate secure password hashes. In tests however, secure hashes\n            # are not important, waste resources and increase test times. The following\n            # reduces the work factor to the lowest possible values.\n            Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface:\n                algorithm: auto\n                cost: 4 # Lowest possible value for bcrypt\n                time_cost: 3 # Lowest possible value for argon\n                memory_cost: 10 # Lowest possible value for argon\n```\n\nDeclare the `/api/login` route in `config/routes/security.yaml`:\n\n**Note** : Here, we append the new content to the existing `security.yaml` file.\n\n```yaml\n### ... other routes ###\napi_login:\n    path: /api/login\n    methods: [POST]\n```\n\nGenerate JWT keys:\n\n```bash\nmkdir config/jwt; cd config/jwt\nopenssl genrsa -out private.pem 4096\nopenssl rsa -pubout -in private.pem -out public.pem\n```\n\nCreate a controller to check the authentication in `src/Controller/TestAuthController.php`:\n\n```php\n\u003c?php\nnamespace App\\Controller;\n\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\n\nclass TestAuthController extends AbstractController\n{\n  #[Route('/api/test/auth', name: 'api_test_auth')]\n  public function testAuth(): Response\n  {\n    /** @var App\\Entity\\User|null $user */\n    $user = $this-\u003egetUser();\n    \n    if ($user) {\n      return $this-\u003ejson(\n        [\n          'id' =\u003e $user-\u003egetId(), \n          'username' =\u003e $user-\u003egetUserIdentifier(),\n          'name' =\u003e $user-\u003egetFirstName() . ' ' . $user-\u003egetLastName(),\n          'roles' =\u003e $user-\u003egetRoles()\n        ]);\n    }\n    return $this-\u003ejson(['message' =\u003e 'Not authenticated'], 401);\n  }\n}\n```\n\n## Testing the API\n\nWhen making a `POST` request to the `/api/login` endpoint to authenticate, you **must** include the `Content-Type: application/json` header.\n\nIf this header is omitted, Symfony's `json_login` listener will not be triggered to handle the credentials. The request will fall through to the next security layer, the JWT authenticator, which will fail because no token is present. This results in a `401 Unauthorized` response with a \"JWT Token not found\" error message.\n\nHere is an example of a correct login request using `curl`:\n\n```bash\ncurl -X POST http://127.0.0.1:8000/api/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"mr@rktmb.org\", \"password\": \"mihamina\"}'\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frakotomandimby%2Fsymfony-enable-database-authentication","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frakotomandimby%2Fsymfony-enable-database-authentication","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frakotomandimby%2Fsymfony-enable-database-authentication/lists"}