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:
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
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
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.
symfony new top-tech-companies
composer create-project symfony/website-skeleton top-tech-companies
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
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;
}
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
.
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.
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']
])
;
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
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
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__);
}
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).
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.
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 %}
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 %}
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 %}
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 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.
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.
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);
}
}
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
;
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>
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