AWS Cognito

How to Integrate AWS Cognito Authentication with Symfony

Houssem Guemer
Active Developement
13 min readApr 26, 2023

--

AWS Cognito is a popular user authentication and management service provided by Amazon Web Services. It offers a secure, easy-to-use solution for managing user authentication, authorization, and user data synchronization. On the other hand, Symfony is a widely used PHP web application framework that offers powerful features for building complex web applications.

In this tutorial, we will explore how to integrate AWS Cognito authentication with Symfony to build secure and scalable web applications. We will cover the steps required to set up AWS Cognito, configure Symfony for authentication, and implement user authentication using AWS Cognito in a Symfony application.

Cognito configuration on AWS Console

AWS Console Cognito search

To use AWS Cognito authentication with Symfony, we first need to set up a few things in the AWS Console :

  • Create a user pool if you haven’t already, which will serve as the container for your users and their information
  • Next, we need to create an app client for this user pool so that we can use Cognito’s OAuth 2.0 service. Make sure to take note of the “client id” and “client secret” as we will need them later
  • We also need to add a callback URL on the allowed callback URLs field. In this case, we’ll use “http://localhost:8000/security/cognito/check" for testing purposes.
  • Additionally, we need to create a group called “ADMIN” to be able to implement this with Symfony’s roles.
  • Finally, we’ll need at least two users for testing purposes, with one of them added to the “ADMIN” group.

That’s all we need from Cognito.

Symfony Logo

Symfony setup

At the time of writing this article, Symfony 6.2 was the newest version and used to test this code.

Symfony installation

Before we can integrate AWS Cognito authentication with Symfony, we need to have a Symfony project set up. If you already have an existing project, you can skip this step. Otherwise, we’ll walk through the process of creating a new Symfony project.

To create a new Symfony project, we can use the command “symfony new --webapp [project-name]”. This will create a new Symfony project with the name specified

Once we have our Symfony project set up, we can verify that everything is working correctly by running the server using the command “symfony server:start”. This will start the Symfony development server and allow us to test our project

Symfony packages

In order to integrate AWS Cognito authentication with Symfony, we’ll need to use a few packages that will help us build our authentication system

We can install these packages using Composer, the package manager for PHP. Simply run the command

composer require knpuniversity/oauth2-client-bundle cakedc/oauth2-cognito aws/aws-sdk-php
  • In order to configure our AWS Cognito authentication with Symfony, we’ll need to add a few entries to our .env file
  • These entries will contain sensitive information such as access keys, pool ID, client ID, and client secret, so it’s important to keep them private and secure.
  • To add the necessary entries, we’ll first need to navigate to our .env file and add the following lines between the “aws/aws-sdk-php” comments
###> aws/aws-sdk-php ###
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
COGNITO_POOL_ID=
COGNITO_CLIENT_ID=
COGNITO_CLIENT_SECRET=
COGNITO_REGION=
# Cognito domain, just the domain, without region and aws suffix
COGNITO_DOMAIN=
###< aws/aws-sdk-php ###
  • Make sure to replace the empty values with the correct information, which can be obtained from your AWS Console.
  • After adding the entries to our .env file, we’ll need to update our “config/packages/knpu_oauth2_client.yaml” file to use the values we set up in our .env file.
knpu_oauth2_client:
clients:
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
cognito: # name of our client
type: 'generic' # type
provider_class: '\CakeDC\OAuth2\Client\Provider\Cognito' # class provided by agent package
client_id: '%env(COGNITO_CLIENT_ID)%' # Cognito app id
client_secret: '%env(COGNITO_CLIENT_SECRET)%' # Cognito app secret
redirect_route: connect_cognito_check # name of the route where we wanna redirect callback, it mush be same as configued in the Cognito app
provider_options:
region: '%env(COGNITO_REGION)%'
cognitoDomain: '%env(COGNITO_DOMAIN)%' # Cognito domain, just the domain, without region and aws suffix
scope: 'email' #scopes configured in cognito

Symfony User

  • In order to proceed with integrating AWS Cognito authentication with Symfony, we need a user that we can log in as. If you don’t already have a user, we can create one using Symfony’s make command.
  • For the purpose of this tutorial, we’ll create a User entity that will be persisted in the database. However, if you prefer not to store your user in the database, you can use a user provider instead.
  • To create a User entity using Symfony’s make command, run the following command in your terminal:
php bin/console make:user
  • When prompted, select “YES” to store the user in the database.
  • Set “username” as the unique property, since the username is the unique identifier in Cognito.
  • We won’t need to hash/check the password as we will be using Cognito for login.

For more informations about what we just did I recommend you take a look at Symfony’s documentation

  • This command will generate a User class in the “src/Entity” directory, which we can then customize to fit our needs.
  • Once we’ve customized our User entity, we can use Symfony’s make command to generate the necessary database tables by running the following command:
php bin/console make:migration
php bin/console doctrine:migrations:migrate
  • With our User entity and database tables in place, we’re now ready to move on to integrating AWS Cognito authentication with Symfony.

Now we need to update our config/packages/security.yaml to look like this :

security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
id: App\Security\UserProvider
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider

# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall

# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
...

We added a provider for our user

Note: to make use of the authentication system you can implement access_controle to protect all the app links except the /login and /security/cognito/check which needs to be unprotected.

src/Security/UserProvider.php

<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
private $userRepository;

public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}

/**
* Symfony calls this method if you use features like switch_user
* or remember_me.
*
* If you're not using these features, you do not need to implement
* this method.
*
* @throws UserNotFoundException if the user is not found
*/
public function loadUserByIdentifier($identifier): UserInterface
{
$user = $this->userRepository->findOneByUsername($identifier);

if (!$user) {
throw new UserNotFoundException('User with identifier "' . $identifier . '" not found.');
}

return $user;
}

/**
* @deprecated since Symfony 5.3, loadUserByIdentifier() is used instead
*/
public function loadUserByUsername($username): UserInterface
{
return $this->loadUserByIdentifier($username);
}

/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*/
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}

// you can refetch the user data from Cognito here but I don't think it is necessary

return $user;
}

/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass(string $class): bool
{
return User::class === $class || is_subclass_of($class, User::class);
}

/**
* Upgrades the hashed password of a user, typically for using a better hash algorithm.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
// when hashed passwords are in use, this method should:
// 1. persist the new password in the user storage
// 2. update the $user object with $user->setPassword($newHashedPassword);
}
}

We will need a Cognito bridge to be able to use the aws-php-sdk

src/Bridge/AwsCognitoClient.php

<?php

namespace App\Bridge;

use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
use Aws\Credentials\Credentials;
use GuzzleHttp\Client;

class AwsCognitoClient
{
private $client;
private $cognitoPoolId;
private $cognitoClientId;
private $cognitoClientSecret;
private $cognitoDomain;

public function __construct(
string $awsAccessKeyId,
string $awsSecretAccessKey,
string $cognitoPoolId,
string $cognitoClientId,
string $cognitoClientSecret,
string $cognitoRegion,
string $cognitoDomain,
string $cognitoVersion = 'latest'
) {
$cognitoConfig = [
'region' => $cognitoRegion,
'version' => $cognitoVersion
];

// If AWS access key ID and secret access key are provided, set them as the credentials
// If the keys are not provided and the app is running on an EC2 instance, the skd will try to get them from the system
if ($awsAccessKeyId && $awsSecretAccessKey) {
$cognitoConfig['credentials'] = new Credentials($awsAccessKeyId, $awsSecretAccessKey);
}

// Initialize the Cognito client with the configured parameters
$this->client = new CognitoIdentityProviderClient($cognitoConfig);
// Store the Cognito pool ID, client ID, client secret, and domain as properties
$this->cognitoPoolId = $cognitoPoolId;
$this->cognitoClientId = $cognitoClientId;
$this->cognitoClientSecret = $cognitoClientSecret;
$this->cognitoDomain = $cognitoDomain;
}

/**
* Define a method to get the groups that a user belongs to
*
* @param string $username
* @return array|null
*/
public function getRolesForUsername(string $username)
{
// Call the AWS SDK method to list the groups for the specified user in the user pool
return $this->client->adminListGroupsForUser([
'UserPoolId' => $this->cognitoPoolId,
'Username' => $username
])->get('Groups');
}

/**
* Define a method to sign out a user from all devices
*
* @param string $username
* @return \Aws\Result
*/
public function globalSignOut(string $username)
{
// Call the AWS SDK method to sign out the specified user from all devices
return $this->client->adminUserGlobalSignOut([
'UserPoolId' => $this->cognitoPoolId,
'Username' => $username
]);
}
}

This is a good start for a Cognito bridge in Symfony

  • It initializes a CognitoIdentityProviderClient with the provided parameters and defines two methods: getRolesForUsername() and globalSignOut().
  • getRolesForUsername() takes a username as a parameter and returns an array of groups that the user belongs to in the Cognito user pool. It calls the adminListGroupsForUser() method from the CognitoIdentityProviderClient.
  • globalSignOut() takes a username as a parameter and signs out the user from all devices. It calls the adminUserGlobalSignOut() method from the CognitoIdentityProviderClient.
  • If you need to use other methods from the SDK, you can add them to this class as well. Check the sdk documentation to learn what the sdk is capable of.

Symfony Authenticator

We will be making a custom Symfony authenticator

  • It extends “ knpuniversity/oauth2-client-bundleOAuth2Authenticator
  • And uses “ CakeDC/oauth2-cognito” as a client provider.
  • It also uses our Cognito bridge to get the user’s roles.

src/Security/CognitoAuthenticator.php

<?php

namespace App\Security;

use App\Bridge\AwsCognitoClient;
use App\Entity\User;
use CakeDC\OAuth2\Client\Provider\CognitoUser;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
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;

class CognitoAuthenticator extends OAuth2Authenticator
{
private ClientRegistry $clientRegistry;
private RouterInterface $router;
private EntityManagerInterface $em;
private AWSCognitoClient $cognitoClient;

public function __construct(ClientRegistry $clientRegistry, RouterInterface $router, EntityManagerInterface $em, AWSCognitoClient $cognitoClient)
{
$this->clientRegistry = $clientRegistry;
$this->router = $router;
$this->em = $em;
$this->cognitoClient = $cognitoClient;
}

public function supports(Request $request): ?bool
{
// continue ONLY if the current ROUTE matches the check ROUTE
return $request->attributes->get('_route') === 'connect_cognito_check';
}

public function authenticate(Request $request): Passport
{
$client = $this->clientRegistry->getClient('cognito');
$accessToken = $this->fetchAccessToken($client);

// NOTE: Here you can store token into session if you are using stateful authentication.

return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
/** @var CognitoUser $user */
$cognitoUser = $client->fetchUserFromToken($accessToken);

$groups = $this->cognitoClient->getRolesForUsername($cognitoUser->getUsername());

//find or create the user
// change this part if your user isn't stored in the database
$user = $this->em->getRepository(User::class)->findOneBy(['username' => $cognitoUser->getUsername()]);

if (!$user) {
$user = new User();
}

$user->setUsername($cognitoUser->getUsername());
$user->setEmail($cognitoUser->getEmail() ?? $cognitoUser->getUsername());
$user->setCognitoId($cognitoUser->getId());

if (count($groups) > 0) {
$user->setRoles(
array_map(
function ($item) {
return 'ROLE_' . $item['GroupName'];
},
$groups
)
);
}

$this->em->persist($user);
$this->em->flush();

return $user;
})
);
}

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

return new RedirectResponse('/');
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());

return new Response($message, Response::HTTP_FORBIDDEN);
}
}
  • The supports() method checks if the current route matches the connect_cognito_check route, and returns true or false accordingly. This ensures that the authenticator is only used for the correct route.
  • The authenticate() method is responsible for authenticating the user. It first fetches the access token using the fetchAccessToken() method of the client registry. It then retrieves the user's information from AWS Cognito using the access token and the fetchUserFromToken() method of the Cognito client. The user's roles are then obtained using the getRolesForUsername() method of the AWSCognitoClient. The user is then found or created in the database, and its roles are set based on the AWS Cognito groups it belongs to. Finally, the user is persisted in the database and returned.
  • The onAuthenticationSuccess() method is called when the authentication is successful, and it returns a redirect response to the front-end base URL.
  • Note that on each login we are either updating or creating the user, this is important because it ensures that the user’s information is up to date. It helps prevent inconsistencies and allows us to store relevant information from the user’s Cognito profile in our application’s user entity for future use

Last thing to do is to update our security.yaml configuration to use our custom authenticator

...
firewalls:
...
main:
...
custom_authenticator: App\Security\CognitoAuthenticator

Symfony Controller

Now that our authentication system is ready we need to create a dedicated controller that handles the logic for user login.

src/Security/CognitoAuthenticator.php

<?php

namespace App\Controller;

use App\Bridge\AwsCognitoClient;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SecurityCognitoController extends AbstractController
{
private AWSCognitoClient $cognitoClient;

public function __construct(AWSCognitoClient $cognitoClient)
{
$this->cognitoClient = $cognitoClient;
}
/**
* Link to this controller to start the "connect" process
*/
#[Route("/login", name:"connect_cognito_start")]
public function connectAction(ClientRegistry $clientRegistry)
{
// will redirect to AWS Cognito!
return $clientRegistry
->getClient('cognito') // key used in config/packages/knpu_oauth2_client.yaml
->redirect();
}

/**
* After going to Cognito, you're redirected back here
* because this is the "callback URL" you configured
* in AWS Cognito APP settings
*/
#[Route("/security/cognito/check", name:"connect_cognito_check")]
public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
{
// ** if you want to *authenticate* the user, then
// leave this method blank and create a Guard authenticator
}

#[Route("/logout", name:"security_logout")]
public function logout(Security $security): RedirectResponse|Response
{
$this->cognitoClient->globalSignOut($this->getUser()->getUsername());
$security->logout(false);

return $this->redirect('/');
}
}

This code shows a simple implementation of user authentication and logout using AWS Cognito in a Symfony application

  • __construct(AWSCognitoClient $cognitoClient) is the constructor of the class that initializes the private $cognitoClient property with an instance of the AWSCognitoClient class.
  • connectAction(ClientRegistry $clientRegistry) is the method that handles the request to initiate the connection with AWS Cognito. The method returns a redirect response to the AWS Cognito login page.
  • connectCheckAction(Request $request, ClientRegistry $clientRegistry) is the method that handles the callback from AWS Cognito after the user has authenticated. This method is empty because we use our authenticator for the login logic.
  • logout(Security $security) is the method that handles the request to logout the user. The method uses the globalSignOut method of the $cognitoClient to sign out the user from AWS Cognito and then logs the user out of the application by calling the logout method of the Security object. Finally, it redirects the user to the home page.

Finalization

Now that we are done the setup of AWS Cognito and Symfony we can go ahead and test our code.
For this example you can make a simple route to the homepage

  #[Route("/", name:"app_homepage")]
public function home(Security $security): Response
{
// we return with html head and body tags as this is needed by Symfony profiler to attach to the page
return new Response(sprintf("<html lang='fr'><head><title>home</title></head><body>Welcome %s</body></html>", $security->getUser() ? $security->getUser()->getUserIdentifier() : 'Not Connected'));
}

If you go to the home page you will see

homepage not connected

Now navigate to the /login page and if everything is done correctly you should see Cognito’s authentication page (it may look differently based on your configuration)

Cognito authentication page

Go ahead and login as the user that you have created on the AWS Console. use the user that you added to the “ADMIN” group

homepage connected

As you can see we are now successfully connected and we have “ROLE_ADMIN” on our user ! (the other role is “ROLE_USER” which is the default symfony role).

To logout you can simply go to /logout route and you will be redirected to the homepage.

Congratulations !

You have successfully set up AWS Cognito authentication on your Symfony application. By following the steps outlined in this article, you have implemented a secure and scalable authentication system that can handle user login throug Cognito.

Overall, implementing AWS Cognito on Symfony provides numerous benefits such as improved security, scalability, and a streamlined user experience. By following the steps outlined in this article, you can leverage the power of AWS Cognito to enhance the authentication system of your Symfony application.

Extra notes

If you get an SSL certificate error

cURL error 60: SSL certificate problem: unable to get local issuer certificate.

You have to configure the SSL certificates for your server, for the dev version you can just deactivate the SSL verification for the “aws php sdk” and the “knpuniversity/oauth2-client-bundle” package by doing the following

src/Bridge/AwsCognitoClient.php

$cognitoConfig = [
'region' => $cognitoRegion,
'version' => $cognitoVersion,
'http' => [
'verify' => false,
]
];

config/services.yaml

services:
...
no_ssl_guzzle_client:
class: GuzzleHttp\Client
arguments:
- { verify: false }

config/packages/knpu_oauth2_client.yaml

knpu_oauth2_client:
http_client: no_ssl_guzzle_client
...

Thank you for reading, and if you have any questions feel free to leave a comment !

--

--