Step-by-step Guide to use Auth0 with Symfony 6

Stefano Alletti
11 min readMar 14, 2023

--

Introduction

Auth0 is a very powerful solution to manage the authentication of your applications.
Even if its documentation is very well done, it is not immediate to understand how to use it.
The PHP SDKs allow you to do many things and today I would like to show how I use them to authenticate a user in a Symfony application.
I’ll show you step by step how to implement statefull authentication and stateless authentication.

So let’s try to write our application, first take a look at the workflow we want to implement.

Priority keys: Symfony application running and security installed

$ composer require symfony/security-bundle

Workflow

1) A command or route to create an account on Auth0 with a temporary password.
2) An invitation/activation email is sent to your account email with a link to change your password.
3) The login allows the user to connect via Auth0 and authenticate him on the Symfony application.

Step 1: create an account on Auth0 and send email to invite him to the application
Step 2: User click on link received and enter new password

Create an account on Auth0 and set up a test API

In order to implement the example we obviously need an account on Auth0.

Once you have created an account you are the manager of a tenant. By default you have an App and an Api Management that we will use for our example.

Now when you go up to your API in Application → Api’s, you click on the Auth0 Management API and then on ApiExplorer and Test tab so as to create an API for testing purposes.

Now we have all the data needed to configure our test application.

But before we just have to configure the callback url in the appropriate section of the Auth0. To do this we go into parameters settings of the application we want to use and we put for example http://localhost/calback in the “login url callback” field.

Setting Up Symfony App

  1. Install the SDK:
composer require auth0/auth0-php

2. Set the Auth0 data in the .env file so that it is available wherever you want it in the application. All this data is available in the setting tab of your app.

AUTH0_DOMAIN=<your domain>
AUTH0_MANAGEMENT_TOKEN=<your management api token>
AUTH0_CLIENT_ID=<your client id>
AUTH0_CLIENT_SECRET=<your client secret>
AUTH0_AUDIENCE=<your audience>
COOKIE_SECRET=<your cookie secret>
LOGIN_REDIRECT_URI=https://localhost/callback

Use this command for generate a secret cookie string:

openssl rand -hex 32

and bind the values:

# config/services.yaml
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$auth0Domain: '%env(AUTH0_DOMAIN)%'
$auth0ManagementToken: '%env(AUTH0_MANAGEMENT_TOKEN)%'
$auth0ClientId: '%env(AUTH0_CLIENT_ID)%'
$auth0ClientSecret: '%env(AUTH0_CLIENT_SECRET)%'
$auth0Audience: '%env(AUTH0_AUDIENCE)%'
$cookieSecret: '%env(COOKIE_SECRET)%'
$loginCallback: '%env(LOGIN_REDIRECT_URI)%'

Create User on your application using Auth0 Sdk

<?php

declare(strict_types=1);

namespace App\Controller;

use Auth0\SDK\API\Management;
use Auth0\SDK\Configuration\SdkConfiguration;

...

final class CreateUserController extends AbstractController
{
private SdkConfiguration $configuration;

public function __construct(
string $auth0Domain,
string $auth0ClientId,
string $auth0ManagementToken,
string $auth0ClientSecret,
string $auth0Audience,
string $cookieSecret,
string $loginCallback
) {
$this->configuration = new SdkConfiguration(
domain: $auth0Domain,
clientId: $auth0ClientId,
redirectUri: $loginCallback,
clientSecret: $auth0ClientSecret,
audience: [$auth0Audience],
cookieSecret: $cookieSecret,
managementToken: $auth0ManagementToken
);
}

#[Route('/create-auth0-user', name: 'create-auth0-user')]
public function __invoke(): Response
{
$auth0Management = new Management($this->configuration);

$response = $auth0Management->users()->create(
'Username-Password-Authentication',
[
'email' => 's.alletti@gmail.com',
'password' => 'Toto1234=:!',
'verify_email' => true,
"app_metadata" => [
"invitedToMyApp" => true,
]
]
);

return new Response($response->getBody()->getContents());
}
}

Now once you have called the route or launched the command if you go to the User section of your Auth0 interface you will find the user you have just created.

Send Invitaiton Email

The invitation email is sent from an Auth0 action, which we will position in the Post User Registration workflow:

The code will look something like this:

var AuthenticationClient = require('auth0').AuthenticationClient;

