Skip to content

Instantly share code, notes, and snippets.

@martialboniou
Last active March 5, 2021 11:21
Show Gist options
  • Save martialboniou/451f7cdee41a61fb961aa2c9124b4c0b to your computer and use it in GitHub Desktop.
Save martialboniou/451f7cdee41a61fb961aa2c9124b4c0b to your computer and use it in GitHub Desktop.
Symfony first auth tutorial

Symfony tutorial

I'd start with this (first app tutorial)[https://auth0.com/blog/creating-your-first-symfony-app-and-adding-authentication/]; it looks obscure (I had to check some additional docs). In this tutorial, you'll create a custom authentication using Symfony Guard. NOTE: don't use: (Symfony tutorial)[https://auth0.com/blog/symfony-tutorial-building-a-blog-part-1]

NOTE: check later:

Composer

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
# I installed in ~/.scrubs/bin (where my own MAMP routine scripts are)
sudo mv composer.phar /usr/local/bin/composer

Symfony

curl -sS https://get.symfony.com/cli/installer | bash

set the $PATH:

# in .zsh/.zprofile
() {
  local SYMFONY_LOCAL_PATH="${HOME}/.symfony"
  local SYMFONY_LOCAL_BIN="${SYMFONY_LOCAL_PATH}/bin"
  if [[ -r "${SYMFONY_LOCAL_BIN}" ]]; then
    export PATH="${SYMFONY_LOCAL_BIN}:$PATH"
  fi
}

then as an example, try this:

symfony new --full HDBlog

This creates a blog HDBlog by calling composer (if installed) and Git-ify your project:

# don't run manually
composer create-project symfony/website-skeleton HDBlog --no-interation
git init HDBlog

Doctrine bundle

Install:

composer require symfony/maker-bundle
composer require orm
composer require symfony/security-bundle
composer require symfony/flex
composer require form validator
composer require annotations
composer require twig

The first one is mandatory for the tutorial:

  • maker-bundle: create commands, controllers, form classes, tests...;
  • orm: Object-Relational Mapper for Doctrine; a set of PHP libraries primarily focused on providing persistence services...
  • security-bundle: authorize authenticated users based on their roles;
  • flex: a tools that makes adding new features seamless through the use of a simple command.

Getting started

Symfony way

symfony new top-tech-companies

Composer way

composer create-project symfony/website-skeleton top-tech-companies

Running the application

Don't install composer require symfony/web-server-bundle --dev ^4.4.2 (is php bin/console server:run deprecated? or is it symfony5 that embeds the server implicitly?). Use:

symfony serve

Create a User Entity

Ensure maker-bundle and orm are here:

cd top-tech-companies
php bin/console make:user

Say yes to the default answers, so:

  • User;
  • yes to store user data in the database;
  • email;
  • yes to hash/check user passwords.

Edit src/Entity/User.php file and add a name variable:

/**
 * @ORM\Column(type="string", length=255)
 */
private $name;

public function getName(): ?string
{
  return $this->name;
}

public function setName(string $name): self
{
  $this->name = $name;
  return $this;
}

Setting up the Controllers

php bin/console make:controller ListController
php bin/console make:controller RegistrationController
php bin/console make:controller SecurityController

Edit src/Controller/ListController.php and replace the index method:

    public function index(): Response
    {
        $companies = [
            'Apple' => '$1.16 trillion USD',
            'Samsung' => '$298.68 billion USD',
            'Microsoft' => '$1.10 trillion USD',
            'Alphabet' => '$878.48 billion USD',
            'Intel Corporation' => '$245.82 billion USD',
            'IBM' => '$120.03 billion USD',
            'Facebook' => '$552.39 billion USD',
            'Hon Hai Precision' => '$38.72 billion USD',
            'Tencent' => '$3.02 trillion USD',
            'Oracle' => '$180.54 billion USD',
        ];

        return $this->render('list/index.html.twig', [
            'companies' => $companies,
        ]);
    }

Edit the src/Controller/Registrationcontroller.php:

namespace App\Controller;

use App\Entity\User;
use App\Form\UserType;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class RegistrationController extends AbstractController
{
    private $passwordEncoder;

    public function __construct(UserPasswordEncoderInterface $passwordEncoder)
    {
      $this->passwordEncoder = $passwordEncoder;
    }
    /**
     * @Route("/registration", name="registration")
     */
    public function index(Request $request): Response
    {
      $user = new User();

      $form = $this->createForm(UserType::class, $user);
      $form->handleRequest($request);

      if ($form->isSubmitted() && $form->isValid()) {
        // Encode the new users password
        $user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPassword()));
        // Set their role
        $user->setRoles(['ROLE_USER']);
        // Save
        $em = $this->getDoctrine()->getManager();
        $em->persist($user);
        $em->flush();

        return $this->redirectToRoute('app_login');
      }

      return $this->render('registration/index.html.twig', [
            'form' => $form->createView(),
      ]);
    }
}

