Fastest Symfony authentication - AWS Cognito integration
Alen Pokos
Posted on February 7, 2022
If you either love AWS services already, or are looking for a good option to use with your multiplatform products, AWS Cognito seems to be a good candidate to adopt into your technical stack.
For me it was unknown, but once I started digging into it, I find it to solve some problems I was bored with solving.
Setup Cognito on AWS
For starters, we should prepare our Cognito user pool.
We can do this via AWS UI. On the Cognito page we select "Create new user pool".
There are no really special settings you need to configure upon creation,
so choose either default settings or settings that fit your needs.
Only thing we need from user pool is to setup APP client.
Configure the APP client as you need.
For the callback URL we will target symfony route /security/cognito/check
ie: http://localhost:8000/security/cognito/check
.
If using local Symfony, be careful about using http or https domains. Spent some time to figure that one out because I was reckless :D.
Symfony
At the time of writing this article, Symfony 6 was the newest version and used to test this code.
Symfony installation
To be able to follow this post, you should either have an existing Symfony project or create a new one.
For ease of example, I will provide quick intro how to set up a new clean Symfony project.
We start with the new symfony project symfony new --webapp .
Please, see Documentation on installation and setup of Symfony project.
To verify we installed successfully, we can run server using symfony server:start
.
Install and configure packages for Cognito integration
Now that we have Symfony and Cognito ready, we can begin integration into our Symfony application.
We will use knpuniversity
bundle that provides variety of built-in connectors, but it is missing Cognito one.
For that, we will install Cognito agent provided by another package.
composer require knpuniversity/oauth2-client-bundle cakedc/oauth2-cognito
Once installation is complete we begin to configure the bundle.
Update config/packages/knpu_oauth2_client.yaml
:
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: '<AWS_CLIENT_ID>' # Cognito app id
client_secret: '<AWS_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: <AWS_REGION>
cognitoDomain: <AWS_COGNITO_DOMAIN> # Cognito domain, just the domain, without region and aws suffix
scope: 'email' #scopes configured in cognito
We also need some Symfony User. If you do not already have this, we can create it now.
If you already have user and user providers, you can skip this part and go to creation of connection controller.
Create user using maker bundle, bin/console make:user
with settings:
- store user in the database: NO # this is for demo purpose. In the real world you would probably wanna store it into a database
- unique property: Email # this can be whatever you need
- Hash/check password: NO # again, if you need it otherwise you can use it
This will generate:
-
src/Security/User.php
Our User class -
src/Security/UserProvider.php
User provider class used by Symfony security. It will also update the security configurationconfig/packages/security.yaml
with information about our new user provider.
If you are not familiar with these terms, I would urge you to read basics of Symfony security in Symfony Security documentation.
Next we need to create connection controller, that will provide routes and calls to oauth client bundle, ie:
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class SecurityCognitoController extends AbstractController
{
/**
* 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(){}
}
Logout is currently not configured, and you can customize this to your needs later.
Last code class we need to create is custom Authenticator, by following KNP docs and guidelines:
<?php
namespace App\Security;
use CakeDC\OAuth2\Client\Provider\CognitoUser;
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;
private $router;
public function __construct(ClientRegistry $clientRegistry, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->router = $router;
}
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);
// NOTE: here you can load/save user from storage such as database
$user = new User();
$user->setEmail($cognitoUser->getEmail());
return $user;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('app_homepage');
return new RedirectResponse($targetUrl);
// or, on success, let the request continue to be handled by the controller
//return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
}
Please note the targetUrl
in the onAuthenticationSuccess
method. Customize this to your needs.
I will also provide simple controller at the end of the article for convenience of testing.
Last thing is to update our security.yaml
configuration to use our custom provider:
...
firewalls:
...
main:
...
custom_authenticator: App\Security\CognitoAuthenticator
This is the main subsection, not the entire contents of the file. For our configuration, we only needed to add custom_authenticator
setting that is our authenticator class.
Once done and you try it, by opening http://localhost:8000/login
you should be redirected to AWS hosted login page.
After you login you will be redirected back to your symfony site with error exception from UserProvider::refreshUser
.
If you just wish to try it out, you can return $user
in this method.
Important!! This is for testing only, and it is not good practice. You should fit this to your implementation.
If you wish to see try it out, here is a simple test controller:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Security;
final class DefaultController
{
#[Route("/", name:"app_homepage")]
#[IsGranted("ROLE_USER")]
public function __invoke(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><head></head><body>Welcome %s</body></html>", $security->getUser()->getUserIdentifier()));
}
}
and you also need to update services.yml
and add section load new controller:
...
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: [ 'controller.service_arguments' ]
And there you have it. You can login into your Symfony application using AWS Cognito.
Feel free to clean up the code, improve the security of it and make it ready for the real world.
Security is important and sometimes hard topic. I would urge you to read Symfony Security documentation to better understand how it works, what are good practices and how to avoid pitfalls.
Have fun coding, creating and learning.
Posted on February 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.