/**
* Handler that will be called during the execution of a PostUserRegistration flow.
*
* @param {Event} event - Details about the context and user that has registered.
* @param {PostUserRegistrationAPI} api - Methods and utilities to help change the behavior after a signup.
*/
exports.onExecutePostUserRegistration = async (event, api) => {
var authClient = new AuthenticationClient({
domain: 'dev-2z68qvf3kkuma6h2.eu.auth0.com',
clientId: 'QjJr4N5kOsnsHccUfula4aaZDqZ5LnFl',
});

var userAndConnection = {
email: event.user.email,
connection: '<Connection_DB>',
connection_id: event.connection.id,
};

authClient.requestChangePasswordEmail(userAndConnection, function(err){
console.error(err)
});
};

Here you can find the specific documentation: Home — Documentation

An invitation email is essentially a password change link reused as an invitation.

To be able to set up an invitation email you can add an information to the user data, in app_metadata when the user is created, as in the example above, and customize the password change template:

{% if user.app_metadata.invitedToMyApp == true %}
// user invitation email
{% else %}
// password change email
{% endif %}

Please note that customisation does not work in a test environment, you must use a third party email provider.

The user we created with the code above will thus receive an email of this type:

On clicking it is redirected to the password insertion screen:

Here it can choose its new password.

The redirection to the Login url is configurable via the template customisation page of the email to send:

Login

In your app, you must configure the login and callback urls. The url from which you will redirect the user to the login box page and the callback url to which Auth0 will redirect you after the user has authenticated (previously configured).

Now in your Symfony app you simply create a controller that redirects the user to the login via the Auth0 SDK:

<?php

declare(strict_types=1);

namespace App\Controller;

use Auth0\SDK\API\Authentication;
use Auth0\SDK\Auth0;
use Auth0\SDK\Configuration\SdkConfiguration;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

...

#[Route('/login', name: 'login')]
final class LoginController extends AbstractController
{
private SdkConfiguration $configuration;

public function __construct(
string $auth0Domain,
string $auth0ClientId,
string $auth0ClientSecret,
string $auth0Audience,
string $cookieSecret,
string $loginCallback
) {
$this->configuration = new SdkConfiguration(
domain: $auth0Domain,
clientId: $auth0ClientId,
redirectUri: $loginCallback,
clientSecret: $auth0ClientSecret,
audience: [$auth0Audience],
cookieSecret: $cookieSecret
);
}

public function __invoke(Request $request): Response
{

$auth0 = new Auth0($this->configuration);

return $this->redirect($auth0->login());
}
}

This will redirect the user to the login box :

Authentication

When the user logs in with the correct credentials, Auth0 will redirect him to the callback url we defined earlier (http://localhost/callback).

At this point we have two alternatives:

1) The backend handles authentication (statefull)
2) The frontend handles authentication and calls the backend with an access token (stateless).

In any case we create a User class extending Symfony’s UserInterface that will contain the authenticated user’s data :

<?php

declare(strict_types=1);

namespace App\Entity;

use Symfony\Component\Security\Core\User\UserInterface;

final class User implements UserInterface
{
private string $userId;

private ?string $username;

private ?string $email;

private ?\DateTimeImmutable $updateAt;

private array $roles;

private ?string $accessToken;

private ?bool $accessTokenExpired;

public function __construct(
string $userId,
?string $username,
?string $email,
?\DateTimeImmutable $updateAt,
?string $accessToken = null,
?bool $accessTokenExpired = null,
?array $roles = []
) {
$this->userId = $userId;
$this->username = $username;
$this->email = $email;
$this->updateAt = $updateAt;
$this->roles = $roles;
$this->accessToken = $accessToken;
$this->accessTokenExpired = $accessTokenExpired;
}

public function getUserId(): string
{
return $this->userId;
}

public function getUsername(): ?string
{
return $this->username;
}

public function getEmail(): ?string
{
return $this->email;
}

public function getUpdateAt(): ?\DateTimeImmutable
{
return $this->updateAt;
}

public function getAccessToken(): ?string
{
return $this->accessToken;
}

public function setAccessToken(?string $accessToken): void
{
$this->accessToken = $accessToken;
}

public function setAccessTokenExpired(?bool $accessTokenExpired): void
{
$this->accessTokenExpired = $accessTokenExpired;
}

public function isAccessTokenExpired(): ?bool
{
return $this->accessTokenExpired;
}

public function getRoles(): array
{
if (empty($this->roles)) {
return ['ROLE_USER'];
}

return $this->roles;
}

public function eraseCredentials()
{
// TODO: Implement eraseCredentials() method.
}

public function getUserIdentifier(): string
{
return $this->userId;
}
}

Statefull and statefull authentication

We configure the secutiry.yaml file like this:

providers:
auth0:
id: App\Security\Auth0UserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: auth0
custom_authenticators:
- App\Security\Auth0Authenticator
api:
pattern: ^/api
stateless: true
provider: auth0
custom_authenticators:
- App\Security\ApiAuth0Authenticator

Here the custom Authenticator for the statefull authentication:

<?php

declare(strict_types=1);

namespace App\Security;

...
...

final class Auth0Authenticator extends AbstractAuthenticator
{
use TargetPathTrait;

private const AUTH0_CALLBACK_ROUTE_NAME = 'auth0_callback';
private Auth0 $auth0;
private RouterInterface $router;

public function __construct(
string $auth0Domain,
string $auth0ClientId,
string $auth0ClientSecret,
string $auth0Audience,
string $cookieSecret,
string $loginCallback,
RouterInterface $router,
){
$configuration = new SdkConfiguration(
domain: $auth0Domain,
clientId: $auth0ClientId,
redirectUri: $loginCallback,
clientSecret: $auth0ClientSecret,
audience: [$auth0Audience],
cookieSecret: $cookieSecret,
);

$this->auth0 = new Auth0($configuration);
$this->router = $router;
}

public function supports(Request $request): ?bool
{
return $request->attributes->get('_route') === self::AUTH0_CALLBACK_ROUTE_NAME;
}

public function authenticate(Request $request): Passport
{
// retrieve connected user data via sdk
$this->auth0->exchange();
$userData = $this->auth0->getCredentials();

return new SelfValidatingPassport(
new UserBadge(json_encode($userData))
);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->router->generate('user-profile'));
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
throw new AccessDeniedHttpException($exception->getMessage(), $exception);
}
}

And the one used in the case of stateless:

<?php

declare(strict_types=1);

namespace App\Security;

use Auth0\SDK\Auth0;
use Auth0\SDK\Configuration\SdkConfiguration;
use Auth0\SDK\Token;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

final class ApiAuth0Authenticator extends AbstractAuthenticator
{
private Auth0 $auth0;

public function __construct(
string $auth0Domain,
string $auth0ClientId,
string $auth0ClientSecret,
string $auth0Audience,
string $cookieSecret,
string $loginCallback
){
$configuration = new SdkConfiguration(
domain: $auth0Domain,
clientId: $auth0ClientId,
redirectUri: $loginCallback,
clientSecret: $auth0ClientSecret,
audience: [$auth0Audience],
cookieSecret: $cookieSecret,
);

$this->auth0 = new Auth0($configuration);
}

public function supports(Request $request): ?bool
{
return
null !== $request->get('token') ||
(
$request->headers->has('Authorization') &&
stripos((string) $request->headers->get('Authorization'), 'Bearer ') === 0
);
}

public function authenticate(Request $request): Passport
{
$token = $this->extractToken($request);

$userToken = $this->auth0->decode(
token: $token,
tokenType: Token::TYPE_TOKEN
);


return new SelfValidatingPassport(
new UserBadge(json_encode(['user' => $userToken->toArray()], JSON_THROW_ON_ERROR))
);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$response = [
'errors' => [
(object) [
'status' => Response::HTTP_UNAUTHORIZED,
'title' => 'Authorization failed',
'detail' => strtr($exception->getMessageKey(), $exception->getMessageData())
]
]
];

return new JsonResponse($response, Response::HTTP_UNAUTHORIZED);
}

private function extractToken(Request $request): string
{
// Extract any available value from the authorization header
$param = $request->get('token');
$header = trim($request->headers->get('Authorization', ''));
$token = $param ?? $header;
$usingHeader = null === $param;

// Ensure the 'authorization' header is present in the request
if ('' === $token) {
throw new AuthenticationException('`Authorization` header not present.');
}

// Ensure the 'authorization' header includes a bearer prefixed JSON web token.
if ($usingHeader && 0 !== stripos($token, 'bearer ')) {
throw new AuthenticationException('`Authorization` header is malformed.');
}

// Strip the 'bearer' portion of the authorization string.
return str_ireplace('bearer ', '', $token);
}
}