Our last controller SecurityController will handle the login process for the User.

Understanding routes

Symfony recommends annotations because it's convenient to put the route and the controller in the same place. We will make use of annotations within our Controllers.

Creating the Forms

UserType

We referenced a form within the RegistrationController.php. Here's the form:

php bin/console make:form

Choose UserType and User as the name of the form class and the name of Entity respectively.

Edit src/Form/UserType.php. First add these modules in the header:

use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

And replace the content of the method buildFormadd in the class UserType by:

$builder
            ->add('email',    EmailType::class)
            ->add('name',     TextType::class)
            ->add('password', RepeatedType::class, [
              'type'           => PasswordType::class,
              'first_options'  => ['label' => 'Password'],
              'second_options' => ['label' => 'Confirm Password']
            ])
        ;

Login

php bin/console make:auth

Choose Login form authenticator, LoginFormAuthenticator as class name, SecurityController as a name for the controller class and, yes to generate a /logout URL. It creates a src/Security/LoginFormAuthenticator.php and a login.html.twig; it also updates config/packages/security.yaml and src/Controller/SecurityController.php.

Let's edit this last file by removing the index method and changing the @Route() annotation above the login method to "/":

    /**
     * @Route("/", name="app_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response

Configuring the database

Don't work with .env as it is not in .gitignore file. Set the environment variable DATABASE_URL to mysql://root:[email protected]:3306/techcompanies?serverVersion=8.0.23&charset=utf8 in .env.local.

Now create the database and the tables based on the User entity:

php bin/console doctrine:database:create
php bin/console doctrine:schema:update --force

Setting up authentification

Symfony ships with an awesome security component called Guard.

Add target: / in logout: node of config/packages/security.yaml.

In this file, there are several nodes:

  • encoders: configure how passwords are created;
  • providers: PHP class that will be used to load a user object from the session;
  • firewalls: this is used to define how users will be authenticated.

BEWARE: when you log, you should be redirected to the list view so fix this ASAP in src/Security/LoginFormAuthenticator.php:

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        return new RedirectResponse($this->urlGenerator->generate('list'));
        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

Setting up views

Symfony ships with a powerful templating engine called Twig.

The views needed for authentication in this app are in the templates/security directory. The base layout has also been configured in the templates/base.html.twig. These views use the Bootstrap CSS framework (see my Bootstrap tutorialrepublic digest).

Base

Edit templates/base.html.twig:

{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>{% block title %}Welcome!{% endblock %}</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    {% block stylesheets %}{% endblock %}
  </head>
  <body>
    <nav
      class="navbar navbar-expand-lg navbar-light bg-light"
      style="height: 70px;"
    >
      <a class="navbar-brand" href="#">Symfony</a>
      <div class="collapse navbar-collapse" id="navbarSupportedContent"></div>
      <ul class="nav navbar-nav navbar-right">
        {% if app.user %}
        <li><a class="nav-link" href="{{ path('list') }}">View List</a></li>
        <li><a class="nav-link" href="{{ path('app_logout') }}">Logout</a></li>
        {% else %}
        <li><a class="nav-link" href="{{ path('app_login') }}">Login</a></li>
        <li>
          <a class="nav-link" href="{{ path('registration') }}">Register</a>
        </li>
        {% endif %}
      </ul>
    </nav>
    {% block body %}{% endblock %} {% block javascripts %}{% endblock %}
  </body>
</html>

Note: the {% if app.user %} to display our list and a logout when logged; the login and register links are displayed otherwise.

List

Edit templates/list/index.html.twig:

{# templates/list/index.html.twig #} {% extends 'base.html.twig' %} {% block body %}
<div class="container">
  <div class="row">
    <div class="col-md-12">
      <div class="card bg-light mb-3 mt-3">
        <div class="card-body">
          <div class="card-header">List of top technology companies</div>
          {% if app.user != null %}
          <table class="table">
            <tr>
              <th>Company Name</th>
              <th>Market Value</th>
            </tr>
            {% for key, item in companies %}
            <tr>
              <td>{{ key }}</td>
              <td>{{ item }}</td>
            </tr>
            {% endfor %}
          </table>
          {% endif %}
        </div>
      </div>
      {% if app.user == null %}
      <a href="{{ path('app_login') }}" class="btn btn-info">
        You need to login to see the list 😜😜 >></a
      >
      {% endif %}
    </div>
  </div>
</div>
{% endblock %}

Login

Edit templates/security/login.html.twig:

{# templates/security/login.html.twig #} {% extends 'base.html.twig' %} {% block title %}Log in!{% endblock %} {% block body %}
<div class="container">
  <div class="row">
    <div class="col-md-10 ml-md-auto">
      <div class="">
        <div class="card bg-light mb-3 mt-5" style="width: 800px;">
          <div class="card-body">
            <form class="form-horizontal" role="form" method="post">
              {% if error %}
              <div class="alert alert-danger">
                {{ error.messageKey|trans(error.messageData, 'security') }}
              </div>
              {% endif %} {% if app.user %}
              <div class="mb-3">
                You are logged in as {{ app.user.username }},
                <a href="{{ path('app_logout') }}">Logout</a>
              </div>
              {% endif %}
              <div class="card-header mb-3">Please sign in</div>
              <div class="form-group">
                <label for="email" class="col-md-4 control-label"
                  >E-Mail Address</label
                >
                <div class="col-md-12">
                  <input
                    id="inputEmail"
                    type="email"
                    class="form-control"
                    name="email"
                    value="{{ last_username }}"
                    required
                    autofocus
                  />
                </div>
              </div>
              <div class="form-group">
                <label for="password" class="col-md-4 control-label"
                  >Password</label
                >
                <div class="col-md-12">
                  <input
                    id="inputPassword"
                    type="password"
                    class="form-control"
                    name="password"
                    required
                  />
                </div>
              </div>
              <input
                type="hidden"
                name="_csrf_token"
                value="{{ csrf_token('authenticate') }}"
              />
              <div class="form-group">
                <div class="col-md-12">
                  <button type="submit" class="btn btn-primary">
                    <i class="fa fa-btn fa-sign-in"></i> Login
                  </button>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
{% endblock %}

Registration

Edit templates/registration/index.html.twig:

{# templates/registration/index.html.twig #} {% extends 'base.html.twig' %} {% block body %}
<div class="container">
  <div class="row">
    <div class="col-md-10 ml-md-auto">
      <div class="card bg-light mb-3 mt-5" style="width: 800px">
        <div class="card-body">
          <div class="card-header mb-3">Registration Form</div>
          {{ form_start(form) }}
          <div class="form_group">
            <div class="col-md-12 mb-3">
              {{ form_row(form.name, {'attr': {'class': 'form-control'}}) }}
            </div>
          </div>
          <div class="form_group">
            <div class="col-md-12 mb-3">
              {{ form_row(form.email, {'attr': {'class': 'form-control'}}) }}
            </div>
          </div>
          <div class="form_group">
            <div class="col-md-12 mb-3">
              {{ form_row(form.password.first, {'attr': {'class':
              'form-control'}}) }}
            </div>
          </div>
          <div class="form_group">
            <div class="col-md-12 mb-3">
              {{ form_row(form.password.second, {'attr': {'class':
              'form-control'}}) }}
            </div>
          </div>
          <div class="form-group">
            <div class="col-md-8 col-md-offset-4" style="margin-top:5px;">
              <button type="submit" class="btn btn-primary">
                <i class="fa fa-btn fa-user"></i> Register
              </button>
            </div>
          </div>
          {{ form_end(form) }}
        </div>
      </div>
    </div>
  </div>
</div>
{% endblock %}

Profiler

composer require --dev profiler

You can disable the toolbar by setting the value of toolbar to false in config/packages/dev/web_profiler.yaml.

Auth0 with Symfony

Auth0 issues JSON web tokens on every login for your users. This means that you can have a solid identity infrastructure, including single sign-on, user management, support for social identity providers (Github, Twitter), enterprise identity providers (LDAP) and your database of users with just a few lines of code.

Create Application

Log in auth0.com and Create Application in the manage.auth0.com: choose Regular Web Applications then click on Settings in the Applications section and take a note of the General Settings section content and add these lines in your .env.local from the Domain, Client ID and Client Secret you get in Basic Information:

AUTH0_DOMAIN=<dev-5e8dxc22.eu.auth0.com from your Domain>
AUTH0_CLIENT_ID=<DU5bmSlNiwr1LAR6bMcRmljuHfbNKvK3 from Client ID>
AUTH0_CLIENT_SECRET=<VW3_g-007-fwn8H2ypXdQqRIZE0yRTPyY3si5yBswIZwpuYFfwnXIXUDhg9W6yDM from Client Secret>

Let this page open.

Install and configure the plugin

composer require hwi/oauth-bundle php-http/guzzle6-adapter php-http/httplug-bundle

Answer No to execute this recipe and edit config/bundles.php to add these two following lines in the returned array:

  Http\HttplugBundle\HttplugBundle::class => ['all' => true], // add this
  HWI\Bundle\OAuthBundle\HWIOAuthBundle::class => ['all' => true],// add this

Update config/routes.yaml to import the redirect.xml and login.xml with this configuration:

hwi_oauth_redirect:
  resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
  prefix: /connect

hwi_oauth_connect:
  resource: "@HWIOAuthBundle/Resources/config/routing/connect.xml"
  prefix: /connect

hwi_oauth_login:
  resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
  prefix: /login

auth0_login:
  path: /auth0/callback

auth0_logout:
  path: /auth0/logout

Create a new file named config/packages/hwi_oauth.yaml to set the name of the firewall in which the HWI0AuthBundle will be active as main:

hwi_oauth:
  firewall_names: [main]
  # https://github.com/hwi/HWIOAuthBundle/blob/master/Resources/doc/2-configuring_resource_owners.md
  resource_owners:
    auth0:
      type: oauth2
      class: 'App\Auth0ResourceOwner'
      client_id: "%env(AUTH0_CLIENT_ID)%"
      client_secret: "%env(AUTH0_CLIENT_SECRET)%"
      base_url: "https://%env(AUTH0_DOMAIN)%"
      scope: "openid profile email"

Here, we created a resource owner and referenced an Auth0ResourceOwner. Let's create another file called src/Auth0ResourceOwner.php:

<?php

namespace App;

use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class Auth0ResourceOwner extends GenericOAuth2ResourceOwner
{
    protected $paths = array(
        'identifier' => 'user_id',
        'nickname' => 'nickname',
        'realname' => 'name',
        'email' => 'email',
        'profilepicture' => 'picture',
    );

    public function getAuthorizationUrl($redirectUri, array $extraParameters = array())
    {
        return parent::getAuthorizationUrl($redirectUri, array_merge(array(
            'audience' => $this->options['audience'],
        ), $extraParameters));
    }

    protected function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);

        $resolver->setDefaults(array(
            'authorization_url' => '{base_url}/authorize',
            'access_token_url' => '{base_url}/oauth/token',
            'infos_url' => '{base_url}/userinfo',
            'audience' => '{base_url}/userinfo',
        ));

        $resolver->setRequired(array(
            'base_url',
        ));

        $normalizer = function (Options $options, $value) {
            return str_replace('{base_url}', $options['base_url'], $value);
        };

        $resolver->setNormalizer('authorization_url', $normalizer);
        $resolver->setNormalizer('access_token_url', $normalizer);
        $resolver->setNormalizer('infos_url', $normalizer);
        $resolver->setNormalizer('audience', $normalizer);
    }
}

Register the callback

Back to your Auth0 dashboard in the Application URIs section, add the:

  • Allowed Callback URLs: http://localhost:8000/auth0/callback;
  • Allowed Logout URLs: http://localhost:8000/auth0/logout;

Include Auth0's Centralized Login page

Edit templates/security/login.html.twig and add the Auth0 login after the end of </form>:

<a href="{{ path('hwi_oauth_service_redirect', {'service': 'auth0'}) }}" style="color: #fff;">
  <div class="card mb-3" style="background-color: #e8542e">
    <div class="card-body" style="padding: 0;">
      <img src="https://i.imgur.com/02VeCz0.png" height="40" />
      Connect with Auth0
    </div>
  </div>
</a>

Update the security layer

Edit config/security.yaml:

security:
  encoders:
    App\Entity\User:
      algorithm: auto
  # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
  providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
      entity:
        class: App\Entity\User
        property: email
    oauth_hwi:
      id: hwi_oauth.user.provider
  firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false
    main:
      anonymous: ~
      provider: oauth_hwi
      oauth:
        resource_owners:
          auth0: "/auth0/callback"
        login_path: /
        failure_path: /
        default_target_path: /list
        oauth_user_provider:
          service: hwi_oauth.user.provider
      guard:
        authenticators:
          - App\Security\LoginFormAuthenticator
      logout:
        path: app_logout
        target: /
  access_control:
    - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }

☑ It worked for me in Symfony 5 (2021-03-04)!

Digest by Martial BONIOU, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment