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/composercurl -sS https://get.symfony.com/cli/installer | bashset 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 HDBlogThis 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 HDBlogInstall:
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 twigThe 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-companiescomposer create-project symfony/website-skeleton top-tech-companiesDon'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 serveEnsure maker-bundle and orm are here:
cd top-tech-companies
php bin/console make:userSay yes to the default answers, so:
User;yesto store user data in the database;email;yesto 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 SecurityControllerEdit 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:formChoose 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:authChoose 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): ResponseDon't work with .env as it is not in .gitignore file. Set the environment variable DATABASE_URL to mysql://root:root@127.0.0.1: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 --forceSymfony 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 profilerYou 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-bundleAnswer 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 thisUpdate 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/logoutCreate 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