The difference lies in the fact that in the case of stateless authentication the authenticator must decode the token passed by the front end to retrieve the user’s information. Instead in the case of a statefull authentication the authenticator uses the exchange and getCredentials methods to retrieve the same user data.

Both use the same UserProvider that returns the authenticated user:

<?php

declare(strict_types=1);

namespace App\Security;

use App\Entity\User;
use Auth0\SDK\API\Authentication;
use Auth0\SDK\API\Management;
use Auth0\SDK\Auth0;
use Auth0\SDK\Configuration\SdkConfiguration;

...

final class Auth0UserProvider implements UserProviderInterface
{
private SdkConfiguration $configuration;
private Authentication $auth0Authentication;

public function __construct(
string $auth0Domain,
string $auth0ClientId,
string $auth0ClientSecret,
string $auth0Audience,
string $cookieSecret,
string $loginCallback
){
$this->configuration = new SdkConfiguration(
domain: $auth0Domain,
clientId: $auth0ClientId,
redirectUri: $loginCallback,
clientSecret: $auth0ClientSecret,
audience: [$auth0Audience],
cookieSecret: $cookieSecret,
);

$this->auth0Authentication = new Authentication($this->configuration);
}

public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException();
}

return $user;
}

public function supportsClass(string $class): bool
{
return User::class === $class;
}

public function loadUserByIdentifier(string $identifier): UserInterface
{
return $this->fetchUser($identifier);
}

private function fetchUser(string $identifier): UserInterface
{
$userData = json_decode($identifier, true);

//retrieve api management token
$response = $this->auth0Authentication->clientCredentials();
$managementTokenResponse = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);

//set api management token
$this->configuration->setManagementToken($managementTokenResponse->access_token);

//retrieve roles
$auth0Management = new Management($this->configuration);
$userRoleResponse = $auth0Management->users()->getRoles($userData['user']['sub']);
$userRoles = json_decode($userRoleResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);

return new User(
$userData['user']['sub'],
$userData['user']['nickname'] ?? null,
$userData['user']['email'] ?? null,
isset($userData['user']['updated_at']) ? new \DateTimeImmutable($userData['user']['updated_at']) : null,
$userData['accessToken'] ?? null,
$userData['accessTokenExpired'] ?? null,
$this->roles($userRoles)
);
}

private function roles(array $userRoles): array
{
$roles = [];
foreach ($userRoles as $role) {
$roles[] = $role->name;
}

return $roles;
}
}

Roles

Auth0 users can have roles assigned, using SDKs you can do this:

//assign role
$apiManagement = new Management($this->configuration);
$apiManagement->users()->addRoles('auth0|63f372e3bac47012f95c38af', [ 'rol_U8PytoTxNbHFHpbl']);

And you can retrieve user roles like this:

//retrieve roles
$auth0Management = new Management($this->configuration);
$userRoleResponse = $auth0Management->users()->getRoles('auth0|63f372e3bac47012f95c38af');
$userRoles = json_decode($userRoleResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);h

Roles can be created via the interface or via the SDK below I have created a role via the interface:

So it is easy to play with user roles:

// security.yaml
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/user-profile, roles: ROLE_USER }
- { path: ^/api/me, roles: ROLE_USER }

Because, as we have seen, in the UserProvider we retrieve roles to construct the authenticated user:

//retrieve roles
$auth0Management = new Management($this->configuration);
$userRoleResponse = $auth0Management->users()->getRoles($userData['user']['sub']);
$userRoles = json_decode($userRoleResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);

return new User(
$userData['user']['sub'],
$userData['user']['nickname'] ?? null,
$userData['user']['email'] ?? null,
isset($userData['user']['updated_at']) ? new \DateTimeImmutable($userData['user']['updated_at']) : null,
$userData['accessToken'] ?? null,
$userData['accessTokenExpired'] ?? null,
$this->roles($userRoles)
);

Conclusions

For any modern application, a robust authentication system is essential but at the same time very complex. By using Auth0, one has the possibility of externalising this task and being able to use it for any source: web, mobile, api, etc.
And integrating it into a Symfony application is easy.

Thank you for reading me, I hope you enjoyed this article, you can follow me on Twitter or LinkedIn.

References:

--

--